"""UFL to unicode."""

import numbers

import ufl
from ufl.algorithms import compute_form_data
from ufl.core.multiindex import FixedIndex, Index
from ufl.corealg.map_dag import map_expr_dag
from ufl.corealg.multifunction import MultiFunction
from ufl.form import Form

try:
    import colorama

    has_colorama = True
except ImportError:
    has_colorama = False


class PrecedenceRules(MultiFunction):
    """An enum-like class for C operator precedence levels."""

    def __init__(self):
        """Initialise."""
        MultiFunction.__init__(self)

    def highest(self, o):
        """Return the highest precendence."""
        return 0

    terminal = highest
    list_tensor = highest
    component_tensor = highest

    def restricted(self, o):
        """Return precedence of a restriced."""
        return 5

    cell_avg = restricted
    facet_avg = restricted

    def call(self, o):
        """Return precedence of a call."""
        return 10

    indexed = call
    min_value = call
    max_value = call
    math_function = call
    bessel_function = call

    def power(self, o):
        """Return precedence of a power."""
        return 12

    def mathop(self, o):
        """Return precedence of a mathop."""
        return 15

    derivative = mathop
    trace = mathop
    deviatoric = mathop
    cofactor = mathop
    skew = mathop
    sym = mathop

    def not_condition(self, o):
        """Return precedence of a not_condition."""
        return 20

    def product(self, o):
        """Return precedence of a product."""
        return 30

    division = product
    # mod = product
    dot = product
    inner = product
    outer = product
    cross = product

    def add(self, o):
        """Return precedence of an add."""
        return 40

    # sub = add
    index_sum = add

    def lt(self, o):
        """Return precedence of a lt."""
        return 50

    le = lt
    gt = lt
    ge = lt

    def eq(self, o):
        """Return precedence of an eq."""
        return 60

    ne = eq

    def and_condition(self, o):
        """Return precedence of an and_condition."""
        return 70

    def or_condition(self, o):
        """Return precedence of an or_condition."""
        return 71

    def conditional(self, o):
        """Return precedence of a conditional."""
        return 72

    def lowest(self, o):
        """Return precedence of a lowest."""
        return 80

    operator = lowest


_precrules = PrecedenceRules()


def precedence(expr):
    """Get the precedence of an expr."""
    return _precrules(expr)


class UC:
    """An enum-like class for unicode characters."""

    # Letters in this alphabet have contiguous code point numbers
    bold_math_a = "𝐚"
    bold_math_A = "𝐀"

    thin_space = "\u2009"

    superscript_plus = "⁺"
    superscript_minus = "⁻"
    superscript_equals = "⁼"
    superscript_left_paren = "⁽"
    superscript_right_paren = "⁾"
    superscript_digits = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"]

    subscript_plus = "₊"
    subscript_minus = "₋"
    subscript_equals = "₌"
    subscript_left_paren = "₍"
    subscript_right_paren = "₎"
    subscript_digits = ["₀", "₁", "₂", "₃", "₄", "₅", "₆", "₇", "₈", "₉"]

    sqrt = "√"
    transpose = "ᵀ"

    integral = "∫"
    integral_double = "∬"
    integral_triple = "∭"
    integral_contour = "∮"
    integral_surface = "∯"
    integral_volume = "∰"

    sum = "∑"
    division_slash = "∕"
    partial = "∂"
    epsilon = "ε"
    omega = "ω"
    Omega = "Ω"
    gamma = "γ"
    Gamma = "Γ"
    nabla = "∇"
    for_all = "∀"

    dot = "⋅"
    cross_product = "⨯"
    circled_times = "⊗"
    nary_product = "∏"

    ne = "≠"
    lt = "<"
    le = "≤"
    gt = ">"
    ge = "≥"

    logical_and = "∧"
    logical_or = "∨"
    logical_not = "¬"

    element_of = "∈"
    not_element_of = "∉"

    left_white_square_bracket = "⟦"
    right_white_squared_bracket = "⟧"
    left_angled_bracket = "⟨"
    right_angled_bracket = "⟩"
    left_double_angled_bracket = "⟪"
    right_double_angled_bracket = "⟫"

    combining_right_arrow_above = "\u20d7"
    combining_overline = "\u0305"


def bolden_letter(c):
    """Bolden a letter."""
    if ord("A") <= ord(c) <= ord("Z"):
        c = chr(ord(c) - ord("A") + ord(UC.bold_math_A))
    elif ord("a") <= ord(c) <= ord("z"):
        c = chr(ord(c) - ord("a") + ord(UC.bold_math_a))
    return c


def superscript_digit(digit):
    """Make a digit superscript."""
    return UC.superscript_digits[ord(digit) - ord("0")]


def subscript_digit(digit):
    """Make a digit subscript."""
    return UC.subscript_digits[ord(digit) - ord("0")]


def bolden_string(s):
    """Bolden a string."""
    return "".join(bolden_letter(c) for c in s)


def overline_string(f):
    """Overline a string."""
    return "".join(f"{c}{UC.combining_overline}" for c in f)


def subscript_number(number):
    """Make a number subscript."""
    assert isinstance(number, int)
    prefix = UC.subscript_minus if number < 0 else ""
    number = str(number)
    return prefix + "".join(subscript_digit(c) for c in str(number))


def superscript_number(number):
    """Make a number superscript."""
    assert isinstance(number, int)
    prefix = UC.superscript_minus if number < 0 else ""
    number = str(number)
    return prefix + "".join(superscript_digit(c) for c in str(number))


def opfont(opname):
    """Use the font for operators."""
    return bolden_string(opname)


def measure_font(dx):
    """Use the font for measures."""
    return bolden_string(dx)


integral_by_dim = {3: UC.integral_triple, 2: UC.integral_double, 1: UC.integral, 0: UC.integral}

integral_type_to_codim = {
    "cell": 0,
    "exterior_facet": 1,
    "interior_facet": 1,
    "vertex": "tdim",
    "point": "tdim",
    "custom": 0,
    "overlap": 0,
    "interface": 1,
    "cutcell": 0,
}

integral_symbols = {
    "cell": UC.integral_volume,
    "exterior_facet": UC.integral_surface,
    "interior_facet": UC.integral_surface,
    "vertex": UC.integral,
    "point": UC.integral,
    "custom": UC.integral,
    "overlap": UC.integral,
    "interface": UC.integral,
    "cutcell": UC.integral,
}

integral_postfixes = {
    "cell": "",
    "exterior_facet": "ext",
    "interior_facet": "int",
    "vertex": "vertex",
    "point": "point",
    "custom": "custom",
    "overlap": "overlap",
    "interface": "interface",
    "cutcell": "cutcell",
}


def get_integral_symbol(integral_type, domain, subdomain_id):
    """Get the symbol for an integral."""
    tdim = domain.topological_dimension()
    codim = integral_type_to_codim[integral_type]
    itgdim = tdim - codim

    # ipost = integral_postfixes[integral_type]
    istr = integral_by_dim[itgdim]

    # TODO: Render domain description

    subdomain_strs = []
    for subdomain in subdomain_id:
        if isinstance(subdomain, numbers.Integral):
            subdomain_strs.append(subscript_number(int(subdomain)))
        elif subdomain == "everywhere":
            pass
        elif subdomain == "otherwise":
            subdomain_strs.append("[rest of domain]")
    istr += ",".join(subdomain_strs)

    dxstr = ufl.measure.integral_type_to_measure_name[integral_type]
    dxstr = measure_font(dxstr)

    return istr, dxstr


def par(s):
    """Wrap in parentheses."""
    return f"({s})"


def is_int(s):
    """Check if a value is an integer."""
    try:
        int(s)
        return True
    except ValueError:
        return False


def format_index(ii):
    """Format an index."""
    if isinstance(ii, FixedIndex):
        s = f"{ii._value}"
    elif isinstance(ii, Index):
        s = "i{subscript_number(ii._count)}"
    else:
        raise ValueError(f"Invalid index type {type(ii)}.")
    return s


def ufl2unicode(expression):
    """Generate Unicode string for a UFL expression or form."""
    if isinstance(expression, Form):
        form_data = compute_form_data(expression)
        preprocessed_form = form_data.preprocessed_form
        return form2unicode(preprocessed_form, form_data)
    else:
        return expression2unicode(ufl.as_ufl(expression))


def expression2unicode(expression, argument_names=None, coefficient_names=None):
    """Generate Unicode string for a UFL expression."""
    rules = Expression2UnicodeHandler(argument_names, coefficient_names)
    return map_expr_dag(rules, expression)


def form2unicode(form, formdata):
    """Generate Unicode string for a UFL form."""
    argument_names = None
    coefficient_names = None

    # Define form as sum of integrals
    lines = []
    integrals = form.integrals()
    for itg in integrals:
        integrand_string = expression2unicode(itg.integrand(), argument_names, coefficient_names)

        istr, dxstr = get_integral_symbol(itg.integral_type(), itg.ufl_domain(), itg.subdomain_id())

        line = f"{istr} {integrand_string} {dxstr}"
        lines.append(line)

    return "\n  + ".join(lines)


def binop(expr, a, b, op, sep=" "):
    """Format a binary operation."""
    eprec = precedence(expr)
    op0, op1 = expr.ufl_operands
    aprec = precedence(op0)
    bprec = precedence(op1)
    # Assuming left-to-right evaluation, therefore >= and > here:
    if aprec >= eprec:
        a = par(a)
    if bprec > eprec:
        b = par(b)
    return sep.join((a, op, b))


def mathop(expr, arg, opname):
    """Format a math operation."""
    eprec = precedence(expr)
    aprec = precedence(expr.ufl_operands[0])
    op = opfont(opname)
    if aprec > eprec:
        arg = par(arg)
        sep = ""
    else:
        sep = UC.thin_space
    return f"{op}{sep}{arg}"


class Expression2UnicodeHandler(MultiFunction):
    """Convert expressions to unicode."""

    def __init__(self, argument_names=None, coefficient_names=None, colorama_bold=False):
        """Initialise."""
        MultiFunction.__init__(self)
        self.argument_names = argument_names
        self.coefficient_names = coefficient_names
        self.colorama_bold = colorama_bold and has_colorama

    # --- Terminal objects ---

    def scalar_value(self, o):
        """Format a scalar_value."""
        if o.ufl_shape and self.colorama_bold:
            return f"{colorama.Style.BRIGHT}{o._value}{colorama.Style.RESET_ALL}"
        return f"{o._value}"

    def zero(self, o):
        """Format a zero."""
        if o.ufl_shape and self.colorama_bold:
            if len(o.ufl_shape) == 1:
                return f"0{UC.combining_right_arrow_above}"
            return f"{colorama.Style.BRIGHT}0{colorama.Style.RESET_ALL}"
        return "0"

    def identity(self, o):
        """Format a identity."""
        if self.colorama_bold:
            return f"{colorama.Style.BRIGHT}I{colorama.Style.RESET_ALL}"
        return "I"

    def permutation_symbol(self, o):
        """Format a permutation_symbol."""
        if self.colorama_bold:
            return f"{colorama.Style.BRIGHT}{UC.epsilon}{colorama.Style.RESET_ALL}"
        return UC.epsilon

    def facet_normal(self, o):
        """Format a facet_normal."""
        return f"n{UC.combining_right_arrow_above}"

    def spatial_coordinate(self, o):
        """Format a spatial_coordinate."""
        return f"x{UC.combining_right_arrow_above}"

    def argument(self, o):
        """Format an argument."""
        # Using ^ for argument numbering and _ for indexing since
        # indexing is more common than exponentiation
        if self.argument_names is None:
            i = o.number()
            bfn = "v" if i == 0 else "u"
            if not o.ufl_shape:
                return bfn
            elif len(o.ufl_shape) == 1:
                return f"{bfn}{UC.combining_right_arrow_above}"
            elif self.colorama_bold:
                return f"{colorama.Style.BRIGHT}{bfn}{colorama.Style.RESET_ALL}"
            else:
                return bfn
        return self.argument_names[(o.number(), o.part())]

    def coefficient(self, o):
        """Format a coefficient."""
        # Using ^ for coefficient numbering and _ for indexing since
        # indexing is more common than exponentiation
        if self.coefficient_names is None:
            i = o.count()
            var = "w"
            if len(o.ufl_shape) == 1:
                var += UC.combining_right_arrow_above
            elif len(o.ufl_shape) > 1 and self.colorama_bold:
                var = f"{colorama.Style.BRIGHT}{var}{colorama.Style.RESET_ALL}"
            return f"{var}{subscript_number(i)}"
        return self.coefficient_names[o.count()]

    def cofunction(self, o):
        """Format a cofunction."""
        if self.coefficient_names is None:
            i = o.count()
            var = "cofunction"
            if len(o.ufl_shape) == 1:
                var += UC.combining_right_arrow_above
            elif len(o.ufl_shape) > 1 and self.colorama_bold:
                var = f"{colorama.Style.BRIGHT}{var}{colorama.Style.RESET_ALL}"
            return f"{var}{subscript_number(i)}"
        return self.coefficient_names[o.count()]

    def base_form_operator(self, o):
        """Format a base_form_operator."""
        return "BaseFormOperator"

    def action(self, o, a, b):
        """Format an Action."""
        return f"Action({a}, {b})"

    def constant(self, o):
        """Format a constant."""
        i = o.count()
        var = "c"
        if len(o.ufl_shape) == 1:
            var += UC.combining_right_arrow_above
        elif len(o.ufl_shape) > 1 and self.colorama_bold:
            var = f"{colorama.Style.BRIGHT}{var}{colorama.Style.RESET_ALL}"
        return f"{var}{superscript_number(i)}"

    def multi_index(self, o):
        """Format a multi_index."""
        return ",".join(format_index(i) for i in o)

    def label(self, o):
        """Format a label."""
        return f"l{subscript_number(o.count())}"

    # --- Non-terminal objects ---

    def variable(self, o, f, a):
        """Format a variable."""
        return f"var({f},{a})"

    def index_sum(self, o, f, i):
        """Format a index_sum."""
        if 1:  # prec(o.ufl_operands[0]) >? prec(o):
            f = par(f)
        return f"{UC.sum}[{i}]{f}"

    def sum(self, o, a, b):
        """Format a sum."""
        return binop(o, a, b, "+")

    def product(self, o, a, b):
        """Format a product."""
        return binop(o, a, b, " ", sep="")

    def division(self, o, a, b):
        """Format a division."""
        if is_int(b):
            b = subscript_number(int(b))
            if is_int(a):
                # Return as a fraction
                # NOTE: Maybe consider using fractional slash
                #  with normal numbers if terminals can handle it
                a = superscript_number(int(a))
            else:
                a = par(a)
            return f"{a} {UC.division_slash} {b}"
        return binop(o, a, b, UC.division_slash)

    def abs(self, o, a):
        """Format an ans."""
        return f"|{a}|"

    def transposed(self, o, a):
        """Format a transposed."""
        a = par(a)
        return f"{a}{UC.transpose}"

    def indexed(self, o, A, ii):
        """Format an indexed."""
        op0, op1 = o.ufl_operands
        Aprec = precedence(op0)
        oprec = precedence(o)
        if Aprec > oprec:
            A = par(A)
        return f"{A}[{ii}]"

    def variable_derivative(self, o, f, v):
        """Format a variable_derivative."""
        f = par(f)
        v = par(v)
        nom = f"{UC.partial}{f}"
        denom = f"{UC.partial}{v}"
        return par(f"{nom}{UC.division_slash}{denom}")

    def coefficient_derivative(self, o, f, w, v, cd):
        """Format a coefficient_derivative."""
        f = par(f)
        w = par(w)
        nom = f"{UC.partial}{f}"
        denom = f"{UC.partial}{w}"
        return par(f"{nom}{UC.division_slash}{denom}[{v}]")

    def grad(self, o, f):
        """Format a grad."""
        return mathop(o, f, "grad")

    def div(self, o, f):
        """Format a div."""
        return mathop(o, f, "div")

    def nabla_grad(self, o, f):
        """Format a nabla_grad."""
        oprec = precedence(o)
        fprec = precedence(o.ufl_operands[0])
        if fprec > oprec:
            f = par(f)
        return f"{UC.nabla}{UC.thin_space}{f}"

    def nabla_div(self, o, f):
        """Format a nabla_div."""
        oprec = precedence(o)
        fprec = precedence(o.ufl_operands[0])
        if fprec > oprec:
            f = par(f)
        return f"{UC.nabla}{UC.thin_space}{UC.dot}{UC.thin_space}{f}"

    def curl(self, o, f):
        """Format a curl."""
        oprec = precedence(o)
        fprec = precedence(o.ufl_operands[0])
        if fprec > oprec:
            f = par(f)
        return f"{UC.nabla}{UC.thin_space}{UC.cross_product}{UC.thin_space}{f}"

    def math_function(self, o, f):
        """Format a math_function."""
        op = opfont(o._name)
        return f"{op}{par(f)}"

    def sqrt(self, o, f):
        """Format a sqrt."""
        return f"{UC.sqrt}{par(f)}"

    def exp(self, o, f):
        """Format a exp."""
        op = opfont("exp")
        return f"{op}{par(f)}"

    def atan2(self, o, f1, f2):
        """Format a atan2."""
        f1 = par(f1)
        f2 = par(f2)
        op = opfont("arctan2")
        return f"{op}({f1}, {f2})"

    def bessel_j(self, o, nu, f):
        """Format a bessel_j."""
        op = opfont("J")
        nu = subscript_number(int(nu))
        return f"{op}{nu}{par(f)}"

    def bessel_y(self, o, nu, f):
        """Format a bessel_y."""
        op = opfont("Y")
        nu = subscript_number(int(nu))
        return f"{op}{nu}{par(f)}"

    def bessel_i(self, o, nu, f):
        """Format a bessel_i."""
        op = opfont("I")
        nu = subscript_number(int(nu))
        return f"{op}{nu}{par(f)}"

    def bessel_K(self, o, nu, f):
        """Format a bessel_K."""
        op = opfont("K")
        nu = subscript_number(int(nu))
        return f"{op}{nu}{par(f)}"

    def power(self, o, a, b):
        """Format a power."""
        if is_int(b):
            b = superscript_number(int(b))
            return binop(o, a, b, "", sep="")
        return binop(o, a, b, "^", sep="")

    def outer(self, o, a, b):
        """Format an outer."""
        return binop(o, a, b, UC.circled_times)

    def inner(self, o, a, b):
        """Format an inner."""
        return f"{UC.left_angled_bracket}{a}, {b}{UC.right_angled_bracket}"

    def dot(self, o, a, b):
        """Format a dot."""
        return binop(o, a, b, UC.dot)

    def cross(self, o, a, b):
        """Format a cross."""
        return binop(o, a, b, UC.cross_product)

    def determinant(self, o, A):
        """Format a determinant."""
        return f"|{A}|"

    def inverse(self, o, A):
        """Format an inverse."""
        A = par(A)
        return f"{A}{superscript_number(-1)}"

    def trace(self, o, A):
        """Format a trace."""
        return mathop(o, A, "tr")

    def deviatoric(self, o, A):
        """Format a deviatoric."""
        return mathop(o, A, "dev")

    def cofactor(self, o, A):
        """Format a cofactor."""
        return mathop(o, A, "cofac")

    def skew(self, o, A):
        """Format a skew."""
        return mathop(o, A, "skew")

    def sym(self, o, A):
        """Format a sym."""
        return mathop(o, A, "sym")

    def conj(self, o, a):
        """Format a conj."""
        # Overbar is already taken for average, and there is no superscript asterix in unicode.
        return mathop(o, a, "conj")

    def real(self, o, a):
        """Format a real."""
        return mathop(o, a, "Re")

    def imag(self, o, a):
        """Format a imag."""
        return mathop(o, a, "Im")

    def list_tensor(self, o, *ops):
        """Format a list_tensor."""
        return f"[{', '.join(ops)}]"

    def component_tensor(self, o, A, ii):
        """Format a component_tensor."""
        return f"[{A} {UC.for_all} {ii}]"

    def positive_restricted(self, o, f):
        """Format a positive_restriced."""
        return f"{par(f)}{UC.superscript_plus}"

    def negative_restricted(self, o, f):
        """Format a negative_restriced."""
        return f"{par(f)}{UC.superscript_minus}"

    def cell_avg(self, o, f):
        """Format a cell_avg."""
        f = overline_string(f)
        return f

    def facet_avg(self, o, f):
        """Format a facet_avg."""
        f = overline_string(f)
        return f

    def eq(self, o, a, b):
        """Format an eq."""
        return binop(o, a, b, "=")

    def ne(self, o, a, b):
        """Format a ne."""
        return binop(o, a, b, UC.ne)

    def le(self, o, a, b):
        """Format a le."""
        return binop(o, a, b, UC.le)

    def ge(self, o, a, b):
        """Format a ge."""
        return binop(o, a, b, UC.ge)

    def lt(self, o, a, b):
        """Format a lt."""
        return binop(o, a, b, UC.lt)

    def gt(self, o, a, b):
        """Format a gt."""
        return binop(o, a, b, UC.gt)

    def and_condition(self, o, a, b):
        """Format an and_condition."""
        return binop(o, a, b, UC.logical_and)

    def or_condition(self, o, a, b):
        """Format an or_condition."""
        return binop(o, a, b, UC.logical_or)

    def not_condition(self, o, a):
        """Format a not_condition."""
        a = par(a)
        return f"{UC.logical_not}{a}"

    def conditional(self, o, c, t, f):
        """Format a conditional."""
        c = par(c)
        t = par(t)
        f = par(t)
        If = opfont("if")
        Else = opfont("else")
        return f"{t} {If} {c} {Else} {f}"

    def min_value(self, o, a, b):
        """Format an min_value."""
        op = opfont("min")
        return f"{op}({a}, {b})"

    def max_value(self, o, a, b):
        """Format an max_value."""
        op = opfont("max")
        return f"{op}({a}, {b})"

    def expr_list(self, o, *ops):
        """Format an expr_list."""
        items = ", ".join(ops)
        return f"{UC.left_white_square_bracket} {items} {UC.right_white_squared_bracket}"

    def expr_mapping(self, o, *ops):
        """Format an expr_mapping."""
        items = ", ".join(ops)
        return f"{UC.left_double_angled_bracket} {items} {UC.left_double_angled_bracket}"

    def expr(self, o):
        """Format an expr."""
        raise ValueError(f"Missing handler for type {type(o)}")
