'''
Created on 20-Jan-2022

@author: bthakur
'''
import matplotlib.pyplot as plt

import inspect
import traceback
import random
import numpy as np
import sympy as sp


from sympy import (simplify, nsimplify, Rational, latex, Symbol,
                   Eq)

from xv.km.util import latex_fraction, wrap_latex_statement, latex_color
from xv.util.functions import number_to_nth
from sympy.core.expr import UnevaluatedExpr

from xv.km.ui import SvgHelper

from ...util import MathUtil
from xv.util import toIntOrFloat, list_to_str
from xv.km.kelements import KProblemTemplate

from ._arithmetic_helper import ArithmeticHelper

# _rational_number_helper
# RationalNumberHelper

class RationalNumberHelper(ArithmeticHelper):

    _DEFAULT_FONT_SIZE = 15

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        pass


    def convert_to_sum_latex(self, nums, mixed_form = False):
        nums_sum_list = []
        for idx, item in enumerate(nums):
            if mixed_form:
                latex_item = Rational(item).mixed_form_latex()
            else:
                latex_item = latex(item)

            if idx == 0:
                nums_sum_list.append(f" {latex_item}")
                continue

            if item >= 0:
                nums_sum_list.append(f" + {latex_item}")
            else:
                nums_sum_list.append(f" {latex_item}")
                pass
            pass
        return nums_sum_list
        pass

    def get_random_mixed_numbers(self, size = 4, mixed_form = False):
        nums = []
        while len(nums) < size:
            num = Rational(random.randint(1, 20)  * random.choice([1, -1]),
                           random.randint(1, 9))

            if not mixed_form:
                nums.append(num)
                continue

            if num.has_mixed_form():
                nums.append(num)
            pass
        return nums
        pass

    def convert_to_mixed_form_rational_list(self, nums):
        nums_list = []
        for num in nums:
            num_list = num.expanded_list()
            nums_list.extend(num_list)
            pass
        return nums_list
        pass

    def convert_to_mixed_form_latex(self, nums):
        nums_mixed_form = []
        for num in nums:
            nums_mixed_form.append(num.mixed_form_latex())
            pass

        return nums_mixed_form
        pass

    def convert_to_mixed_calc_latex(self, nums):
        nums_mixed_calc = []
        for num in nums:
            nums_mixed_calc.append(num.mixed_calc_latex())
            pass

        return nums_mixed_calc
        pass

    def convert_to_mixed_expanded_latex(self, nums):
        nums_mixed_expanded = []
        for num in nums:
            nums_mixed_expanded.append(num.mixed_form_expanded_latex())
            pass
        return nums_mixed_expanded
        pass


    def get_random_rational_numbers(self, *numbers, **kwargs):
        size = kwargs.get('size', random.randint(2, 6))
        minV = kwargs.get('minV', 1)
        maxV = kwargs.get('maxV', 9)

        if len(numbers) < 1:
            rational_numbers = self._init_random_rational_numbers(size, minV, maxV)
            return rational_numbers

        rational_numbers = self._convert_to_rationals(*numbers)
        return rational_numbers
        pass


    def solution_common_denom(self, rational_numbers):
        soln_s, modified_numerators = self._compute_common_denom(rational_numbers)

        answer_s = soln_s[-1].replace("=", "").strip()

        soln_s = "\n\n".join(soln_s)
        return soln_s, answer_s
        pass


    def solution_sort(self, rational_numbers):
        (soln_s,
         sorted_rational_numbers,
         sorted_rational_numbers_str ) = self._compute_sort(rational_numbers)

        soln_s = "\n\n".join(soln_s)
        return soln_s, sorted_rational_numbers_str
        pass


    def solution_median(self, rational_numbers):
        (soln__sort,
         sorted_rational_numbers,
         sorted_rational_numbers_str ) = self._compute_sort(rational_numbers)


        soln_median, median = self._compute_median(rational_numbers)

        soln_s = soln__sort
        soln_s.extend(soln_median)

        answer_s = f"$\\displaystyle {median}$"

        soln_s = "\n\n".join(soln_s)
        return soln_s, answer_s
        pass


    def _compute_median(self, rational_numbers):
        sorted_rational_numbers = sorted(rational_numbers)

        soln_s = []

        arr_len = len(sorted_rational_numbers)
        if arr_len % 2 != 0:
            midx_latex = latex_fraction(f"{arr_len} + 1", 2)
            midx = (arr_len - 1) // 2
            midx_th = number_to_nth(midx + 1)

            median = sorted_rational_numbers[midx]

            stmt = f"""
            The number of fractions is {arr_len}, an odd number.
            Hence, the median will be the middle term,
            $\\displaystyle {midx_latex}$ = {midx_th} term.

            Median = $\\displaystyle {latex(median)}$
            """

            soln_s.append(stmt.strip())
            pass
        else:
            midxs = [arr_len // 2 - 1, arr_len // 2]

            midx_th_0 = number_to_nth(midxs[0] + 1)
            midx_th_1 = number_to_nth(midxs[1] + 1)


            midx_latex = latex_fraction(f"{arr_len} + 1", 2)
            midx_latex_2 = latex_fraction(f"{arr_len + 1}", 2)

            terms = [sorted_rational_numbers[x] for x in midxs]
            sum_terms = sum(terms)

            median = nsimplify(sum(terms) / 2)

            median_latex = latex_fraction(f"""{latex(terms[0])} + {latex(terms[1])}""",
                                          2)

            median_latex_2 = latex_fraction(f"""{latex(sum_terms)}""",
                                          2)

            stmt = f"""
            The number of fractions is {arr_len}, an even number.

            The middle term is, $
            \\displaystyle {midx_latex} = {midx_latex_2}
            $th term.

            Hence, the median will be average of {midx_th_0} and {midx_th_1} terms.

            Median

            $\\displaystyle = {median_latex}$

            $\\displaystyle = {median_latex_2}$

            $\\displaystyle = {latex(median)}$
            """

            soln_s.append(stmt.strip())
            pass

        return soln_s, median
        pass


    def _compute_sort(self, rational_numbers):
        soln_s, modified_numerators = self._compute_common_denom(rational_numbers,
                                            include_original = True)

        soln_s.append("Sort the fractions by values with common denominators:")

        denoms_lcm = self._get_denoms_lcm(rational_numbers)
        denoms_factors = self._get_common_denom_factors(rational_numbers)

        numers = [r.p * denoms_factors[idx]
                         for idx, r in enumerate(rational_numbers)]
        numers_sorted = sorted(numers)

        frct_sorted = [ latex_fraction(numer, denoms_lcm)
                        for idx, numer in enumerate(numers_sorted)
                      ]

        tuple_sorted = [ (numer, denoms_lcm)
                        for idx, numer in enumerate(numers_sorted)
                      ]

        tuple_sorted_str = self.rational_numbers_to_str(
                                        tuple_sorted,
                                        operator= ",",
                                        prepend = "")

        soln_s.append(tuple_sorted_str)

        sorted_rational_numbers = sorted(rational_numbers)

        terms_list = [f"\\left({latex(sorted_rational_numbers[i])} = {x}\\right)"
                      for i, x in enumerate(frct_sorted)]

        terms_list_str = ", ".join(terms_list)
        soln_s.append(f"$\\displaystyle = {terms_list_str}$")

        soln_s.append(f"""
        Write back original numbers in the sorted order:
        """.strip())

        sorted_rational_numbers_str = self.rational_numbers_to_str(
                                        sorted_rational_numbers,
                                        operator= ",",
                                        prepend = "")

        soln_s.append(sorted_rational_numbers_str)

        return soln_s, sorted_rational_numbers, sorted_rational_numbers_str
        pass


    def _compute_common_denom(self, rational_numbers, include_original = False):
        soln_s = []

        denoms_lcm = self._get_denoms_lcm(rational_numbers)
        denoms_factors = self._get_common_denom_factors(rational_numbers)
        modified_numerators = [r.p * denoms_factors[idx]
                         for idx, r in enumerate(rational_numbers)]

        if not self._are_denoms_common(rational_numbers):
            stmt = f"""
            Let us make all denominators equal to their LCM = {denoms_lcm}
            """
            soln_s.append(stmt.strip())

            stmt = self.rational_numbers_to_str(rational_numbers,
                            multiplier = denoms_factors,
                            simplify = False,
                            operator= ",",
                            include_original = include_original)

            soln_s.append(f"{stmt}")

            stmt = self.rational_numbers_to_str(rational_numbers,
                            multiplier = denoms_factors,
                            simplify = True,
                            operator= ",",
                            include_original = include_original)

            soln_s.append(f"{stmt}")
            pass

        return soln_s, modified_numerators
        pass


    def solution_sum(self, rational_numbers):
        soln_common_denom, modified_numerators = self._compute_common_denom(
                                                    rational_numbers)
        soln_sum, sum_value = self._compute_sum(rational_numbers)

        soln_s = soln_common_denom
        soln_s.extend(soln_sum)

        soln_s = "\n\n".join(soln_s)

        answer_s = f"$\\displaystyle {latex(sum_value)}$"
        return soln_s, answer_s
        pass

    def solution_average(self, rational_numbers):
        soln_sum, sum_value = self._compute_sum(rational_numbers)
        soln_average, average_value = self._compute_average(rational_numbers)

        soln_s = soln_sum
        soln_s.extend(soln_average)

        soln_s = "\n\n".join(soln_s)

        answer_s = f"$\\displaystyle {latex(average_value)}$"

        return soln_s, answer_s
        pass

    def _compute_average(self, rational_numbers):
        soln_s = []
        p = sum(rational_numbers)
        q = len(rational_numbers)

        average_latex = latex_fraction(latex(p), q)

        inverse_len = Rational(1, q)

        answer = Rational(p, q)

        stmt = f"""
        Average of numbers

        $\\displaystyle
        = {average_latex}
        $

        $\\displaystyle
        = {latex(inverse_len)} * {latex(p)}
        $

        $\\displaystyle
        = {latex(answer)}
        $

        """
        soln_s.append(stmt.strip())
        return soln_s, answer
        pass


    def _compute_sum(self, rational_numbers):
        soln_s = []

        denoms_lcm = self._get_denoms_lcm(rational_numbers)
        denoms_factors = self._get_common_denom_factors(rational_numbers)

        numr_terms = [r.p * denoms_factors[idx]
                      for idx, r in enumerate(rational_numbers)]

        num_terms_ltx = [f"\\frac{{ {latex(x)}  }}  {{ {latex(denoms_lcm)} }}"
                         for x in numr_terms]
        num_terms_ltx = " + ".join(num_terms_ltx)

        stmt = f"""

        $\\displaystyle
        = {num_terms_ltx}
        $

        As we have common denominator for all fractions, we can add all numerators.
        """
        soln_s.append(stmt.strip())

        numer_sum = sum(numr_terms)

        stmt = f"""
        $\\displaystyle
        = {latex_fraction(numer_sum, denoms_lcm)}
        $
        """
        soln_s.append(stmt.strip())

        reduced_latexes = self._reduce_rational_number_latexes(numer_sum, denoms_lcm)
        if reduced_latexes:
            for reduced_latex in reduced_latexes:
                soln_s.append(f"$\\displaystyle = {reduced_latex}$")
                pass

            stmt = f"""
            $\\displaystyle
            = {latex(sum(rational_numbers))}
            $
            """
            soln_s.append(stmt.strip())
            pass

        sum_value = sum(rational_numbers)
        return soln_s, sum_value
        pass


    def _are_denoms_common(self, rational_numbers):
        denoms = set([r.q for r in rational_numbers])
        if len(denoms) == 1:
            return True
        return False
        pass

    def _get_denoms_lcm(self, rational_numbers):
        denoms = [r.q for r in rational_numbers]
        return MathUtil.lcm(*denoms)

    def _get_common_denom_factors(self, rational_numbers):
        denoms_lcm = self._get_denoms_lcm(rational_numbers)
        return [denoms_lcm // r.q for r in rational_numbers]
        pass


    def _convert_to_rationals(self, *numbers):
        return [Rational(n[0], n[1]) for n in numbers]
        pass


    def _init_random_rational_numbers(self, size, minV, maxV):
        rational_numbers = []
        while len(rational_numbers) < size:
            nSign = 1 if random.random() < 0.5 else -1
            n = np.random.randint(low = minV,
                            high = maxV,
                            size = (2)
                           ).astype('int')
            r = Rational(n[0] * nSign, n[1])
            rational_numbers.append(r)
            pass
        return rational_numbers
        pass



    def get_rational_denominator_transform(self):
        p = random.randint(0, 9)
        q = random.randint(1, 9)
        ratio = random.randint(2, 9)
        n = Rational(p, q)

        n_latex = latex_fraction(p, q)
        n_similar_expanded_latex = latex_fraction(f"{p} * {ratio}", f"{q} * {ratio}")
        n_similar_latex = latex_fraction(p * ratio, q * ratio)
        factor_latex = latex_fraction(q * ratio, q)

        result = {'n': n,
                  'n_latex': n_latex,
                  'q': q,
                  'ratio': ratio,
                  'factor_latex': factor_latex,
                  'n_similar_expanded_latex': n_similar_expanded_latex,
                  'n_similar_latex': n_similar_latex,

                  }

        return result
        pass

    def get_similar_rational_pairs(self):
        p1 = random.randint(0, 9)
        q1 = random.randint(1, 9)
        n1 = Rational(p1, q1)
        n1_latex = latex_fraction(p1, q1)

        p2 = random.randint(0, 9)
        q2 = random.randint(1, 9)
        n2 = Rational(p2, q2)
        n2_latex = latex_fraction(p2, q2)

        lcm  = MathUtil.lcm(q1, q2)
        n1_multiplier = lcm // q1
        n2_multiplier = lcm // q2

        n1_sim_expanded = latex_fraction(f"{p1} * {n1_multiplier}", f"{q1} * {n1_multiplier}")
        n2_sim_expanded = latex_fraction(f"{p2} * {n2_multiplier}", f"{q2} * {n2_multiplier}")

        n1_sim = latex_fraction(f"{p1 * n1_multiplier}", f"{q1 * n1_multiplier}")
        n2_sim = latex_fraction(f"{p2 * n2_multiplier}", f"{q2 * n2_multiplier}")


        result = {'n1_latex': n1_latex,
                  'n2_latex': n2_latex,
                  'q1': q1,
                  'q2': q2,
                  'lcm': lcm,
                  'n1_multiplier': n1_multiplier,
                  'n2_multiplier': n2_multiplier,
                  'n1_sim_expanded': n1_sim_expanded,
                  'n2_sim_expanded': n2_sim_expanded,
                  'n1_sim': n1_sim,
                  'n2_sim': n2_sim,
            }

        return result
        pass

    def get_random_price_qty_data(self,
                                   int_qty = False,
                                   simple_fraction_qty = False,
                                   unit_price_100 = True,
                                   decimal_answer = True,
                                   ):
        name = random.choice(['cake', 'chocolate', 'icecream'])
        currency, currency_name = self.get_random_currency()

        if unit_price_100:
            unit_price = 100
        else:
            unit_prices = [random.randint(1, 10),
                      random.randint(1, 10) * 10,
                      random.randint(1, 10) * 100]
            unit_price = random.choice(unit_prices)
            pass

        if int_qty:
            qty = random.randint(2, 10)
            amount = qty * unit_price

        elif simple_fraction_qty:
            qty = Rational(1, random.choice([2, 4, 5, 10, 20, 25, 50]))
            amount = toIntOrFloat(qty * unit_price)

        else:
            amounts = [random.randint(1, 10),
                      random.randint(1, 10) * 10,
                      random.randint(1, 10) * 100]
            amount = random.choice(amounts)
            hcf = MathUtil.hcf(amount, unit_price)
            qty = Rational(amount//hcf, unit_price//hcf)

            pass

        currency_latex = self._get_currency_latex(currency)


        amount_with_currency = f"{currency_latex} {latex(amount)}"
        unit_price_with_currency = f"{currency_latex} {latex(unit_price)}"



        qty_rational_latex = latex_fraction(amount_with_currency, unit_price_with_currency)

        computation_latex, decimal_qty = self._get_qty_computation_latex(
                                name, currency, amount, unit_price,
                                qty,
                                decimal_answer = decimal_answer)

        if int_qty:
            int_qty_computation_latex = self._get_int_qty_computation_latex(
                                name, currency, amount, unit_price)
            if int_qty_computation_latex:
                computation_latex = f"""
                {int_qty_computation_latex}

                <h4>We can calculate it in another way:</h4>
                {computation_latex}
                """.strip()
            pass

        if decimal_qty:
            qty_stmt = f"""
            $\\displaystyle
            {latex(qty)}
            $ {name}s = $\\displaystyle
            {latex(decimal_qty)}
            $ {name}s
            """
            pass
        else:
            qty_stmt = f"""
            $\\displaystyle
            {latex(qty)}
            $ {name}s"""
            pass

        data = {'name': name,
              'currency': currency,
              'currency_latex': currency_latex,
              'currency_name': currency_name,
              'unit_price': unit_price,
              'qty': qty,
              'qty_stmt': qty_stmt.strip(),
              'amount': amount,
              'amount_with_currency': amount_with_currency,
              'unit_price_with_currency': unit_price_with_currency,
              'qty_rational_latex': qty_rational_latex,
              'computation_latex': computation_latex,
             }

        return data
        pass

    def _get_currency_latex(self, currency):
        if not  r'$' in f"{currency}":
            currency_latex = f"{latex(currency)}"
            return currency_latex

        currency_latex = f"{currency}"
        currency_latex = currency_latex.replace(r"\mathtt{\text{", r"{{")
        currency_latex = currency_latex.replace(r"\\$", r"\$")
        return currency_latex
        pass


    def _get_qty_computation_latex(self,
                                   name, currency,
                                   amount, unit_price,
                                   qty,
                                   decimal_answer = True,
                                   prefix = "="):
        stmts = []

        currency_latex = self._get_currency_latex(currency)

        unit_price_with_currency = f"{currency_latex} {latex(unit_price)}"
        amount_with_currency = f"{currency_latex} {latex(amount)}"
        price_latex =  latex_fraction(f"1 \\; \\text{{{name}}}",
                                      unit_price_with_currency)

        qty_rational_latex = latex_fraction(f"{latex(amount)}",
                                            f"{latex(unit_price)}")



        stmt = f"""
        Let us start with price of the {name}.
        As we need the qty of {name}, let us keep it in numerator:

        $\\displaystyle
        {price_latex}
        $

        $\\displaystyle
        = {price_latex} * {amount_with_currency}
        $ \t\t(as we have $\\displaystyle {amount_with_currency}$)

        $\\displaystyle
        = {qty_rational_latex} \\; \\text{{{name}}}
        $ \t\t(cancel $\\displaystyle {currency_latex}$ from numerator and denominator)
        """.strip()
        stmts.append(stmt)

        if amount != unit_price:
            hcz = MathUtil.highest_common_tens_exp(amount, unit_price)
            if hcz > 1:
                stmt = latex_fraction(f"{amount}\\;/\\;{hcz}", f"{unit_price}\\;/\\;{hcz}")
                stmts.append(f"$\\displaystyle {prefix} {stmt} \\; \\text{{{name}}}  $ \t\t(cancel zeroes)")
                amount = amount // hcz
                unit_price = unit_price // hcz

                stmt = latex_fraction(f"{amount}", f"{unit_price}")
                stmts.append(f"$\\displaystyle {prefix} {stmt} \\; \\text{{{name}}}  $ ")
                pass
            pass

        reduced_latexes = self._reduce_rational_number_latexes(amount, unit_price)
        if reduced_latexes:
            for reduced_latex in reduced_latexes:
                stmts.append(f"$\\displaystyle {prefix} {reduced_latex} \\; \\text{{{name}}} $")
                pass
            pass

        stmts.append(f"$\\displaystyle {prefix} {latex(qty)} \\; \\text{{{name}}} $")

        decimal_qty = self._get_decimal_if_not_integer(amount, unit_price, 4)
        if (decimal_answer and decimal_qty):
            stmts.append(f"$\\displaystyle {prefix} {decimal_qty} \\; \\text{{{name}}} $")
            pass


        stmts = "\n\n".join(stmts)
        return stmts.strip(), decimal_qty
        pass

    def _reduce_rational_number_latexes(self, p, q):
        lcf = MathUtil.lowest_prime_divisor(p, q)

        reduced_latexes = []
        while lcf:
            if lcf < 1:
                break

            #stmt = latex_fraction(f"{p}", f"{q}")
            #reduced_latexes.append(stmt)

            stmt = latex_fraction(f"{p}\\;/\\;{lcf}", f"{q}\\;/\\;{lcf}")
            reduced_latexes.append(stmt.strip())

            p = p // lcf
            q = q // lcf

            stmt = latex_fraction(f"{p}", f"{q}")
            reduced_latexes.append(stmt)


            lcf = MathUtil.lowest_prime_divisor(p, q)
            pass
        return reduced_latexes
        pass


    def _get_int_qty_computation_latex(self, name, currency,
                                       amount, unit_price ):

        currency_latex = self._get_currency_latex(currency)

        stmts = []

        cal_amount = 1

        qty = 1
        while cal_amount < amount:
            cal_amount = unit_price * qty
            stmt = f"""
            {qty} {name} $\\displaystyle
            = {qty} * {currency_latex} {unit_price}
            = {currency_latex} {cal_amount}
            $
            """
            stmts.append(stmt.strip())

            qty = qty + 1
            pass

        stmts = "\n".join(stmts)
        return stmts.strip()
        pass


    def _get_decimal_if_not_integer(self, p, q, precision = 4):
        if p // q == round(p / q, precision):
            return None
        r = toIntOrFloat(p / q, precision)
        return r

    def rational_numbers_to_str(self, rational_numbers,
                        multiplier = 1,
                        simplify = True,
                        prepend = "= ",
                        operator = "+",
                        include_original = False
                        ):

        if include_original:
            rn_list = self.rational_numbers_to_str_list(
                                          rational_numbers,
                                          multiplier = multiplier,
                                          simplify = simplify,
                                          operator = ",")
            rn_list = [f"\\left({latex(rational_numbers[i])} = {x}\\right)"
                       for i, x in enumerate(rn_list)]
        else:
            rn_list = self.rational_numbers_to_str_list(
                                          rational_numbers,
                                          multiplier = multiplier,
                                          simplify = simplify,
                                          operator = operator)
            pass

        if operator.strip() == ",":
            sOut = f"{operator}".join(rn_list)
        else:
            sOut = "".join(rn_list)

        sOut = sOut.strip()

        if sOut[0] == operator:
            sOut = sOut[1:]

        return wrap_latex_statement(prepend + sOut)
        pass

    def rational_numbers_to_str_list(self,
                                     rational_numbers,
                                     multiplier = 1,
                                     simplify = True,
                                     operator = "+"
                                     ):
        rn_list = []
        for idx, r in enumerate(rational_numbers):

            rf = self._transform_rational_number(r, idx,
                                            multiplier = multiplier,
                                            simplify = simplify,
                                            operator = operator)
            rn_list.append(rf)
            pass

        return rn_list
        pass

    def _transform_rational_number(self,
                                   r,
                                   idx,
                                   multiplier = 1,
                                   simplify = True,
                                   operator = "+"):

        if hasattr(r, "p") and hasattr(r, "q"):
            r_p = r.p
            r_q = r.q
        else:
            r_p = r[0]
            r_q = r[1]
            r = Rational(r_p, r_q)

        if multiplier == 1:
            rf = latex_fraction(abs(r_p), abs(r_q))
        else:
            if simplify:
                rf = latex_fraction(abs(r_p * multiplier[idx]),
                                    abs(r_q * multiplier[idx]))
            else:
                rf = latex_fraction(f"{abs(r_p)} * {multiplier[idx]}",
                                    f"{abs(r_q)} * {multiplier[idx]}")
            pass

        if operator.strip() == ",":
            if r < 0:
                return "- " + rf
            return rf

        if operator.strip() == "+":
            if r < 0:
                return "- " + rf
            if idx == 0:
                return rf
            return "+ " + rf

        if r < 0:
            return f" {operator} -" + rf

        return f" {operator} " + rf
        pass



    def solution_plot(self, rational_numbers):
        (soln_sorted,
         sorted_rational_numbers,
         sorted_rational_numbers_str ) = self._compute_sort(rational_numbers)

        sorted_composite_rational_numbers_str = soln_sorted[-4]
        sorted_rational_numbers_str = soln_sorted[-1]

        soln_sorted = "\n\n".join(soln_sorted)

        soln_common_denom, modified_numerators = self._compute_common_denom(
                                        rational_numbers)


        soln_common_denom = "\n\n".join(soln_common_denom)

        soln_median, median = self._compute_median(rational_numbers)
        soln_median = "\n\n".join(soln_median)

        soln_sum, sum_value = self._compute_sum(rational_numbers)
        soln_sum = "\n\n".join(soln_sum)

        soln_average, average = self._compute_average(rational_numbers)
        soln_average = "\n\n".join(soln_average)

        plt.ioff()
        self._plot_rational_numbers(rational_numbers,
                                    modified_numerators,
                                    average,
                                    median,
                                    )
        svgHelper = SvgHelper()
        svg_str = svgHelper.plt_to_svg()
        #plt.figure().clear()
        plt.clf()
        plt.close()



        result = {
            'soln_common_denom': soln_common_denom,
            'sorted_rational_numbers': sorted_rational_numbers,
            'sorted_rational_numbers_str': sorted_rational_numbers_str,
            'sorted_composite_rational_numbers_str': sorted_composite_rational_numbers_str,
            'soln_sorted': soln_sorted,
            'soln_median': soln_median,
            'median': median,
            'soln_average': soln_average,
            'average': average,
            'soln_sum': soln_sum,
            'sum_value': sum_value,
            'svg_str': svg_str,

            }

        return result
        pass


    def _plot_rational_numbers(self,
                               rational_numbers,
                               modified_numerators,
                               average,
                               median,
                               ):
        lineNumbers = self._initLineNumbers(modified_numerators)
        denoms_lcm = self._get_denoms_lcm(rational_numbers)

        xytext = (0, 0)
        fig = plt.figure(figsize= (6, 10))
        ax = plt.axes()
        plt.xlim(-2, 2)
        lns = lineNumbers

        plt.plot((0, 0), (lns[0], lns[-1]))
        for i in lns:
            plt.plot(0, i, marker = 'o', markersize = '5', color = "grey")

            label = f"{i}/{denoms_lcm}"
            plt.annotate(label,
                        xy=(-.1, i),
                        ha='right',
                        rotation = 0,
                        fontsize= int(RationalNumberHelper._DEFAULT_FONT_SIZE * .60),
                        )

        mns = [(x, rational_numbers[i].p)
               for i, x in enumerate(modified_numerators)]

        mns.append([average.p * denoms_lcm // average.q, average.p])
        mns.append([median.p * denoms_lcm // median.q, median.p])

        rns = rational_numbers
        rns.append(average)
        rns.append(median)
        for idx, n in enumerate(mns):
            point, = plt.plot(0, n[0], marker = 'o', markersize = '10')
            label = f"{rns[idx]} = {mns[idx][0]}/{denoms_lcm}"

            xy_offset = (+.2, n[0])
            if idx == len(mns) - 2:
                label = f"{label} (Avg)"
                xy_offset = (+1.5, n[0])
                pass

            if idx == len(mns) - 1:
                label = f"{label} (Med)"
                xy_offset = (+1.5, n[0])
                pass

            plt.annotate(label,
                        xy=xy_offset,
                        ha='left',
                        rotation = 0,
                        fontsize= RationalNumberHelper._DEFAULT_FONT_SIZE * .75,
                         color = point.get_color()
                        )

        self._showAxesAndGrid(plt.gca())
        pass

    def _showAxesAndGrid(self, ax, showFlag = False):
        ax.xaxis.set_visible(showFlag)
        ax.yaxis.set_visible(showFlag)
        ax.set_frame_on(showFlag)
        ax.grid(showFlag)


    def _initLineNumbers(self, nums, pointCount = 10):
        low = min(nums)
        high = max(nums)

        interval = self._getInterval(high-low, pointCount)

        low = (low // interval - 1) * interval
        high = (high // interval + 1) * interval

        linspacePoints = np.arange(low - interval,
                                   high +  interval + 1,
                                   interval)
        return linspacePoints
        pass

    def _getInterval(self, numRange, pointCount = 10):
        a1 = 10 ** np.arange(6)
        a2 = a1 * 2
        a5 = a1 * 5
        intervalArray =  np.concatenate((a1, a2, a5))
        interval = numRange // pointCount + 1
        offsetArray =  abs(intervalArray - interval)

        indOfMinOffset = np.unravel_index(np.argmin(
            offsetArray, axis=None), offsetArray.shape)[0]
        interval = intervalArray[indOfMinOffset]
        return interval
        pass

    def get_currency_names(self):
        try:
            from forex_python.converter import CurrencyCodes
        except:
            print("Module missing: Please install: \n\t pip install forex-python")

        c = CurrencyCodes()
        return [x['cc'] for x in c._currency_data]

    def get_random_currency(self):
        from forex_python.converter import CurrencyCodes
        c = CurrencyCodes()
        name =  random.choice(self.get_currency_names())
        currency_symbol_latex = latex(c.get_symbol(name)).replace(r"$", r"\$")

        currency_symbol = Symbol(f"{currency_symbol_latex}\\;")

        return [currency_symbol, c.get_currency_name(name)]
        pass


    def get_problem_fractions_from_competitions(self, *args, **kwargs):
        sInfo = f"""Processing {self.__class__.__name__}.{inspect.stack()[0][3]}: """
        self._log(sInfo.strip())

        index = kwargs.get('index', None)

        try:

            if index is None:
                random_choices = list(range(1, 10))

                identifier = f"{self.__class__.__name__}{id(self)}.{inspect.stack()[0][3]}.random_choices"
                random_choice = self.get_no_repeat_random_choice(identifier, random_choices)
            else:
                random_choice = index

            if random_choice == 1:
                return self._apos_one()

            if random_choice == 2:
                return self._apos_two()

            if random_choice == 3:
                return self._grade_11_12_problems()

            if random_choice == 4:
                return self._grade_11_12_problems_two()

            if random_choice == 5:
                return self._grade_03_04_problems_one()

            if random_choice == 6:
                return self._grade_03_04_problems_two()

            if random_choice == 7:
                return self._grade_03_04_problems_three()

            if random_choice == 8:
                return self._grade_05_06_problems_one()



            return self._grade_11_12_problems()

        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}: {e}
            """
            self._log(sError)
            self._log(traceback.format_exc())
            pass

        return None
        pass

    def _apos_one(self, **kwargs):
        prob_s = f"""
        Call a fraction $\\displaystyle \\frac{{a}}{{b}}$,
        not necessarily in the simplest form, special
        if $a$ and $b$ are positive integers whose sum is 15 .
        How many distinct integers can be written as the sum of two,
        not necessarily different, special fractions?
        <br/>
        (A) 9
        (B) 10
        (C) 11
        (D) 12
        (E) 13


        """
        prob_s = prob_s.replace("\n", " ").strip()


        soln_url = "https://artofproblemsolving.com/wiki/index.php/2021_Fall_AMC_12B_Problems/Problem_5"

        soln_s = f"""
        <a target="_blank" href="{soln_url}"> See solution at AOPS website</a>
        """
        soln_s = soln_s.replace("\n", " ").strip()

        answer_s = f"""
        <a target="_blank" href="{soln_url}"> See solution at AOPS website</a>
        """
        answer_s = answer_s.replace("\n", " ").strip()

        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result


    def _apos_two(self, **kwargs):
        prob_s = f"""
        Let $S$ be the set of all positive rational numbers $r$
        such that when the two numbers $r$ and $55 r$ are written
        as fractions in lowest terms,
        the sum of the numerator and denominator of one
        fraction is the same as the sum of the numerator
        and denominator of the other fraction.
        The sum of all the elements of $S$ can be expressed
        in the form $\\displaystyle \\frac{{p}}{{q}}$, where $p$ and $q$
        are relatively prime positive integers. Find $p+q$.
        """
        prob_s = prob_s.replace("\n", " ").strip()


        soln_url = "https://artofproblemsolving.com/wiki/index.php/2023_AIME_II_Problems/Problem_5"

        soln_s = f"""
        <a target="_blank" href="{soln_url}"> See solution at AOPS website</a>
        """
        soln_s = soln_s.replace("\n", " ").strip()

        answer_s = f"""
        <a target="_blank" href="{soln_url}"> See solution at AOPS website</a>
        """
        answer_s = answer_s.replace("\n", " ").strip()

        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result

    def _grade_11_12_problems(self, **kwargs):
        prob_s = f"""
        When divided by 3,6 , and 12 , a number has remainders
        $R_1, R_2$, and $R_3$, respectively.
        If the sum of the three remainders is equal to 15 , what is $R_1$ ?
        """
        prob_s = prob_s.replace("\n", " ").strip()



        soln_s = f"""
        TODO:
        $R_1=1$
        The other remainders are
        $R_2=4$ and
        $R_3=10 .

        R_2-R_1$ is a multiple of 3 , and
        $R_3-R_2$ is a multiple of 6 .
        """
        soln_s = soln_s.replace("\n", " ").strip()

        answer_s = f"""
        $R_1=1$
        """
        answer_s = answer_s.replace("\n", " ").strip()

        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result

    def _grade_11_12_problems_two(self, **kwargs):

        problemTemplate = KProblemTemplate()

        image_url = "see image::\images\math\grade_11_12_problems_two.png"
        image_url = problemTemplate.transform_image_links(image_url.strip())


        prob_s = f"""
        A trapezoid ABCD, where AB is parallel to CD, is circumscribed around a
        circle. The sides AD and BC are the diameters of two circles (not shown).
        Prove that these two circles are tangent, i.e. intersect at exactly one point.
        <br/>
        {image_url}
        """
        prob_s = prob_s.replace("\n", " ").strip()


        soln_s = f"""
        TODO:
        If a circle is inscribed in a quadrilateral, then the sums of the lengths of opposite
        sides are the same. The line connecting the centers of the tangent circles is the
        center line of the trapezoid and goes through the point of tangency. Its length
        is half the sum of the sides AD and BC and therefore equal to the sum of the
        radii of the two circles.
        """
        soln_s = soln_s.replace("\n", " ").strip()

        answer_s = f"""
        """
        answer_s = answer_s.replace("\n", " ").strip()

        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result

    def _grade_03_04_problems_one(self, **kwargs):

        n = random.randint(20, 1000)
        count = random.randint(3, 10)
        points = list('ABCDEFGHIJKLMN')[:count]
        points_str = list_to_str(points, last_sep = " \\; and \\; ")

        last_idx = (n - 1) %  count

        prob_s = f"""
        An ant travels around a circle in the direction shown. As it moves, it touches
        each of the labeled points in order. The first three points that the ant touches
        are ${points_str}$, in that order. What is the ${n}$th point that the ant touches?
        """
        prob_s = prob_s.replace("\n", " ").strip()


        soln_s = f"""
        ${n}$th point is actually at index ${n - 1}
        $ if we start counting from $0$.

        Now, if we divide ${n - 1}$ with the count of points, ${count}
        $, the remainder gives the index of the exact last point the ant touches.

        $\\displaystyle
        \\text{{Index of the exact last point }}
        = {n - 1} \\; \\% \\; {count}

        = {last_idx}
        $

        $\\displaystyle
        \\begin{{aligned}}

        & \\; \\text{{The exact last point }}
        \\\\
        = & \\; \\text{{The point at index }} {last_idx} \\text{{ in }} {points_str}
        \\text{{ when counting starts at 0 }}
        \\\\
        = & \\; {points[last_idx]}

        \\end{{aligned}}
        $

        """
        soln_s = soln_s.strip()

        answer_s = f"""
        {points[last_idx]}
        """
        answer_s = answer_s.replace("\n", " ").strip()

        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result



    def _grade_03_04_problems_two(self, **kwargs):

        prob_s = f"""
        Camila is hanging upside-down from the monkey bars at the playground. If her
        left ear is facing east, then what direction is her nose facing?
        """
        prob_s = prob_s.replace("\n", " ").strip()


        soln_s = f"""
        TODO:
        """
        soln_s = soln_s.replace("\n", " ").strip()

        answer_s = f"""
        North
        """
        answer_s = answer_s.replace("\n", " ").strip()

        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result




    def _grade_03_04_problems_three(self, **kwargs):

        prob_s = f"""
        A worker must paint a fence made out of vertical wooden poles. Whenever two
        poles are such that 2 or 3 other poles are in between them, the two poles are
        required to be painted in different colors. What is the smallest number of colors
        the worker can use to paint the fence?
        """
        prob_s = prob_s.replace("\n", " ").strip()


        soln_s = f"""
        TODO:
        """
        soln_s = soln_s.replace("\n", " ").strip()

        answer_s = f"""
        3
        """
        answer_s = answer_s.replace("\n", " ").strip()

        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result



    def _grade_05_06_problems_one(self, **kwargs):

        prob_s = f"""
        The denominators of two fractions are consecutive natural numbers. Both
        fractions are in lowest terms, and their sum is
        $\\displaystyle \\frac {{51}} {{56}}$.
        Find the greater of the two fractions.
        """
        prob_s = prob_s.replace("\n", " ").strip()


        soln_s = f"""
        TODO:
        """
        soln_s = soln_s.replace("\n", " ").strip()

        answer_s = f"""
        $\\displaystyle \\frac {{5}} {{8}}$
        """
        answer_s = answer_s.replace("\n", " ").strip()

        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result



    def get_problem_simplify_rational_numbers(self, **kwargs):

        pn_helper = self.get_prime_number_helper()

        size = random.randint(3, 7)
        prime_numbers = pn_helper.getPrimeNumbers(size = size - size // 3)


        nums = []
        ps = []
        qs = []
        for i in range(size):
            p = random.randint(1, 9) * random.choice(prime_numbers)
            q = random.randint(1, 9) * random.choice(prime_numbers)

            nums.append(Rational(p, q, gcd=1))
            ps.append(p)
            qs.append(q)
            pass

        nums_ltx = [f"{latex(x)}" for x in nums]
        nums_ltx_str = "\\; \\times \\;".join(nums_ltx)

        prob_s = f"""
        Simplify $\\displaystyle {nums_ltx_str} $
        """
        prob_s = prob_s.replace("\n", " ").strip()


        soln_stmts = []

        soln_stmt = f"""
        $\\displaystyle
        \\begin {{aligned}}

        & {nums_ltx_str}
        \\\\ \\\\
        """
        soln_stmts.append(soln_stmt.strip())

        result_product = self._get_simplify_product_stmts(ps, qs, prime_numbers)

        soln_stmts.extend(result_product['soln_stmts'])
        ps = result_product['ps']
        qs = result_product['qs']


        num_calc = Rational(sp.Mul(*ps), sp.Mul(*qs))
        soln_stmt = f"""
        & = {latex(num_calc)}
        """
        soln_stmts.append(soln_stmt.strip())



        soln_stmt = f"""
        \\end {{aligned}}
        $
        """
        soln_stmts.append(soln_stmt.strip())


        answer_s = f"""
        $\\displaystyle {latex(num_calc)}$
        """
        answer_s = answer_s.replace("\n", " ").strip()

        soln_s = "\n\n".join(soln_stmts)

        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result



    def get_problem_simplify_divisions_of_rational_numbers(self, **kwargs):

        pn_helper = self.get_prime_number_helper()

        size = random.randint(3, 7)
        prime_numbers = pn_helper.getPrimeNumbers(size = size - size // 3)

        ops = [f"\\times", f"\\div"]

        num_pairs = []
        mul_nums = []
        nums_ltx = []
        for i in range(size):
            p = random.randint(1, 9) * random.choice(prime_numbers)
            q = random.randint(1, 9) * random.choice(prime_numbers)
            num = Rational(p, q, gcd=1)
            inv_num = Rational(q, p, gcd=1)

            if i == 0:
                num_op = ""
                nums_ltx.append(f"{latex(num)}")
                mul_nums.append(num)
            else:
                num_op = random.choice(ops)
                nums_ltx.append(f"{num_op} \\; {latex(num)}")

                if num_op == ops[0]:
                    mul_nums.append(num)
                else:
                    mul_nums.append(inv_num)
                pass


            num_pairs.append([num, num_op])
            pass

        nums_ltx_str = "\\;".join(nums_ltx)

        mul_nums_ltx = [f"{latex(x)}" for x in mul_nums]
        mul_nums_ltx_str = "\\; \\times \\;".join(mul_nums_ltx)


        prob_s = f"""
        Simplify $\\displaystyle {nums_ltx_str} $
        """
        prob_s = prob_s.replace("\n", " ").strip()


        soln_stmts = []

        soln_stmt = f"""
        $\\displaystyle
        \\begin {{aligned}}

        & {nums_ltx_str}
        \\\\ \\\\

        & \\text{{Convert each division symbol into a multiplication symbol
        and invert the number following the division symbol. }}
        \\\\ \\\\

        & = {mul_nums_ltx_str}
        \\\\ \\\\
        """
        soln_stmts.append(soln_stmt.strip())

        ps = [x.p for x in mul_nums]
        qs = [x.q for x in mul_nums]

        result_product = self._get_simplify_product_stmts(ps, qs, prime_numbers)

        soln_stmts.extend(result_product['soln_stmts'])
        ps = result_product['ps']
        qs = result_product['qs']


        num_calc = Rational(sp.Mul(*ps), sp.Mul(*qs))
        soln_stmt = f"""
        & = {latex(num_calc)}
        """
        soln_stmts.append(soln_stmt.strip())



        soln_stmt = f"""
        \\end {{aligned}}
        $
        """
        soln_stmts.append(soln_stmt.strip())


        answer_s = f"""
        $\\displaystyle {latex(num_calc)}$
        """
        answer_s = answer_s.replace("\n", " ").strip()

        soln_s = "\n\n".join(soln_stmts)

        result = {
            'prob_s': prob_s.strip(),
            'soln_s': soln_s.strip(),
            'answer_s': answer_s.strip(),
            }

        return result



    def _get_simplify_product_stmts(self, ps, qs, prime_numbers):
        soln_stmts = []
        while True:
            common_divisor_result = self._get_common_divisor(ps, qs, prime_numbers)
            if common_divisor_result is None:
                break

            p_idx, q_idx, common_divisor = common_divisor_result

            step_result = self._update_ps_qs(ps, qs, p_idx, q_idx, common_divisor)

            ps = step_result['ps_mod']
            qs = step_result['qs_mod']

            soln_stmt = f"""
            & = {step_result['mod_nums_ueval_ltx_str']}
            \\\\ \\\\
            & = {step_result['mod_nums_ltx_str']}
            \\\\ \\\\
            """
            soln_stmts.append(soln_stmt.strip())

            pass

        result = {
            'soln_stmts': soln_stmts,
            'ps': ps,
            'qs': qs,
            }

        return result


    def _update_ps_qs(self, ps, qs, p_idx, q_idx, common_divisor):

        common_divisor_ltx = latex_color(latex(common_divisor), color = 'red')

        ps_mod = []
        ps_mod_uval = []

        qs_mod = []
        qs_mod_uval = []

        mod_nums = []
        mod_nums_ueval = []

        for idx in range(len(ps)):

            p_mod = ps[idx]
            p_mod_ueval = p_mod
            if idx == p_idx:
                p_mod = p_mod // common_divisor
                p_mod_ueval = Symbol(f"{latex(p_mod_ueval)} \\; / \\; {common_divisor_ltx}")

            q_mod = qs[idx]
            q_mod_ueval = q_mod
            if idx == q_idx:
                q_mod = q_mod // common_divisor
                q_mod_ueval = Symbol(f"{latex(q_mod_ueval)} \\; / \\; {common_divisor_ltx}")

            ps_mod.append(p_mod)
            ps_mod_uval.append(p_mod_ueval)

            qs_mod.append(q_mod)
            qs_mod_uval.append(q_mod_ueval)

            mod_num = Rational(p_mod, q_mod, gcd = 1)
            mod_nums.append(mod_num)

            mod_num_ueval = Symbol(f"\\frac{{ {latex(p_mod_ueval)} }}{{ {latex(q_mod_ueval)} }}")
            mod_nums_ueval.append(mod_num_ueval)

            pass


        mod_nums_ltx = [f"\\frac{{ {latex(x.p)} }}{{ {latex(x.q)} }}"
                        for x in mod_nums]
        mod_nums_ltx_str = "\\; \\times \\;".join(mod_nums_ltx)

        mod_nums_ueval_ltx = [f"{latex(x)}" for x in mod_nums_ueval]
        mod_nums_ueval_ltx_str = "\\; \\times \\;".join(mod_nums_ueval_ltx)

        result = {
            'ps_mod': ps_mod,
            'ps_mod_uval': ps_mod_uval,
            'qs_mod': qs_mod,
            'qs_mod_uval': qs_mod_uval,
            'mod_nums': mod_nums,
            'mod_nums_ueval': mod_nums_ueval,
            'mod_nums_ltx_str': mod_nums_ltx_str,
            'mod_nums_ueval_ltx_str': mod_nums_ueval_ltx_str,
            }

        return result
        pass

    def _get_common_divisor(self, ps, qs, prime_numbers):

        common_result = self._get_common_numbers(ps, qs)
        if not common_result is None:
            return common_result

        for prime_number in prime_numbers:
            for p_idx, p in enumerate(ps):
                if p % prime_number != 0:
                    continue

                for q_idx, q in enumerate(qs):
                    if q % prime_number == 0:
                        return (p_idx, q_idx, prime_number)
                    pass
                pass
            pass

        return None
        pass


    def _get_common_numbers(self, ps, qs):
        for p_idx, p in enumerate(ps):
            if p == 1:
                continue
            for q_idx, q in enumerate(qs):
                if p == q:
                    return (p_idx, q_idx, p)
                pass
            pass
        return None
        pass


    pass
#class RationalNumberHelper:
