import itertools as it

from theano.scalar.basic import Apply, ScalarOp, as_scalar, float64, float32, int64
from theano.gof.utils import remove

imported_sympy = False
try:
    from sympy.utilities.codegen import get_default_datatype, codegen
    imported_sympy = True
except ImportError:
    pass

names = ("sympy_func_%d" % i for i in it.count(0))


def include_line(line):
    return '#include' in line


def sympy_dtype(expr):
    return get_default_datatype(expr).cname


def theano_dtype(expr):
    return {'double': float64,
            'float': float32,
            'int': int64}[sympy_dtype(expr)]


class SymPyCCode(ScalarOp):
    """
    An Operator that wraps SymPy's C code generation.

    Examples
    --------
    >>> from sympy.abc import x, y  # SymPy Variables
    >>> from theano.scalar.basic_sympy import SymPyCCode
    >>> op = SymPyCCode([x, y], x + y)

    >>> from theano.scalar.basic import floats
    >>> xt, yt = floats('xy') # Theano variables
    >>> zt = op(xt, yt)

    >>> import theano
    >>> f = theano.function([xt, yt], zt)
    >>> f(1.0, 2.0)
    3.0

    """

    def __init__(self, inputs, expr, name=None):
        self.name = name or next(names)
        self.inputs = inputs
        self.expr = expr

    def _sympy_c_code(self):
        [(c_name, c_code), (h_name, c_header)] = codegen(
            (self.name, self.expr), 'C', 'project_name',
            header=False, argument_sequence=self.inputs)
        return c_code

    def c_support_code(self):
        c_code = self._sympy_c_code()
        return '\n'.join(remove(include_line, c_code.split('\n')))

    def c_headers(self):
        c_code = self._sympy_c_code()
        return [line.replace("#include", "").strip() for line in
                c_code.split('\n') if include_line(line) and
                'project_name' not in line]

    def c_code(self, node, name, input_names, output_names, sub):
        y, = output_names
        xs = ', '.join(input_names)
        f = self.name
        return "%(y)s = %(f)s(%(xs)s);" % locals()

    def output_types_preference(self, *inputs):
        return [theano_dtype(self.expr)]

    def make_node(self, *inputs):
        # TODO: assert input types are correct use get_default_datatype

        if len(inputs) != len(self.inputs):
            raise TypeError("Wrong number of inputs for %s.make_node (got %i(%s), expected %i)" % (self, len(inputs), str(inputs), self.nin))

        inputs = [as_scalar(input) for input in inputs]
        outputs = [t() for t in self.output_types([input.type for input in inputs])]
        return Apply(self, inputs, outputs)

    def perform(self, node, inputs, output_storage):
        raise NotImplementedError()

    def grad(self, inputs, output_grads):
        return [SymPyCCode(self.inputs,
                           self.expr.diff(inp),
                           name=self.name + "_grad_%d" % i)(*inputs)
                for i, inp in enumerate(self.inputs)]

    def _info(self):
        return type(self), self.name, tuple(self.inputs), self.expr

    def __eq__(self, other):
        return type(self) == type(other) and self._info() == other._info()

    def __hash__(self):
        return hash(self._info())
