""" Change index / Reorder / Reverse order of limits of Sums and Products"""
from sympy.concrete import Product, Sum
from sympy import S


class ReorderError(NotImplementedError):
    """
    Exception raised when trying to reorder dependent limits.
    """
    def __init__(self, expr, msg):
        super(ReorderError, self).__init__(
            "%s could not be reordered: %s." % (expr, msg))


def index(expr, x):
    """
    Return the index of a limit variable.

    Usage
    =====

    ``index(expr, x)``  returns the index of the limit variable ``x`` in the
    limits of ``expr``. Note that we start counting with 0 at the inner-most
    limits tuple.

    Examples
    ========

    >>> from sympy.concrete.simplification import index
    >>> from sympy.abc import x, y, a, b, c, d
    >>> from sympy import Sum, Product
    >>> index(Sum(x*y, (x, a, b), (y, c, d)), x)
    0
    >>> index(Sum(x*y, (x, a, b), (y, c, d)), y)
    1
    >>> index(Product(x*y, (x, a, b), (y, c, d)), x)
    0
    >>> index(Product(x*y, (x, a, b), (y, c, d)), y)
    1

    See Also
    ========

    sympy.concrete.simplification.change_index,
    sympy.concrete.simplification.reorder_limit,
    sympy.concrete.simplification.reorder,
    sympy.concrete.simplification.reverse_order
    """
    if isinstance(expr, Sum) or isinstance(expr, Product):
        variables = [limit[0] for limit in expr.limits]

        if variables.count(x) != 1:
            raise ValueError(expr, "Number of instances of variable not equal to one")
        else:
            return variables.index(x)


def change_index(expr, var, trafo, newvar=None):
    """
    Change index of a Sum or Product.

    Perform a linear transformation `x \mapsto a x + b` on the index variable
    `x`. For `a` the only values allowed are `\pm 1`. A new variable to be used
    after the change of index can also be specified.

    Usage
    =====

    ``change_index(expr, var, trafo, newvar=None)`` where ``var`` specifies the
    index variable `x` to transform. The transformation ``trafo`` must be linear
    and given in terms of ``var``. If the optional argument ``newvar`` is
    provided then ``var`` gets replaced by ``newvar`` in the final expression.

    Examples
    ========

    >>> from sympy.concrete.simplification import change_index
    >>> from sympy import Sum, Product, simplify
    >>> from sympy.abc import x, y, a, b, c, d, u, v, i, j, k, l

    >>> S = Sum(x, (x, a, b))
    >>> S.doit()
    -a**2/2 + a/2 + b**2/2 + b/2

    >>> Sn = change_index(S, x, x + 1, y)
    >>> Sn
    Sum(y - 1, (y, a + 1, b + 1))
    >>> Sn.doit()
    -a**2/2 + a/2 + b**2/2 + b/2

    >>> Sn = change_index(S, x, -x, y)
    >>> Sn
    Sum(-y, (y, -b, -a))
    >>> Sn.doit()
    -a**2/2 + a/2 + b**2/2 + b/2

    >>> Sn = change_index(S, x, x+u)
    >>> Sn
    Sum(-u + x, (x, a + u, b + u))
    >>> Sn.doit()
    -a**2/2 - a*u + a/2 + b**2/2 + b*u + b/2 - u*(-a + b + 1) + u
    >>> simplify(Sn.doit())
    -a**2/2 + a/2 + b**2/2 + b/2

    >>> Sn = change_index(S, x, -x - u, y)
    >>> Sn
    Sum(-u - y, (y, -b - u, -a - u))
    >>> Sn.doit()
    -a**2/2 - a*u + a/2 + b**2/2 + b*u + b/2 - u*(-a + b + 1) + u
    >>> simplify(Sn.doit())
    -a**2/2 + a/2 + b**2/2 + b/2

    >>> P = Product(i*j**2, (i, a, b), (j, c, d))
    >>> P
    Product(i*j**2, (i, a, b), (j, c, d))
    >>> P2 = change_index(P, i, i+3, k)
    >>> P2
    Product(j**2*(k - 3), (k, a + 3, b + 3), (j, c, d))
    >>> P3 = change_index(P2, j, -j, l)
    >>> P3
    Product(l**2*(k - 3), (k, a + 3, b + 3), (l, -d, -c))

    When dealing with symbols only, we can make a
    general linear transformation:

    >>> Sn = change_index(S, x, u*x+v, y)
    >>> Sn
    Sum((-v + y)/u, (y, b*u + v, a*u + v))
    >>> Sn.doit()
    -v*(a*u - b*u + 1)/u + (a**2*u**2/2 + a*u*v + a*u/2 - b**2*u**2/2 - b*u*v + b*u/2 + v)/u
    >>> simplify(Sn.doit())
    a**2*u/2 + a/2 - b**2*u/2 + b/2

    However, the last result can be inconsistent with usual
    summation where the index increment is always 1. This is
    obvious as we get back the original value only for ``u``
    equal +1 or -1.

    See Also
    ========

    sympy.concrete.simplification.index,
    sympy.concrete.simplification.reorder_limit,
    sympy.concrete.simplification.reorder,
    sympy.concrete.simplification.reverse_order
    """
    if newvar is None:
        newvar = var

    limits = []
    for limit in expr.limits:
        if limit[0] == var:
            p = trafo.as_poly(var)
            if p.degree() != 1:
                raise ValueError("Index transformation is not linear")
            alpha = p.coeff_monomial(var)
            beta = p.coeff_monomial(S.One)
            if alpha.is_number:
                if alpha == S.One:
                    limits.append((newvar, alpha*limit[1] + beta, alpha*limit[2] + beta))
                elif alpha == S.NegativeOne:
                    limits.append((newvar, alpha*limit[2] + beta, alpha*limit[1] + beta))
                else:
                    raise ValueError("Linear transformation results in non-linear summation stepsize")
            else:
                # Note that the case of alpha being symbolic can give issues if alpha < 0.
                limits.append((newvar, alpha*limit[2] + beta, alpha*limit[1] + beta))
        else:
            limits.append(limit)

    function = expr.function.subs(var, (var - beta)/alpha)
    function = function.subs(var, newvar)

    if isinstance(expr, Sum):
        return Sum(function, *tuple(limits))
    elif isinstance(expr, Product):
        return Product(function, *tuple(limits))
    else:
        raise NotImplementedError(expr, "change_index only implemented for Sum and Product")


def reorder(expr, *arg):
    """
    Reorder limits in a expression containing a Sum or a Product.

    Usage
    =====

    ``reorder(expr, *arg)`` reorders the limits in the expression ``expr``
    according to the list of tuples given by ``arg``. These tuples can
    contain numerical indices or index variable names or involve both.

    Examples
    ========

    >>> from sympy.concrete.simplification import reorder
    >>> from sympy import Sum, Product
    >>> from sympy.abc import x, y, z, a, b, c, d, e, f

    >>> reorder(Sum(x*y, (x, a, b), (y, c, d)), (x, y))
    Sum(x*y, (y, c, d), (x, a, b))

    >>> reorder(Sum(x*y*z, (x, a, b), (y, c, d), (z, e, f)), (x, y), (x, z), (y, z))
    Sum(x*y*z, (z, e, f), (y, c, d), (x, a, b))

    >>> P = Product(x*y*z, (x, a, b), (y, c, d), (z, e, f))
    >>> reorder(P, (x, y), (x, z), (y, z))
    Product(x*y*z, (z, e, f), (y, c, d), (x, a, b))

    We can also select the index variables by counting them, starting
    with the inner-most one:

    >>> reorder(Sum(x**2, (x, a, b), (x, c, d)), (0, 1))
    Sum(x**2, (x, c, d), (x, a, b))

    And of course we can mix both schemes:

    >>> reorder(Sum(x*y, (x, a, b), (y, c, d)), (y, x))
    Sum(x*y, (y, c, d), (x, a, b))
    >>> reorder(Sum(x*y, (x, a, b), (y, c, d)), (y, 0))
    Sum(x*y, (y, c, d), (x, a, b))

    See Also
    ========

    sympy.concrete.simplification.index,
    sympy.concrete.simplification.change_index,
    sympy.concrete.simplification.reorder_limit,
    sympy.concrete.simplification.reverse_order
    """
    new_expr = expr

    for r in arg:
        if len(r) != 2:
            raise ValueError(r, "Invalid number of arguments")

        index1 = r[0]
        index2 = r[1]

        if not isinstance(r[0], int):
            index1 = index(expr, r[0])
        if not isinstance(r[1], int):
            index2 = index(expr, r[1])

        new_expr = reorder_limit(new_expr, index1, index2)

    return new_expr


def reorder_limit(expr, x, y):
    """
    Interchange two limit tuples of a Sum or Product expression.

    Usage
    =====

    ``reorder_limit(expr, x, y)`` interchanges two limit tuples. The
    arguments ``x`` and ``y`` are integers corresponding to the index
    variables of the two limits which are to be interchanged. The
    expression ``expr`` has to be either a Sum or a Product.

    Examples
    ========

    >>> from sympy.concrete.simplification import reorder_limit
    >>> from sympy.abc import x, y, z, a, b, c, d, e, f
    >>> from sympy import Sum, Product

    >>> reorder_limit(Sum(x*y*z, (x, a, b), (y, c, d), (z, e, f)), 0, 2)
    Sum(x*y*z, (z, e, f), (y, c, d), (x, a, b))
    >>> reorder_limit(Sum(x**2, (x, a, b), (x, c, d)), 1, 0)
    Sum(x**2, (x, c, d), (x, a, b))

    >>> reorder_limit(Product(x*y*z, (x, a, b), (y, c, d), (z, e, f)), 0, 2)
    Product(x*y*z, (z, e, f), (y, c, d), (x, a, b))

    See Also
    ========

    sympy.concrete.simplification.index,
    sympy.concrete.simplification.change_index,
    sympy.concrete.simplification.reorder,
    sympy.concrete.simplification.reverse_order
    """
    var = set([limit[0] for limit in expr.limits])
    limit_x = expr.limits[x]
    limit_y = expr.limits[y]

    if (len(set(limit_x[1].free_symbols).intersection(var)) == 0 and
        len(set(limit_x[2].free_symbols).intersection(var)) == 0 and
        len(set(limit_y[1].free_symbols).intersection(var)) == 0 and
        len(set(limit_y[2].free_symbols).intersection(var)) == 0):

        limits = []
        for i, limit in enumerate(expr.limits):
            if i == x:
                limits.append(limit_y)
            elif i == y:
                limits.append(limit_x)
            else:
                limits.append(limit)

        if isinstance(expr, Sum):
            return Sum(expr.function, *limits)
        elif isinstance(expr, Product):
            return Product(expr.function, *limits)
        else:
            raise NotImplementedError(expr, "reorder only implemented for Sum and Product")

    else:
        raise ReorderError(expr, "could not interchange the two limits specified")


def reverse_order(expr, *indices):
    """
    Reverse the order of a limit in a Sum or Product.

    Usage
    =====

    ``reverse_order(expr, *indices)`` reverses some limits in the expression
    ``expr`` which can be either a ``Sum`` or a ``Product``. The selectors in
    the argument ``indices`` specify some indices whose limits get reversed.
    These selectors are either variable names or numerical indices counted
    starting from the inner-most limit tuple.

    Examples
    ========

    >>> from sympy.concrete.simplification import reverse_order
    >>> from sympy import Sum
    >>> from sympy.abc import x, y, a, b, c, d

    >>> reverse_order(Sum(x, (x, 0, 3)), x)
    Sum(-x, (x, 4, -1))
    >>> reverse_order(Sum(x*y, (x, 1, 5), (y, 0, 6)), x, y)
    Sum(x*y, (x, 6, 0), (y, 7, -1))
    >>> reverse_order(Sum(x, (x, a, b)), x)
    Sum(-x, (x, b + 1, a - 1))
    >>> reverse_order(Sum(x, (x, a, b)), 0)
    Sum(-x, (x, b + 1, a - 1))

    >>> from sympy import Product, simplify, RisingFactorial, gamma
    >>> P = Product(x, (x, a, b))
    >>> Pr = reverse_order(P, x)
    >>> Pr
    Product(1/x, (x, b + 1, a - 1))
    >>> Pr = Pr.doit()
    >>> Pr
    1/RisingFactorial(b + 1, a - b - 1)
    >>> simplify(Pr)
    gamma(b + 1)/gamma(a)
    >>> P = P.doit()
    >>> P
    RisingFactorial(a, -a + b + 1)
    >>> simplify(P)
    gamma(b + 1)/gamma(a)

    While one should prefer variable names when specifying which limits
    to reverse, the index counting notation comes in handy in case there
    are several symbols with the same name.

    >>> S = Sum(x**2, (x, a, b), (x, c, d))
    >>> S
    Sum(x**2, (x, a, b), (x, c, d))
    >>> S0 = reverse_order(S, 0)
    >>> S0
    Sum(-x**2, (x, b + 1, a - 1), (x, c, d))
    >>> S1 = reverse_order(S0, 1)
    >>> S1
    Sum(x**2, (x, b + 1, a - 1), (x, d + 1, c - 1))

    Of course we can mix both notations:

    >>> reverse_order(Sum(x*y, (x, a, b), (y, 2, 5)), x, 1)
    Sum(x*y, (x, b + 1, a - 1), (y, 6, 1))
    >>> reverse_order(Sum(x*y, (x, a, b), (y, 2, 5)), y, x)
    Sum(x*y, (x, b + 1, a - 1), (y, 6, 1))

    See Also
    ========

    sympy.concrete.simplification.index,
    sympy.concrete.simplification.change_index,
    sympy.concrete.simplification.reorder_limit,
    sympy.concrete.simplification.reorder

    References
    ==========

    .. [1] Michael Karr, "Summation in Finite Terms", Journal of the ACM,
           Volume 28 Issue 2, April 1981, Pages 305-350
           http://dl.acm.org/citation.cfm?doid=322248.322255
    """
    l_indices = list(indices)

    for i, indx in enumerate(l_indices):
        if not isinstance(indx, int):
            l_indices[i] = index(expr, indx)

    if isinstance(expr, Sum) or isinstance(expr, Product):
        e = 1
        limits = []
        for i, limit in enumerate(expr.limits):
            l = limit
            if i in l_indices:
                e = -e
                l = (limit[0], limit[2] + 1 , limit[1] - 1)
            limits.append(l)

    if isinstance(expr, Sum):
        return Sum(e * expr.function, *limits)
    elif isinstance(expr, Product):
        return Product(expr.function ** e, *limits)
    else:
        return expr
