'''
Created on 10-Jun-2021

@author: bthakur
'''
import numpy as np
from sympy import symbols, Rational, sympify
import random
from xv.km.util import latex_fraction, wrap_left_right
import matplotlib.pyplot as plt
from functools import reduce
from sympy.core.expr import UnevaluatedExpr as ueval


class _FractionLayer:
    def __init__(self, **kwargs):
        self.p = kwargs.get('p', None)
        self.q = kwargs.get('q', None)
        self.fraction = kwargs.get('fraction', None)
        self.latext_p_q = kwargs.get('latext_p_q', None)
        self.new_fraction_as_num = kwargs.get('new_fraction_as_num', None)
        self.sympy_fraction = kwargs.get('sympy_fraction', None)


        self.numerators = kwargs.get('numerators', None)
        self.denominators = kwargs.get('denominators', None)
        self.cum_latex_fraction = kwargs.get('cum_latex_fraction', None)
        self.braced_cum_latex_fraction = kwargs.get('braced_cum_latex_fraction', None)
        pass

    def _multiply(self, *nums):
        return reduce(lambda x, y: x * y, nums, 1)
        pass


    def get_value_details(self):
        nums_prod = self._multiply(*self.numerators)
        denm_prod = self._multiply(*self.denominators)
        value = nums_prod / denm_prod

        num_den_latex = latex_fraction(
            " * ".join([str(x) for x in self.numerators]),
            " * ".join([str(x) for x in self.denominators])
            )

        value_latex = latex_fraction(nums_prod,denm_prod)
        return num_den_latex, value_latex, value

    def __str__(self):
        s = ""
        s = s + "\n" + f"""p = {self.p}"""
        s = s + "\n" + f"""q = {self.q}"""
        s = s + "\n\n" + f"""fraction = ${self.fraction}$"""
        s = s + "\n\n" + f"""latext_p_q = ${self.latext_p_q}$"""
        s = s + "\n\n" + f"""new_fraction_as_num = ${self.new_fraction_as_num}$"""
        s = s + "\n\n" + f"""sympy_fraction = ${self.sympy_fraction}$"""

        s = s + "\n\n" + f"""numerators = {self.numerators}"""
        s = s + "\n\n" + f"""denominators = {self.denominators}"""


        s = s + "\n\n" + f"""value check = ${self.sympy_fraction}$"""


        s = s + "\n\n" + f"""cum_latex_fraction = ${self.cum_latex_fraction}$"""
        s = s + "\n\n" + f"""braced_cum_latex_fraction = ${self.braced_cum_latex_fraction}$"""
        return s
        pass

    def _repr_html_(self):
        s = ""
        s = s + "<br/>" + f"p = {self.p}"
        s = s + "<br/>" + f"q = {self.q}"
        s = s + "<br/><br/>" + f"fraction = ${self.fraction}$"
        s = s + "<br/><br/>" + f"latext_p_q = ${self.latext_p_q}$"
        s = s + "<br/><br/>" + f"new_fraction_as_num = ${self.new_fraction_as_num}$"
        s = s + "<br/><br/>" + f"sympy_fraction = ${self.sympy_fraction}$"

        nums_prod = self._multiply(*self.numerators)
        denm_prod = self._multiply(*self.denominators)
        result1 = nums_prod / denm_prod

        result2 = sympify(self.sympy_fraction)
        s = s + "<br/><br/>" + f"""
        result1 = nums_prod * denm_prod = {nums_prod} * {denm_prod} = {result1} = {float(result1)}

        <br/><br/>
        result1 = {self.sympy_fraction} = {result2}  = {float(result2)}
        """


        s = s + "<br/><br/>" + f"numerators = {self.numerators}"
        s = s + "<br/><br/>" + f"denominators = {self.denominators}"
        s = s + "<br/><br/>" + f"cum_latex_fraction = ${self.cum_latex_fraction}$"
        s = s + "<br/><br/>" + f"braced_cum_latex_fraction = ${self.braced_cum_latex_fraction}$"
        return s
        pass

    def __repr__(self):
        return self._repr_html_()


    pass

class MultiLayeredFraction:
    def __init__(self):
        self.fraction_layers = []
        self.numerators = []
        self.denominators = []
        self._cum_latex_fraction = None
        self.prev_sympy_fraction = "1"
        self.colors = plt.rcParams["axes.prop_cycle"]()
        pass

    def get_new_fraction(self, p = None, q = None):
        if p is None:
            p = random.choice([random.randint(2, 10)])
        if q is None:
            q = random.choice([random.randint(1, 10)])

        if q == 1:
            latext_p_q = f"{p}"
        else:
            latext_p_q = latex_fraction(p, q)

        #fraction = ueval(Rational(p, q))
        fraction = Rational(p, q)
        return p, q, fraction, latext_p_q
        pass

    def get_final_fraction(self):
        return self.fraction_layers[-1]
        pass

    def append_fractions(self, p = None, q = None,
                         new_fraction_as_num = None,
                           pColor = None,
                           qColor = None,
                           ):

        if new_fraction_as_num is None:
            new_fraction_as_num = random.choice([True, False])

        if pColor is None:
            pColor = self._next_color()

        if qColor is None:
            qColor = self._next_color()

        p, q, fraction, latext_p_q = self.get_new_fraction(p, q)

        if new_fraction_as_num:
            self._swap_nums_dens()
            self.numerators.append(p)
            self.denominators.append(q)
            pass
        else:
            self.numerators.append(q)
            self.denominators.append(p)
            pass

        sympy_fraction = self._get_sympy_fraction(p, q, new_fraction_as_num)
        self.prev_sympy_fraction = sympy_fraction

        cum_latex_fraction = self._get_cum_latex_fraction(latext_p_q,
                                                       new_fraction_as_num,
                                                       pColor = pColor,
                                                       qColor = qColor,
                                                       braced = False)

        braced_cum_latex_fraction = self._get_cum_latex_fraction(latext_p_q,
                                                       new_fraction_as_num,
                                                       pColor = pColor,
                                                       qColor = qColor,
                                                       braced = True)

        self._cum_latex_fraction = cum_latex_fraction

        fraction_layer = _FractionLayer(
                            p = p,
                            q = q,
                            fraction = fraction,
                            latext_p_q = latext_p_q,
                            new_fraction_as_num = new_fraction_as_num,
                            sympy_fraction = sympy_fraction,

                            numerators = self.numerators.copy(),
                            denominators = self.denominators.copy(),
                            cum_latex_fraction = cum_latex_fraction,
                            braced_cum_latex_fraction = braced_cum_latex_fraction,
                            )

        self.fraction_layers.append(fraction_layer)

        pass

    def append_fractions_advanced(self, p = None, q = None,
                         new_fraction_as_num = None,
                           pColor = None,
                           qColor = None,
                           ):

        if new_fraction_as_num is None:
            new_fraction_as_num = random.choice([True, False])

        if pColor is None:
            pColor = self._next_color()

        if qColor is None:
            qColor = self._next_color()

        p, q, fraction, latext_p_q = self.get_new_fraction(p, q)

        if new_fraction_as_num:
            self._swap_nums_dens()
            self.numerators.append(p)
            self.denominators.append(q)
            pass
        else:
            self.numerators.append(q)
            self.denominators.append(p)
            pass

        sympy_fraction = self._get_sympy_fraction(p, q, new_fraction_as_num)
        self.prev_sympy_fraction = sympy_fraction

        cum_latex_fraction = self._get_cum_latex_fraction(latext_p_q,
                                                       new_fraction_as_num,
                                                       pColor = pColor,
                                                       qColor = qColor,
                                                       braced = False)

        braced_cum_latex_fraction = self._get_cum_latex_fraction(latext_p_q,
                                                       new_fraction_as_num,
                                                       pColor = pColor,
                                                       qColor = qColor,
                                                       braced = True)

        self._cum_latex_fraction = cum_latex_fraction

        fraction_layer = _FractionLayer(
                            p = p,
                            q = q,
                            fraction = fraction,
                            latext_p_q = latext_p_q,
                            new_fraction_as_num = new_fraction_as_num,
                            sympy_fraction = sympy_fraction,

                            numerators = self.numerators.copy(),
                            denominators = self.denominators.copy(),
                            cum_latex_fraction = cum_latex_fraction,
                            braced_cum_latex_fraction = braced_cum_latex_fraction,
                            )

        self.fraction_layers.append(fraction_layer)

        pass

    def _get_sympy_fraction(self, p, q, new_fraction_as_num):
        if new_fraction_as_num:
            sympy_fraction = f"1/({self.prev_sympy_fraction}) * ({p}/{q})"
        else:
            sympy_fraction = f"({self.prev_sympy_fraction}) * ({q}/{p})"


        return sympy_fraction
        pass

    def _get_cum_latex_fraction(self,
                           latext_p_q,
                           new_fraction_as_num,
                           pColor = None,
                           qColor = None,
                           braced = False
                           ):
        prev_cum = self._cum_latex_fraction

        if prev_cum is None:
            if new_fraction_as_num:
                return latext_p_q
            else:
                reverse_p_q = latex_fraction(1, latext_p_q,
                                      pColor = pColor,
                                      qColor = qColor
                                      )
                return  reverse_p_q
            pass


        if new_fraction_as_num:
            latex_p = f"{latext_p_q} * {prev_cum}"
            return latex_p

        latex_p = prev_cum
        latex_q = latext_p_q
        if braced:
            latex_p = wrap_left_right(latex_p)
            latex_q = wrap_left_right(latex_q)


        return latex_fraction(latex_p, latex_q,
                              pColor = pColor,
                              qColor = qColor
                              )
        pass


    def _swap_nums_dens(self):
        temp = self.numerators.copy()
        self.numerators = self.denominators
        self.denominators = temp
        pass

    def _next_color(self):
        return next(self.colors)["color"]
        pass

    def __str__(self):
        s = ""
        s = s + "\n" + f"""layers = {len(self.fraction_layers)}\n"""

        for layer in self.fraction_layers:
            s = s + "\n" + f"{layer}"
            pass
        return s

    def _repr_html_(self):
        s = ""
        for layer_idx, layer in enumerate(self.fraction_layers):
            s = s + "\n<br />" + f"""layer_idx = {layer_idx}\n"""
            s = s + "\n<br />" + f"{layer._repr_html_()}"
            s = s + "\n<br />" + "-" * 50 + "<br />"
            pass
        return s
        pass

    def _repr_html_reverse_order(self):
        spaces = "&nbsp;" * 20
        s = []
        prev_incr_fraction = []
        for layer_idx in range(len(self.fraction_layers)-1, -1, -1):
            layer = self.fraction_layers[layer_idx]
            #s = s + "\n<br />" + f"""layer_idx = {layer_idx}\n"""
            #s = s + "<br/><br/>" + f"braced_cum_latex_fraction = ${layer.braced_cum_latex_fraction}$"

            if layer_idx == len(self.fraction_layers)-1:
                s.append(f"""
                $\\displaystyle \\large{{{layer.braced_cum_latex_fraction}}}$
                """)

            if layer_idx > 0:
                prev_layer  = self.fraction_layers[layer_idx - 1]

                numerators = layer.numerators
                prev_numerators = prev_layer.numerators
                denominators = layer.denominators
                prev_denominators = prev_layer.denominators

                incr_numerators = numerators[len(prev_numerators):]
                incr_denominators = denominators[len(prev_denominators):]

                incr_fraction, comment = self._get_incr_fraction(
                    incr_numerators, incr_denominators, layer.new_fraction_as_num)

                prev_incr_fraction.append(incr_fraction)
                incr_fraction_str = " * ".join(prev_incr_fraction)

                braced_cum_latex_fraction = wrap_left_right(prev_layer.cum_latex_fraction)

                if layer_idx > 0:
                    braced_cum_latex_fraction = f"\\large{{{braced_cum_latex_fraction}}}"

                s.append(f"""
                    $\\displaystyle = \\left[{incr_fraction_str}\\right] * {braced_cum_latex_fraction}$\t\t{comment}

                    """)

            if layer_idx == 0:
                incr_fraction, comment = self._get_incr_fraction(
                    layer.numerators, layer.denominators, layer.new_fraction_as_num)

                prev_incr_fraction.append(incr_fraction)
                incr_fraction_str = " * ".join(prev_incr_fraction)

                result = f""" {incr_fraction_str} """
                s.append(f"""
                $\\displaystyle = {result} $\t\t{comment}
                """)
            pass

        s = [item.strip() for item in s]
        s = "\n\n".join(s)

        return s, result
        pass


    def _get_incr_fraction(self, incr_numerators, incr_denominators, new_fraction_as_num):


        incr_fraction = latex_fraction(
            " * ".join([str(x) for x in incr_numerators]),
            " * ".join([str(x) for x in incr_denominators])
            )

        if new_fraction_as_num:
            comment = f"Simplify numerator fraction ${incr_fraction}$ "
            pass
        else:
            rev_incr_fraction = latex_fraction(
                " * ".join([str(x) for x in incr_denominators]),
                " * ".join([str(x) for x in incr_numerators])
                )
            comment = f"Simplify denominator fraction ${rev_incr_fraction}$ "
            pass

        return incr_fraction, comment
        pass

    def __repr__(self):
        return self._repr_html_()

    def _repr_latex_(self):
        return self._repr_html_()


    pass
#class MultiLayeredFraction