# Copyright 2020-present the HuggingFace Inc. team.
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import types

import numpy as np
import paddle
from paddle.common_ops_import import LayerHelper

from ...utils.log import logger


def npu_accelerate_plugin(optimizer):
    """npu_accelerate_plugin uses the flatten_param_grads method to speed up the performance of the model on NPU devices.
    flatten_param_grads method will be added to `step` function of optimizer.

    Args:
        optimizer (`paddle.optimizer.Optimizer`):
            The Optimizer whose `step` method will be modified.
    """
    optimizer.step = types.MethodType(_optimizer_step_with_flatten_param_grads, optimizer)


def _optimizer_step_with_flatten_param_grads(optimizer):
    if not isinstance(optimizer._param_groups[0], dict):
        params_grads = []
        for param in optimizer._param_groups:
            if param.stop_gradient:
                continue
            if param._grad_ivar() is not None:
                grad_var = param._grad_ivar()
                params_grads.append((param, grad_var))

        # currently, only support ClipGradByGlobalNorm and without regularization.
        if isinstance(params_grads, list) and optimizer.regularization is None:
            if optimizer._grad_clip is None or isinstance(optimizer._grad_clip, paddle.nn.ClipGradByGlobalNorm):
                params_grads = _flatten_param_grads(optimizer, params_grads)

        optimizer._apply_optimize(
            loss=None,
            startup_program=None,
            params_grads=params_grads,
            param_group_idx=0,
        )
    else:
        raise RuntimeError("flatten_param_grads is not supported when _param_groups[0] is dict.")


def _flatten_param_grads(optimizer, params_grads):
    optimizer.helper = LayerHelper(optimizer.__class__.__name__)
    need_flatten_params = []
    need_flatten_grads = []
    for p, g in params_grads:
        if g is None:
            continue
        g.persistable = True
        if getattr(p, "need_clip", True) is False or getattr(p, "regularizer", None) is not None:
            logger.warning(
                f"flatten_param_grads=True will be discarded since paramter {p.name}'s need_clip is False or "
                "the regularizer is set."
            )
            return params_grads

        need_flatten_params.append(p)
        need_flatten_grads.append(g)

    shape = [np.prod(p.shape) for p in need_flatten_params]

    flatten_param = optimizer.helper.create_global_variable(
        name="flatten_param",
        persistable=True,
        dtype=need_flatten_params[0].dtype,
        shape=[np.sum(shape)],
        belong_to_optimizer=True,
    )

    flatten_grad = optimizer.helper.create_global_variable(
        name="flatten_grad",
        persistable=True,
        dtype=need_flatten_grads[0].dtype,
        shape=[np.sum(shape)],
        belong_to_optimizer=True,
    )

    flatten_param.stop_gradient = False
    # In the final state of the dynamic graph, the `coalesce_tensor` op
    # does not support passing the output as an input into the op in
    # temporary, so _legacy_C_ops is temporarily used here.
    # `use_align` is set to false, which is different from the behavior
    # under static graphs. `use_align` can be set to true after calling
    # the coalesce_tensor op of the final state (_C_ops).
    paddle._legacy_C_ops.coalesce_tensor(
        need_flatten_params,
        need_flatten_params,
        flatten_param,
        "copy_data",
        True,
        "use_align",
        False,
        "dtype",
        need_flatten_params[0].dtype,
    )

    paddle._legacy_C_ops.coalesce_tensor(
        need_flatten_grads,
        need_flatten_grads,
        flatten_grad,
        "copy_data",
        True,
        "use_align",
        False,
        "dtype",
        need_flatten_grads[0].dtype,
    )
    return [(flatten_param, flatten_grad)]
