# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you 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.

"""Fallback-to-NumPy operator implementation."""

from distutils.version import StrictVersion
import functools
import ast
import numpy as np
from . import operator
from . import numpy as _mx_np  # pylint: disable=reimported
from .util import np_array, use_np
from .numpy.utils import _STR_2_DTYPE_
from .ndarray.numpy import _internal as _nd_npi
from .symbol.numpy import _internal as _sym_npi


def register(op_name, imperative=True, symbolic=True):
    """Register operators that fallback to NumPy in modules
    ``mxnet.ndarray.numpy._internal`` and ``mxnet.symbol.numpy._internal``."""
    def _save_op(mod):
        if hasattr(mod, op_name):
            raise ValueError('Duplicate name {} found in module {}'.format(op_name, str(mod)))
        op = functools.partial(mod.Custom, op_type=op_name)
        setattr(mod, op_name, op)

    def _register_helper(prop_cls):
        with np_array():
            prop_cls = operator.register(op_name)(prop_cls)
        if imperative:
            _save_op(_nd_npi)
        if symbolic:
            _save_op(_sym_npi)
        return prop_cls

    return _register_helper


@use_np  # enforce np shape and array semantics for all the methods in this class
class EmptyLike(operator.CustomOp):
    """Fallback to NumPy empty_like operator."""
    def __init__(self, dtype, order, subok, shape):
        super(EmptyLike, self).__init__()
        self._dtype = dtype
        self._order = order
        self._subok = subok
        self._shape = shape

    def forward(self, is_train, req, in_data, out_data, aux):
        np_version = np.version.version
        if StrictVersion(np_version) >= StrictVersion('1.6.0'):
            out = np.empty_like(in_data[0].asnumpy(), dtype=self._dtype, order=self._order,
                                subok=self._subok)
        else:
            out = np.empty_like(in_data[0].asnumpy())
        self.assign(out_data[0], req[0], _mx_np.array(out, ctx=in_data[0].ctx))

    def backward(self, req, out_grad, in_data, out_data, in_grad, aux):
        raise NotImplementedError('Operator empty_like does not support gradient computation')


@register('empty_like_fallback')
class EmptyLikeProp(operator.CustomOpProp):
    """Fallback empty_like operator properties."""
    def __init__(self, dtype, order, subok, shape):
        super(EmptyLikeProp, self).__init__(need_top_grad=True)
        self._dtype = None if dtype == 'None' else dtype
        self._order = order
        self._subok = ast.literal_eval(subok)
        self._shape = ast.literal_eval(shape)

    def list_arguments(self):
        return ['prototype']

    def infer_shape(self, in_shape):
        return (in_shape[0],), (in_shape[0],), ()

    def infer_type(self, in_type):
        if self._dtype is None:
            return (in_type[0],), (in_type[0],), ()
        else:
            return (in_type[0],), (_STR_2_DTYPE_[self._dtype],), ()

    def create_operator(self, ctx, in_shapes, in_dtypes):
        return EmptyLike(self._dtype, self._order, self._subok, self._shape)


@use_np  # enforce np shape and array semantics for all the methods in this class
class Resize(operator.CustomOp):
    """Fallback to NumPy resize operator."""
    def __init__(self, new_shape):
        super(Resize, self).__init__()
        self._new_shape = new_shape

    def forward(self, is_train, req, in_data, out_data, aux):
        out = np.resize(in_data[0].asnumpy(), self._new_shape)
        self.assign(out_data[0], req[0], _mx_np.array(out, dtype=out.dtype, ctx=out_data[0].ctx))

    def backward(self, req, out_grad, in_data, out_data, in_grad, aux):
        raise NotImplementedError('Operator resize does not support gradient computation')


@register('resize_fallback')
class ResizeProp(operator.CustomOpProp):
    """Fallback resize operator properties."""
    def __init__(self, new_shape):
        super(ResizeProp, self).__init__(need_top_grad=True)
        self._new_shape = ast.literal_eval(new_shape)

    def list_arguments(self):
        return ['a']

    def infer_shape(self, in_shape):
        out_shape = (self._new_shape,) if np.isscalar(self._new_shape) else self._new_shape
        return (in_shape[0],), (out_shape,), ()

    def create_operator(self, ctx, in_shapes, in_dtypes):
        return Resize(self._new_shape)


@use_np
class Unravel_index(operator.CustomOp):
    """Fallback to NumPy Unravel_index operator."""
    def __init__(self, shape):
        super(Unravel_index, self).__init__()
        self._shape = shape

    def forward(self, is_train, req, in_data, out_data, aux):
        out = np.unravel_index(in_data[0].asnumpy(), self._shape)
        self.assign(out_data[0], req[0], _mx_np.array(out, dtype=out[0].dtype, ctx=out_data[0].ctx))

    def backward(self, req, out_grad, in_data, out_data, in_grad, aux):
        raise NotImplementedError('Operator Unravel_index does not support gradient computation')


@register('unravel_index_fallback')
class Unravel_indexProp(operator.CustomOpProp):
    """Fallback unravel_index operator properties."""
    def __init__(self, shape):
        super(Unravel_indexProp, self).__init__(need_top_grad=True)
        self._shape = ast.literal_eval(shape)

    def list_arguments(self):
        return ['indices']

    def infer_shape(self, in_shape):
        dim_list = (1,) if np.isscalar(self._shape) else (len(self._shape),)
        out_shape = dim_list + tuple(in_shape[0])
        return (in_shape[0],), (out_shape,), ()

    def create_operator(self, ctx, in_shapes, in_dtypes):
        return Unravel_index(self._shape)


@use_np
class MultivariateNormal(operator.CustomOp):
    """Fallback to the front-end implementation of random.multivariate_normal."""
    def __init__(self, size=None):
        super(MultivariateNormal, self).__init__()
        self._size = size

    def forward(self, is_train, req, in_data, out_data, aux):
        loc = in_data[0]
        cov = in_data[1]
        if cov.dtype == np.float16:
            scale = _mx_np.linalg.cholesky(cov.astype(np.float32)).astype(np.float16)
        else:
            scale = _mx_np.linalg.cholesky(cov)
        #set context
        noise = _mx_np.random.normal(size=out_data[0].shape, dtype=loc.dtype, ctx=loc.ctx)
        out = loc + _mx_np.einsum('...jk,...j->...k', scale, noise)
        self.assign(out_data[0], req[0], out)

    def backward(self, req, out_grad, in_data, out_data, in_grad, aux):
        raise NotImplementedError('Operator random.multivariate_normal'
                                  ' does not support gradient computation')


@register('mvn_fallback')
class MultivariateNormalProp(operator.CustomOpProp):
    """Fallback np.random.multivariate_normal operator properties."""

    def __init__(self, size=None):
        super(MultivariateNormalProp, self).__init__(need_top_grad=True)
        self._size = ast.literal_eval(
            size) if size is not None else None

    def list_arguments(self):
        return ['mean', 'cov']

    def infer_shape(self, in_shape):
        loc_shape = in_shape[0]
        cov_shape = in_shape[1]
        if len(loc_shape) < 1:
            raise ValueError("mean must be at least 1 dimensional")
        if len(cov_shape) < 2:
            raise ValueError("cov must be at least 2 dimensional")
        if cov_shape[-1] != cov_shape[-2]:
            raise ValueError("the last two dimentions of the parameter cov have to be the same,"
                             " whereas the shape of cov is {}".format(cov_shape))
        if cov_shape[-1] != loc_shape[-1]:
            raise ValueError("mean and cov must have same length."
                             "The shape of mean is {} but the shape of cov is {}"
                             .format(loc_shape[-1:], cov_shape[-2:]))
        # handle shape mismatch here
        out_shape = np.broadcast(np.empty(loc_shape), np.empty(cov_shape[:-1])).shape
        if self._size is not None:
            self._size = (self._size,) if np.isscalar(
                self._size) else self._size
            out_shape = self._size + out_shape

        return in_shape, (out_shape,), ()

    def create_operator(self, ctx, in_shapes, in_dtypes):
        return MultivariateNormal(self._size)
