""" Copied this from PyTorch's gradcheck.py in order to make changes.
"""


import torch
from torch.autograd import Variable
from collections import Iterable


def iter_variables(x):
    if isinstance(x, Variable):
        if x.requires_grad:
            yield (x.grad.data, x.data) if x.grad is not None else (None, None)
    elif isinstance(x, Iterable):
        for elem in x:
            for result in iter_variables(elem):
                yield result


def zero_gradients(x):
    if isinstance(x, Variable):
        if x.grad is not None:
            x.grad.detach_()
            x.grad.data.zero_()
    elif isinstance(x, Iterable):
        for elem in x:
            zero_gradients(elem)


def make_jacobian(input, num_out):
    if isinstance(input, Variable) and not input.requires_grad:
        return None
    elif torch.is_tensor(input) or isinstance(input, Variable):
        return torch.zeros(input.nelement(), num_out)
    elif isinstance(input, Iterable):
        jacobians = list(filter(
            lambda x: x is not None, (make_jacobian(elem, num_out) for elem in input)))
        if not jacobians:
            return None
        return type(input)(jacobians)
    else:
        return None


def iter_tensors(x, only_requiring_grad=False):
    if torch.is_tensor(x):
        yield x
    elif isinstance(x, Variable):
        if x.requires_grad or not only_requiring_grad:
            yield x.data
    elif isinstance(x, Iterable):
        for elem in x:
            for result in iter_tensors(elem, only_requiring_grad):
                yield result


def contiguous(input):
    if torch.is_tensor(input):
        return input.contiguous()
    elif isinstance(input, Variable):
        return input.contiguous()
    elif isinstance(input, Iterable):
        return type(input)(contiguous(e) for e in input)
    return input


def get_numerical_jacobian(fn, input, eps=1e-3, use_double=False):
    # To be able to use .view(-1) input must be contiguous
    input = contiguous(input)
    if use_double:
        input = [torch.nn.Parameter(x.data.double()) if isinstance(x, torch.nn.Parameter) else x.double()
                 for x in input]
    output_size = fn(input).numel()
    jacobian = make_jacobian(input, output_size)

    # It's much easier to iterate over flattened lists of tensors.
    # These are reference to the same objects in jacobian, so any changes
    # will be reflected in it as well.
    x_tensors = [t for t in iter_tensors(input, True)]
    j_tensors = [t for t in iter_tensors(jacobian)]

    if use_double:
        outa = torch.DoubleTensor(output_size)
        outb = torch.DoubleTensor(output_size)
    else:
        outa = torch.FloatTensor(output_size)
        outb = torch.FloatTensor(output_size)

    # TODO: compare structure
    for x_tensor, d_tensor in zip(x_tensors, j_tensors):
        flat_tensor = x_tensor.view(-1)
        for i in range(flat_tensor.nelement()):
            orig = flat_tensor[i]
            flat_tensor[i] = orig - eps
            outa.copy_(fn(input).view(-1))
            flat_tensor[i] = orig + eps
            outb.copy_(fn(input).view(-1))
            flat_tensor[i] = orig

            outb.add_(-1, outa).div_(eps)
            d_tensor[i] = outb

    return tuple(x.float() for x in jacobian)


def get_analytical_jacobian(input, output):
    jacobian = make_jacobian(input, output.numel())
    jacobian_reentrant = make_jacobian(input, output.numel())
    grad_output = output.data.clone().zero_()
    flat_grad_output = grad_output.view(-1)
    reentrant = 0.0
    correct_grad_sizes = True

    for i in range(flat_grad_output.numel()):
        flat_grad_output.zero_()
        flat_grad_output[i] = 1
        for jacobian_c in (jacobian, jacobian_reentrant):
            zero_gradients(input)
            output.backward(grad_output, create_graph=True)
            for jacobian_x, (d_x, x) in zip(jacobian_c, iter_variables(input)):
                if d_x is None:
                    jacobian_x[:, i].zero_()
                else:
                    if d_x.size() != x.size():
                        correct_grad_sizes = False
                    jacobian_x[:, i] = (
                        d_x.to_dense() if d_x.is_sparse else d_x).view(-1)

    for jacobian_x, jacobian_reentrant_x in zip(jacobian, jacobian_reentrant):
        reentrant = max(
            reentrant, (jacobian_x - jacobian_reentrant_x).abs().max())

    return jacobian, reentrant, correct_grad_sizes


def _as_tuple(x):
    if isinstance(x, tuple):
        return x
    elif isinstance(x, list):
        return tuple(x)
    else:
        return x,


def _differentiable_outputs(x):
    return tuple(o for o in _as_tuple(x) if o.requires_grad)


def gradcheck(func, inputs, eps=1e-6, atol=1e-5, rtol=1e-3, retol=1e-4, raise_exception=True,
              func_numerical=None, use_double=False):
    """Check gradients computed via small finite differences
       against analytical gradients
    The check between numerical and analytical has the same behaviour as
    numpy.allclose https://docs.scipy.org/doc/numpy/reference/generated/numpy.allclose.html
    meaning it check that
        absolute(a - n) <= (atol + rtol * absolute(n))
    is true for all elements of analytical jacobian a and numerical jacobian n.
    Args:
        func: Python function that takes Variable inputs and returns
            a tuple of Variables
        inputs: tuple of Variables
        eps: perturbation for finite differences
        atol: absolute tolerance
        rtol: relative tolerance
        retol: re-entrant tolerance
        raise_exception: bool indicating whether to raise an exception if
            gradcheck fails. The exception gives more information about the
            exact nature of the failure. This is helpful when debugging gradchecks.
        func_numerical: Python function identical to func but to be used when doing
                        numerical derivatives.
        use_double: If true, will pass double tensors to func when computing numerical
                    grads.
    Returns:
        True if all differences satisfy allclose condition
    """
    output = _differentiable_outputs(func(*inputs))

    def fail_test(msg):
        if raise_exception:
            raise RuntimeError(msg)
        return False

    for i, o in enumerate(output):
        if not o.requires_grad:
            continue

        if func_numerical is None:
            func_numerical = func

        def fn(input):
            return _as_tuple(func_numerical(*input))[i].data

        analytical, reentrant, correct_grad_sizes = get_analytical_jacobian(
            _as_tuple(inputs), o)
        numerical = get_numerical_jacobian(fn, inputs, eps, use_double)

        if reentrant > retol:
            return fail_test('not reentrant, %g exceeded reentrance tolerance of %g.'
                             % (reentrant, retol))

        for j, (a, n) in enumerate(zip(analytical, numerical)):
            if a.numel() != 0 or n.numel() != 0:
                if not ((a - n).abs() <= (atol + rtol * n.abs())).all():
                    return fail_test('for output no. %d,\n numerical:%s\nanalytical:%s\n' % (j, numerical, analytical))

        if not correct_grad_sizes:
            return fail_test('not correct_grad_sizes')

    # check if the backward multiplies by grad_output
    zero_gradients(inputs)
    output = _differentiable_outputs(func(*inputs))
    if any([o.requires_grad for o in output]):
        torch.autograd.backward(