from __future__ import division
from builtins import zip
from builtins import next
from builtins import range
import unittest
from string import ascii_lowercase
from itertools import chain, cycle, repeat
import math
from nineml.values import SingleValue, ArrayValue
from operator import (
    add, sub, mul, truediv, pow, floordiv, mod, neg, iadd,
    ifloordiv, imod, imul, ipow, isub, itruediv, and_, or_, inv)
from nineml.utils.comprehensive_example import instances_of_all_types
import numpy as np  # This is only imported here in the test as it is not dependency
from sympy import sympify, Basic as SympyBaseClass, Symbol
from nineml.abstraction.expressions import Expression, Alias
from nineml import units as un

INCR = 0.01
NUM_VALUES = 10
ARRAY_SIZE = 5

single_values = [SingleValue(v) for v in np.arange(
    INCR, INCR * NUM_VALUES + INCR, INCR)]
array_values = [ArrayValue(np.arange(INCR * i, INCR * i * ARRAY_SIZE,
                                     INCR * i))
                for i in range(1, NUM_VALUES + 1)]
# Add non-numpy version of array values
array_values += [ArrayValue(list(iter(v))) for v in array_values]
units = [getattr(un, u) for u in dir(un)
         if isinstance(getattr(un, u), un.Unit)]
dimensions = [getattr(un, d) for d in dir(un)
              if isinstance(getattr(un, d), un.Dimension)]
quantities = [un.Quantity(v, u) for v, u in zip(
    chain(single_values, array_values), cycle(units))]

named_expressions = sorted(chain(*(list(instances_of_all_types[t].values())
                                 for t in ('Alias', 'StateAssignment'))),
                           key=lambda e: e.key)
logical_expressions = sorted(instances_of_all_types['Trigger'].values(),
                             key=lambda e: str(e.key))
anonymous_expressions = sorted(
    instances_of_all_types['TimeDerivative'].values(),
    key=lambda e: e.key)
expressions = list(chain(named_expressions, anonymous_expressions))

div_ops = (truediv, floordiv, mod, itruediv, ifloordiv, imod)
uniary_ops = [neg, abs, inv]


class TestValues(unittest.TestCase):

    ops = [
        floordiv, pow, truediv, sub, pow, neg, mod, add, pow, floordiv,
        mod, mod, floordiv, truediv, truediv, abs, add, abs, pow,
        neg, truediv, mul, mul, mod, mul, abs, abs, pow, neg,
        add, floordiv, add, mul, truediv, sub, add, mod, neg, sub,
        floordiv, sub, sub, neg, mul, abs]

    iops = [iadd, ifloordiv, imod, imul, ipow, isub, itruediv]

    def test_conversions(self):
        for val in single_values:
            self.assertEqual(float(val), val._value)
            self.assertLess(float(val) - int(val), 1.0)

    def test_single_value_operators(self):
        result = SingleValue(10.5)  # Random starting value
        val_iter = cycle(single_values)
        for op in self.ops:
            if op in uniary_ops:
                ff_result = op(float(result))
                vv_result = op(result)
                op_str = ("{}({})".format(op.__name__, result))
            else:
                val = next(val_iter)
                if op in div_ops and float(val) == 0.0:
                    val = SingleValue(0.1)
                ff_result = op(float(result), float(val))
                vv_result = op(result, val)
                vf_result = op(result, float(val))
                fv_result = op(float(result), val)
                op_str = ("{}({}, {})".format(op.__name__, re
sult, val))
                self.assertEqual(
                    float(vf_result), ff_result,
                    op_str + " not equal between single value and fl
oat")
                self.assertEqual(
                    float(fv_result), ff_result,
                    op_str + " not equal between single value and float")
                self.assertIsInstance(vf_result, SingleValue,
                                      op_str + " did not return a SingleValue")
                self.assertIsInstance(fv_result, SingleValue,
                                      op_str + " did not return a SingleValue")
            self.assertEqual(
                float(vv_result), ff_result,
                "{} not equal between single value ({}) and float ({})"
                .format(op_str, vv_result, ff_result))
            self.assertIsInstance(vv_result, SingleValue,
                                  op_str + " did not return a SingleValue")

    def test_array_value_operators(self):
        for array_val in array_values:
            np_val = np.asarray(array_val)
            np_array_val = ArrayValue(np.asarray(array_val))
            val_iter = cycle(single_values)
            for op in self.ops:
                if op in uniary_ops:
                    vv_result = op(array_val)
                    nv_result = op(np_array_val)
                    try:
                        np_result = op(np_val)
                    except AttributeError:
                        np_result = getattr(np, op.__name__)(np_val)
                    op_str = ("{}({})".format(op.__name__, array_val))
                else:
                    val = next(val_iter)
                    if op is pow:
                        # Negative numbers can't be raised to a fractional
                        # power so we avoid this by either using absolute
                        # values and scale back to sensible values to avoid
                        # overflow errors
                        array_val = abs(array_val)
                        np_array_val = abs(np_array_val)
                        np_val = abs(np_val)
                        val = abs(val) + 0.001
                        val = val / 10. ** round(math.log10(val))
                        val_scale = np_val.max() / 10.0
                        array_val = array_val * val_scale
                        np_array_val = np_array_val * val_scale
                        np_val = np_val * val_scale
                    elif op in div_ops:
                        # Ensure there are no zero values
                        array_val = array_val ** 2 + 1
                        np_array_val = np_array_val ** 2 + 1
                        np_val = np_val ** 2 + 1
                        val = val ** 2 + 1
                    vv_result = op(array_val, val)
                    vf_result = op(array_val, float(val))
                    nv_result = op(np_array_val, val)
                    nf_result = op(np_array_val, float(val))
                    np_result = op(np_val, float(val))
                    op_str = ("{}({}, {})".format(op.__name__, array_val, val))
                    rop_str = ("{}({}, {})".format(op.__name__, val,
                                                   array_val))
                    self.assertTrue(
                        all(np.asarray(vf_result) == np_result),
                        "{} not equal between array value ({}) and "
                        "numpy ({})".format(op_str, vf_result, np_result))
                    self.assertIsInstance(
                        vf_result, ArrayValue,
                        op_str + " did not return a ArrayValue")
                    self.assertTrue(
                        all(np.asarray(nf_result) == np_result),
                        "{} not equal between array value ({}) and "
                        "numpy ({})".format(op_str, nf_result, np_result))
                    self.assertIsInstance(
                        nv_result, ArrayValue,
                        "{} did not return a ArrayValue ({})"
                        .format(op_str, nf_result))
                    self.assertIsInstance(
                        nv_result._values, np.ndarray,
                        "{} did not maintain numpy _values in resultant "
                        "ArrayValue ({})"
                        .format(op_str, nv_result))
                    self.assertIsInstance(
                        nf_result._values, np.ndarray,
                        "{} did not maintain numpy _values in resultant "
                        "ArrayValue ({})"
                        .format(op_str, nf_result))
                    rvv_result = op(val, array_val)
                    rvf_result = op(float(val), array_val)
                    rnv_res