# Copyright (c) 2020 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.
"""
mpc math op layers.
"""

from ..framework import MpcVariable
from ..framework import check_mpc_variable_and_dtype
from ..mpc_layer_helper import MpcLayerHelper
from .ml import reshape

__all__ = [
    'mean',
    'square',
    'sum',
    'square_error_cost',
    'reduce_sum',
    'scale'
]


def mean(x, name=None):
    """
    Mean Operator calculates the mean of all elements in X.

    Args:
        x(MpcVariable): (Tensor) The input of mean op
        name(basestring|None): Name of the output.
    Returns:
        out(MpcVariable): (Tensor) The output of mean op
    Examples: todo
    """
    helper = MpcLayerHelper("mean", **locals())
    check_mpc_variable_and_dtype(x, 'x', ['int64'], 'mean')
    if name is None:
        out = helper.create_mpc_variable_for_type_inference(dtype=x.dtype)
    else:
        out = helper.create_mpc_variable(
            name=name, dtype=x.dtype, persistable=False)

    helper.append_op(
        type="mpc_mean", inputs={"X": x}, attrs={}, outputs={"Out": out})

    return out


def square(x, name=None):
    """
    square Operator calculates the square of each element in X.

    Args:
        x(MpcVariable): (Tensor) The input of square op
        name(basestring|None): Name of the output.
    Returns:
        out(MpcVariable): (Tensor) The output of square op
    Examples: todo
    """
    helper = MpcLayerHelper("square", **locals())
    check_mpc_variable_and_dtype(x, 'x', ['int64'], 'square')
    if name is None:
        out = helper.create_mpc_variable_for_type_inference(dtype=x.dtype)
    else:
        out = helper.create_mpc_variable(
            name=name, dtype=x.dtype, persistable=False)

    helper.append_op(
        type="mpc_square", inputs={"X": x}, attrs={}, outputs={"Out": out})

    return out


def sum(x):
    """
    Sum Operator calculates the sum of all elements in X.

    Args:
        x (MpcVariable|list(MpcVariable)) The input of sum op
        name(basestring|None): Name of the output.
    Returns:
        out(MpcVariable): (Tensor) The output of mean op
    Examples: todo
    """
    helper = MpcLayerHelper("sum", **locals())
    out = helper.create_mpc_variable_for_type_inference(dtype=helper.input_dtype('x'))
    helper.append_op(
        type="mpc_sum",
        inputs={"X": x},
        outputs={"Out": out},
        attrs={'use_mkldnn': False})
    return out


def square_error_cost(input, label):
    """
    This op accepts input predictions and target label and returns the
    squared error cost.
    For predictions label, and target label, the equation is:
    .. math::
        Out = (input - label)^2
    Parameters:
        input (MpcVariable): Input tensor, the data type should be float32.
        label (MpcVariable): Label tensor, the data type should be float32.
    Returns:
        The tensor variable storing the element-wise squared error \
                  difference between input and label.
    Return type: MpcVariable.
    Examples: todo
    """
    helper = MpcLayerHelper('square_error_cost', **locals())
    minus_out = helper.create_mpc_variable_for_type_inference(dtype=input.dtype)
    helper.append_op(
        type='mpc_elementwise_sub',
        inputs={'X': [input],
                'Y': [label]},
        outputs={'Out': [minus_out]})

    square_out = helper.create_mpc_variable_for_type_inference(dtype=input.dtype)
    helper.append_op(
        type='mpc_square',
        inputs={'X': [minus_out]},
        outputs={'Out': [square_out]})
    return square_out



def reduce_sum(input, dim=None, keep_dim=False, name=None):
    """
    Computes the sum of tensor elements over the given dimension.

    Args:
        input (MpcVariable) The input of sum op name(basestring|None): Name of the output.
        dim (list|int, optional): The dimensions along which the sum is performed. If
            :attr:`None`, sum all elements of :attr:`input` and return a
            Tensor variable with a single element, otherwise must be in the
            range :math:`[-rank(input), rank(input))`. If :math:`dim[i] < 0`,
            the dimension to reduce is :math:`rank + dim[i]`.
            NOTE: 'dim' should not contain 0, becausedims[0] is share number.
        keep_dim (bool, optional): Whether to reserve the reduced dimension in the
            output Tensor. The result tensor will have one fewer dimension
            than the :attr:`input` unless :attr:`keep_dim` is true, default
            value is False.
        name(str, optional): The default value is None.  Normally there is no need for
            user to set this property.  For more information, please refer to :ref:`api_guide_Name`
    Returns:
        Variable: Tensor, results of summation operation on the specified dim of input tensor,
        it's data type is the same as input's Tensor.
    Raises:
        TypeError, if out data type is different with the input data type.

    Returns:
        out(MpcVariable): (Tensor) The output of mean op
    Examples:
        .. code-block:: python

            import paddle_fl.mpc as pfl_mpc

            pfl_mpc.init("aby3", int(args.role), "localhost", args.server, int(args.port))
            data_1 = pfl_mpc.data(name='x', shape=[3, 3], dtype='int64')
            pfl_mpc.layers.reshape(data_1, [1, 2])  # shape: [2, 1, 1]
            # data_1 = np.full(shape=(3, 4), fill_value=2)
            # reduce_sum: 24
    """
    if dim is not None and not isinstance(dim, list):
        dim = [dim]

    if dim != None and dim != []:
        if 0 in dim:
            raise ValueError(
                "'dim' should not contain 0, because dim[0] is share number."
            )
    else:
        dim = [i for i in range(len(input.shape))][1:]

    attrs = {
        'dim': dim,
        'keep_dim': keep_dim,
        'reduce_all': False
    }
    check_mpc_variable_and_dtype(
        input, 'input', ['int64'], 'reduce_sum')
    helper = MpcLayerHelper('reduce_sum', **locals())
    out = helper.create_mpc_variable_for_type_inference(dtype=helper.input_dtype())
    helper.append_op(
        type='reduce_sum',
        inputs={'X': input},
        outputs={'Out': out},
        attrs=attrs)
    if out.shape  == (2,):
        out = reshape(out, list(out.shape) + [1])
    return out


def scale(x, scale=1.0, bias=0.0, bias_after_scale=True, act=None, name=None):
    """
    Scale operator.
    Putting scale and bias to the input Tensor as following:
    ``bias_after_scale`` is True:
    .. math::
                            Out=scale*X+bias
    ``bias_after_scale`` is False:
    .. math::
                            Out=scale*(X+bias)
    Args:
        x(MpcVariable): Input N-D Tensor of scale operator. Data type should be int64.
        scale(float|Variable): The scale factor of the input, it should be a float number or a Variable with shape [1] and data type as float32.
        bias(float): The bias to be put on the input.
        bias_after_scale(bool): Apply bias addition after or before scaling. It is useful for numeric stability in some circumstances.
        act(str, optional): Activation applied to the output such as tanh, softmax, sigmoid, relu.
        name(str, optional): The default value is None. Normally there is no need for user to set this property.  For more information, please refer to :ref:`api_guide_Name`
    Returns:
        Variable(Tensor|LoDTensor): Output tensor of scale operator, with shape and data type same as input.
    Examples:
        .. code-block:: python
            import paddle_fl.mpc as pfl_mpc
            pfl_mpc.init("aby3", int(args.role), "localhost", args.server, int(args.port))
            data_1 = pfl_mpc.data(name='x', shape=[3, 3], dtype='int64')
            pfl_mpc.layers.scale(data_1, 0.5)
    """

    check_mpc_variable_and_dtype(x, "x", ['int64'], "scale")
    inputs = {'X': [x]}
    attrs = {
        'bias': float(bias),
        'bias_after_scale': bias_after_scale,
    }
    if isinstance(scale, MpcVariable):
        inputs['ScaleTensor'] = [scale]
    else:
        attrs['scale'] = float(scale)
    helper = MpcLayerHelper('scale', **locals())
    out = helper.create_mpc_variable_for_type_inference(dtype=x.dtype)

    helper.append_op(
        type='mpc_scale', inputs=inputs, outputs={'Out': out}, attrs=attrs)
    return helper.append_activation(out)

