'''
Created on 20-Jan-2022

@author: bthakur
'''

import inspect
import traceback
import random
import numpy as np

from sympy import (Symbol, symbols, factor,
                   simplify, Rational, latex,
                   Add, Mul,
                   )

from xv.km.util import latex_fraction, latex_color
from sympy.core.expr import UnevaluatedExpr as ueval

from xv.util import toIntOrFloat, list_to_str, format_integers
from sympy.core.sympify import sympify

# _fast_calculation_helper
# FastCalculationHelper

class FastCalculationHelper:

    def __init__(self, **kwargs):
        self.verbose = kwargs.get('verbose', False)
        pass


    def _format_numbers_to_sum_html(self, nums):
        nums.append(nums[0] + nums[1])
        nums_str = format_integers(nums, leading_space = "&nbsp;")

        nums_str[0] = f"&nbsp;{nums_str[0]}"
        nums_str[1] = f"<u>+{nums_str[1]}</u>"

        nums_str[2] = f"""<span style="color: red;">&nbsp;{nums_str[2]}</span>"""

        nums_str = "<br/>".join(nums_str)

        nums_str = f"""
        <div style="font-family: monospace; font-size:2rem; line-height: 1.3em;">{nums_str}</div>
        """
        return nums_str.strip()

    def _format_numbers_to_diff_html(self, nums):
        nums.append(nums[0] - nums[1])
        nums_str = format_integers(nums, leading_space = "&nbsp;")

        nums_str[0] = f"&nbsp;{nums_str[0]}"
        nums_str[1] = f"<u>-{nums_str[1]}</u>"

        nums_str[2] = f"""<span style="color: red;">&nbsp;{nums_str[2]}</span>"""

        nums_str = "<br/>".join(nums_str)

        nums_str = f"""
        <div style="font-family: monospace; font-size:2rem; line-height: 1.3em;">{nums_str}</div>
        """
        return nums_str.strip()

    def get_problem_square_ending_with_5(self, *args, **kwargs):
        n = random.randint(1, 12) * 10 + 5

        n_str = str(n)
        left_num1 = int(n_str[:-1])
        left_num2 = left_num1 + 1
        left_num = left_num1 * left_num2
        right_num1 = 5
        right_num2 = 5
        right_num = right_num1 * right_num2

        problem_s = f"""
        Find square of ${n}$.
        """

        soln_s = f"""
        square of ${n} =  {n} * {n}$

        As the number is ending with 5 in unit place, we can apply the following rule:

        To find square of number ending with digit 5,
        Remove the last digit {right_num1}, we get = ${left_num1}$
        Get its next number of {left_num1} = ${left_num1} + 1 = {left_num2}$
        Multiply them = ${left_num1} * {left_num2} = {left_num}$

        In the above result, put ${right_num1} * {right_num2} = {right_num}$
        The answer is ${left_num} \\; {right_num} = {n**2}$

        """

        answer_s = f"""
        ${n ** 2}$
        """

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

        return result


    def get_problem_multipy_units_sum_10(self, *args, **kwargs):
        n1_units = random.randint(1, 8)
        n2_units = 10 - n1_units
        n_tens = 10 * random.randint(1, 12)

        n1 = n_tens + n1_units
        n2 = n_tens + n2_units

        n1_str = str(n1)
        n2_str = str(n2)

        left_num1 = int(n1_str[:-1])
        left_num2 = int(n2_str[:-1])
        left_num_plus_1 = left_num1 + 1
        left_num = left_num1 * left_num_plus_1

        right_num1 = int(n1_str[-1])
        right_num2 = int(n2_str[-1])
        right_num = right_num1 * right_num2

        problem_s = f"""
        Find the product ${n1}$ and ${n2}$.
        """

        soln_s = f"""
        We have to find ${n1} * {n2}$

        Please note that the sum of unit digits is 10:
        {right_num1} + {right_num2} = {right_num1 + right_num2}

        If we remove unit digits in both numbers, they are identical:
        {left_num1} = {left_num2}

        Once, the above two conditions are satisfied, the shortcut is as follows:

        Remove the last digit of either number, we get = ${left_num1}$
        Get its next number of {left_num1} = ${left_num1} + 1 = {left_num_plus_1}$
        Multiply them = ${left_num1} * {left_num_plus_1} = {left_num}$

        Multiply the unit digits, make sure the result has two digits:
        ${right_num1} * {right_num2} = {right_num:02d}$

        Place them together and you get the result:
        ${left_num} \\; {right_num:02d} = {left_num}{right_num:02d}$
        """

        answer_s = f"""
        ${n1 * n2}$
        """

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

        return result


    def get_problem_multipy_number_close_to_round_off(self, *args, **kwargs):
        n1_left_mul = random.randint(1, 3)
        pow1 = random.randint(1, 3)
        pow2 = random.randint(1, 5)
        units_diff = random.randint(1, 3)

        n1_left_tens = n1_left_mul * 10 ** pow1
        n1_left = n1_left_tens - units_diff

        n1_right = 10 ** pow2
        n1 = n1_left * n1_right
        n2 = random.randint(2, 99)


        n1_left_tens_str = format_integers(n1_left_tens)
        units_diff_str = format_integers(units_diff)
        n1_left_str = format_integers(n1_left)
        n1_str = format_integers(n1)
        n2_str = format_integers(n2)
        n1n2_str = format_integers(n1 * n2)


        prod_left1_str = format_integers(n1_left_tens * n2)
        prod_left2_str = format_integers(units_diff * n2)
        prod_left_str = format_integers(n1_left_tens * n2 - units_diff * n2)


        problem_s = f"""
        Find the value of ${n1_str} * {n2_str}$.
        """

        soln_s = f"""
        Remove right side zeros from ${n1_str}$.
        We remove ${pow2}$ zeros from the right side
        $= {n1_left_str}$
        $= {n1_left_tens_str} - {units_diff_str}$

        Multiply it by ${n2_str}$
        $= ({n1_left_tens_str} - {units_diff_str}) * {n2_str}$
        $= {n1_left_tens_str} * {n2_str} - {units_diff_str} * {n2_str}$
        $= {prod_left1_str} - {prod_left2_str}$
        $= {prod_left_str}$

        Now, put back ${pow2}$ zeros on the right side
        $= {n1n2_str}$
        """

        answer_s = f"""
        ${n1n2_str}$
        """

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

        return result


    def get_problem_multipy_both_numbers_close_to_round_off(self, *args, **kwargs):

        #first number
        n1_left_mul = random.randint(1, 3)
        n1_pow1 = random.randint(1, 3)
        n1_pow2 = random.randint(1, 5)
        n1_units_diff = random.randint(1, 3)

        n1_left_tens = n1_left_mul * 10 ** n1_pow1
        n1_left = n1_left_tens - n1_units_diff

        n1_right = 10 ** n1_pow2
        n1 = n1_left * n1_right

        #second number
        n2_left_mul = random.randint(1, 3)
        n2_pow1 = random.randint(1, 3)
        n2_pow2 = random.randint(1, 5)
        n2_units_diff = random.randint(1, 3)

        n2_left_tens = n2_left_mul * 10 ** n2_pow1
        n2_left = n2_left_tens - n2_units_diff

        n2_right = 10 ** n2_pow2
        n2 = n2_left * n2_right


        n1_str = format_integers(n1)
        n2_str = format_integers(n2)

        problem_s = f"""
        Find the value of ${n1_str} * {n2_str}$.
        """

        soln_items = []

        #strip zeros from n1
        n1_left_str = format_integers(n1_left)
        n1_left_tens_str = format_integers(n1_left_tens)
        n1_units_diff_str = format_integers(n1_units_diff)

        soln_item = f"""
        Remove right side zeros from ${n1_str}$.
        We remove ${n1_pow2}$ zeros from the right side
        $= {n1_left_str}$
        $= {n1_left_tens_str} - {n1_units_diff_str}$
        """

        soln_items.append(soln_item.strip())


        #strip zeros from n2
        n2_left_str = format_integers(n2_left)
        n2_left_tens_str = format_integers(n2_left_tens)
        n2_units_diff_str = format_integers(n2_units_diff)

        n2_left_tens_str = f"\\color{{red}}{{{n2_left_tens_str}}}"
        n2_units_diff_str = f"\\color{{blue}}{{{n2_units_diff_str}}}"

        soln_item = f"""
        Remove right side zeros from ${n2_str}$.
        We remove ${n2_pow2}$ zeros from the right side
        $= {n2_left_str}$
        $= {n2_left_tens_str} - {n2_units_diff_str}$
        """

        soln_items.append(soln_item.strip())

        #cross multiply them
        t1 = n1_left_tens * n2_left_tens
        t2 = n1_units_diff * n2_left_tens
        t3 = n1_left_tens * n2_units_diff
        t4 = n1_units_diff * n2_units_diff
        t1_str = format_integers(t1)
        t2_str = format_integers(t2)
        t3_str = format_integers(t3)
        t4_str = format_integers(t4)

        t1t4_str = format_integers(t1 + t4)
        t2t3_str = format_integers(t2 + t3)
        t1t2t3t4_str = format_integers(t1 - t2 - t3 + t4)


        soln_item = f"""
        Multiply them:

        $= ({n1_left_tens_str} - {n1_units_diff_str})
        * ({n2_left_tens_str} - {n2_units_diff_str})
        $

        $= {n1_left_tens_str} * {n2_left_tens_str}
        - {n1_units_diff_str} * {n2_left_tens_str}
        - {n1_left_tens_str} * {n2_units_diff_str}
        + {n1_units_diff_str} * {n2_units_diff_str}
        $

        $= {t1_str} - {t2_str} - {t3_str} + {t4_str}$

        $= {t1t4_str} - {t2t3_str}$

        $= {t1t2t3t4_str}$
        """

        soln_items.append(soln_item.strip())


        #final product
        n1n2_str = format_integers(n1 * n2)

        soln_item = f"""
        We had removed ${n2_pow1}$ and ${n2_pow2}$ zeros from the right side of the numbers.
        Put all of them, ${n2_pow1 + n2_pow2}$ zeros, back.

        = {n1n2_str}
        """

        soln_items.append(soln_item.strip())

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

        answer_s = f"""
        {n1n2_str}
        """

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

        return result


    def get_problem_multipy_number_with_99(self, *args, **kwargs):

        n1_left_mul = 1
        pow1 = random.randint(1, 5)
        pow2 = random.randint(1, 5)
        units_diff = 1

        n1_left_tens = n1_left_mul * 10 ** pow1
        n1_left = n1_left_tens - units_diff

        n1_right = 10 ** pow2
        n1 = n1_left * n1_right
        n2 = random.randint(2, 99999)


        n1_left_tens_str = format_integers(n1_left_tens)
        units_diff_str = format_integers(units_diff)
        n1_left_str = format_integers(n1_left)
        n1_str = format_integers(n1)
        n2_str = format_integers(n2)
        n1n2_str = format_integers(n1 * n2)


        prod_left1_str = format_integers(n1_left_tens * n2)
        prod_left2_str = format_integers(units_diff * n2)
        prod_left_str = format_integers(n1_left_tens * n2 - units_diff * n2)

        diff_html = self._format_numbers_to_diff_html([n1_left_tens * n2,
                                                       units_diff * n2])

        problem_s = f"""
        Find the value of ${n1_str} * {n2_str}$.
        """


        soln_s = f"""
        Remove right side zeros from ${n1_str}$.
        We remove ${pow2}$ zeros from the right side
        $= {n1_left_str}$
        $= {n1_left_tens_str} - {units_diff_str}$

        Multiply it by ${n2_str}$
        $= ({n1_left_tens_str} - {units_diff_str}) * {n2_str}$
        $= {n1_left_tens_str} * {n2_str} - {units_diff_str} * {n2_str}$
        $= {prod_left1_str} - {prod_left2_str}$

        {diff_html}

        $= {prod_left_str}$

        Now, put back ${pow2}$ zeros on the right side
        $= {n1n2_str}$
        """

        answer_s = f"""
        ${n1n2_str}$
        """
        result = {'problem_s': problem_s.strip(),
                  'soln_s': soln_s.strip(),
                  'answer_s': answer_s.strip(),
                  }

        return result


    def get_problem_multipy_number_close_to_100(self, *args, **kwargs):

        n1_left_mul = 1
        pow1 = random.randint(1, 5)
        pow2 = random.randint(1, 5)
        units_diff = 1

        n1_left_tens = n1_left_mul * 10 ** pow1
        n1_left = n1_left_tens + units_diff

        n1_right = 10 ** pow2
        n1 = n1_left * n1_right
        n2 = random.randint(2, 99999)


        n1_left_tens_str = format_integers(n1_left_tens)
        units_diff_str = format_integers(units_diff)
        n1_left_str = format_integers(n1_left)
        n1_str = format_integers(n1)
        n2_str = format_integers(n2)
        n1n2_str = format_integers(n1 * n2)


        prod_left1_str = format_integers(n1_left_tens * n2)
        prod_left2_str = format_integers(units_diff * n2)
        prod_left_str = format_integers(n1_left_tens * n2 + units_diff * n2)

        diff_html = self._format_numbers_to_sum_html([n1_left_tens * n2,
                                                       units_diff * n2])

        problem_s = f"""
        Find the value of ${n1_str} * {n2_str}$.
        """


        soln_s = f"""
        Remove right side zeros from ${n1_str}$.
        We remove ${pow2}$ zeros from the right side
        $= {n1_left_str}$
        $= {n1_left_tens_str} + {units_diff_str}$

        Multiply it by ${n2_str}$
        $= ({n1_left_tens_str} + {units_diff_str}) * {n2_str}$
        $= {n1_left_tens_str} * {n2_str} + {units_diff_str} * {n2_str}$
        $= {prod_left1_str} + {prod_left2_str}$

        {diff_html}

        $= {prod_left_str}$

        Now, put back ${pow2}$ zeros on the right side
        $= {n1n2_str}$
        """

        answer_s = f"""
        ${n1n2_str}$
        """
        result = {'problem_s': problem_s.strip(),
                  'soln_s': soln_s.strip(),
                  'answer_s': answer_s.strip(),
                  }

        return result



    def get_problem_cubic_root_of_number(self, *args, **kwargs):


        n_power = 3
        n_root = Rational(1, n_power)
        root_base = random.randint(2, 30)

        a = root_base ** n_power
        a_delta = (root_base + 1) ** n_power - a

        b = random.randint(1, a_delta - 1)
        num = a + b

        actual_root = round(num ** n_root, 2)

        problem_s = f"""
        Find approximate value of the cubic root of ${num}$.
        """

        answer_s = f"""
        ${actual_root}$
        """

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

        soln_stmts = []

        a_symb = Symbol('a')
        b_symb = Symbol('b')
        x_symb = Symbol('x')


        soln_stmt = f"""

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

        (a+b)^{{ {latex(n_root)} }}
        & = {a_symb}^{{ {latex(n_root)} }}
          + {latex(n_root)}  {a_symb}^{{  {latex(n_root)} - 1 }} \\cdot {b_symb}^1 + \\cdots
        \\\\ \\\\

        & = {a_symb}^{{ {latex(n_root)} }}
          + {latex(n_root)}  {a_symb}^{{  {latex(n_root - 1)} }} \\cdot {b_symb} + \\cdots

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


        soln_stmt = f"""

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

        \\text{{Let }}
        & {x_symb} = {a_symb}^{{ {latex(n_root)} }}
        \\\\ \\\\

        \\implies
        & {x_symb}^{{2}} = {a_symb}^{{  {latex(-(n_root - 1))} }}
        \\\\ \\\\

        \\implies
        & \\frac{{ 1 }}{{ {x_symb}^{{2}} }} = {a_symb}^{{  {latex(n_root - 1)} }}
        \\\\ \\\\

        \\implies
        & (a+b)^{{ {latex(n_root)} }}
        \\approx  {x_symb}
        + {latex(n_root)}  \\frac{{ 1 }}{{ {x_symb}^{{2}} }}   \\cdot {b_symb}

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



        soln_stmt = self._get_solution_cubic_root_of_number(num, n_power, n_root, root_base, a, b)
        soln_stmts.append(soln_stmt.strip())

        org_root_base = root_base

        root_base = 5 * (root_base // 5)

        if abs(root_base ** n_power - num) > abs((root_base + 5) ** n_power - num):
            root_base = root_base + 5
            pass

        # if root_base was already same, return
        if org_root_base != root_base:
            soln_stmt = f"""
            <h3>Second Method:</h3>
            """
            soln_stmts.append(soln_stmt.strip())

            a = root_base ** n_power
            b = num - a
            soln_stmt = self._get_solution_cubic_root_of_number(num, n_power, n_root, root_base, a, b,
                                                                is_multiple_of_five = True)
            soln_stmts.append(soln_stmt.strip())
            pass

        soln_s = "\n\n".join(soln_stmts)
        result['soln_s'] = soln_s.strip()
        return result


    def _get_solution_cubic_root_of_number(self, num, n_power, n_root, root_base, a, b,
                                           is_multiple_of_five = False):

        actual_root = round(num ** n_root, 2)

        #root_frac_part2 = Rational(a ** n_root, a ** (-1))
        root_frac_part2 = Rational(1, root_base ** 2)
        root_frac_part2_latex = f"\\frac{{ 1 }}{{ {root_base}^2 }}"


        root_frac_p = n_root.p * root_frac_part2.p * b
        root_frac_q = n_root.q * root_frac_part2.q

        root_frac = Rational(root_frac_p, root_frac_q)

        root_deci = round(float(root_frac), 2)

        num_root = round(root_base + root_deci, 2)

        a_symb = Symbol('a')
        b_symb = Symbol('b')
        x_symb = Symbol('x')

        soln_stmts = []

        if is_multiple_of_five:
            soln_stmt = f"""
            The closest perfect ${n_power}$ power of a number multiple of 5 is ${a} = {root_base}^{{ {n_power} }}$.
            Therefore,
            """
        else:
            soln_stmt = f"""
            The closest perfect ${n_power}$ power of a number is ${a} = {root_base}^{{ {n_power} }}$.
            Therefore,
            """
        soln_stmts.append(soln_stmt.strip())


        soln_stmt = f"""

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

        {num}
        & = {a} + {b}
        \\\\ \\\\

        \\implies
        & {a_symb} = {a} \\\\ \\\\
        & {b_symb} = {b} \\\\ \\\\
        & {x_symb} = {a}^{{ {latex(n_root)} }} = {root_base} \\\\ \\\\
        \\end{{aligned}}
        $

        """
        soln_stmts.append(soln_stmt.strip())

        soln_stmt = f"""

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

        ({num})^{{ {latex(n_root)} }}
        & = ({a} + {b})^{{ {latex(n_root)} }}
        \\\\ \\\\

        & = {x_symb}
        + {latex(n_root)}  \\frac{{ 1 }}{{ {x_symb}^{{2}} }} \\cdot {b_symb}
        \\\\ \\\\

        & = {root_base}
          + {latex(n_root)} \\cdot {root_frac_part2_latex} \\cdot {b}
        \\\\ \\\\

        & = {root_base} + \\frac{{ {root_frac_p} }}{{ {root_frac_q} }}
        \\\\ \\\\

        & = {root_base} + {latex(root_deci)}
        \\\\ \\\\

        & = {num_root}

        \\end{{aligned}}
        $

        Please note that the actual root is ${actual_root}$.

        """
        soln_stmts.append(soln_stmt.strip())

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


    def get_problem_square_of_number(self, *args, **kwargs):

        hundred = 100
        fifty = 50
        num = random.randint(2, 220)

        '''
        if num // 10 == num / 10:
            num_bases = [0]
        else:
        '''

        num_bases = [0, 10, 50, 100, 150, 200]
        num_bases = sorted(num_bases, key = lambda x: abs(num - x))

        num_base = num_bases[0]

        problem_s = f"""
        Find square of {num}.
        """

        answer_s = f"""
        {num**2}
        """

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

        soln_stmts = []


        if (num_base == 0 or num_base == 10):
            soln_stmt = f"""
            $\\displaystyle {num}^2 = {num**2}$
            """
            soln_stmts.append(soln_stmt.strip())

            soln_s = f"\n".join(soln_stmts)
            result['soln_s'] = soln_s.strip()
            return result


        num_delta = num - num_base

        if num_delta >= 0:
            num_delta_symb = Symbol(f"{num_delta}")
            num_delta_signed = Symbol(f" + {num_delta}")
        else:
            num_delta_symb = Symbol(f"({num_delta})")
            num_delta_signed = Symbol(f"{num_delta}")



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

        {num}
        & = {num_base} {num_delta_signed}
        \\\\ \\\\

        \\implies {num}^2
        & = \\left( {num_base} {num_delta_signed} \\right)^2
        \\\\ \\\\

        & = {num_base}^2 + 2 \\cdot {num_base} \\cdot {num_delta_symb} + {num_delta_symb}^2
        \\\\ \\\\

        & = {num_base**2} + {2 * num_base * num_delta} + {num_delta**2}
        \\\\ \\\\

        & = {num_base**2 + 2 * num_base * num_delta + num_delta**2}
        \\\\ \\\\

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



        m_factor = Rational(num_base, hundred)
        m_factor_latex = f"\\frac{{{num_base}}}{{{hundred}}}"

        term1 = toIntOrFloat(float(m_factor)**2 * hundred)
        coeff_term2 = toIntOrFloat(2 * float(m_factor))
        term2 = toIntOrFloat(2 * float(m_factor) * num_delta)

        a_symb = Symbol('a')
        b_symb = Symbol('b')


        soln_stmt = f"""
        <h3>Shortcut Method:</h3>

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

        {num}
        & = {num_base} {num_delta_signed}
        \\\\ \\\\

        \\implies
        \\text {{Multiplying Factor }}
        & = {m_factor_latex} = {latex(m_factor)}
        \\\\ \\\\

        \\implies
        {num}^2
        & = \\left( {latex(m_factor)} \\cdot  {latex(m_factor)} \\cdot {hundred}
        + 2 \\cdot {latex(m_factor)} \\cdot {num_delta}  \\right)
        \\text{{ hundreds }} + {num_delta_symb}^2
        \\\\ \\\\

        & = \\left( {latex_color(term1, 'blue')} + {latex_color(coeff_term2, 'red')}
            \\cdot  {latex(num_delta)} \\right)
        \\text{{ hundreds }} + {num_delta_symb}^2
        \\qquad \\text{{Note: This is shortcut formula for base {num_base}}}
        \\\\ \\\\

        & = {term1 + term2} \\text{{ hundreds }} + {num_delta**2}
        \\\\ \\\\

        & = {num**2}
        \\\\ \\\\

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


        m_factor_symb = Symbol('a')
        m50_factor_symb = Symbol('b')

        m50_factor = Rational(num_base, fifty)
        m50_factor_latex = f"\\frac{{{num_base}}}{{{fifty}}}"

        m_factor_sqr_hun = m_factor ** 2 * hundred

        soln_stmt = f"""
        <h3>Fast Shortcut Method:</h3>

        $\\displaystyle
        {num} = {num_base} {num_delta_signed}
        $

        We will call {num_base} as base number.


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

        {latex(m_factor_symb)}
        & = \\text{{ base number is what multiple of {hundred} }}
        = {m_factor_latex} = {latex(m_factor)}
        \\\\ \\\\

        {latex(m_factor_symb**2)} * {hundred}
        & = {latex(m_factor ** 2)} * {hundred} = {latex(m_factor_sqr_hun)}
        \\\\ \\\\

        {latex(m50_factor_symb)}
        & = \\text{{ base number is what multiple of {fifty} }}
        = {m50_factor_latex} = {latex(m50_factor)}

        \\end{{aligned}}
        $

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

        \\implies
        \\left( {num_base} {num_delta_signed} \\right)^2
        & = \\left( {latex(m_factor_symb ** 2)} \\cdot {hundred}
        + {latex(m50_factor_symb)} \\cdot {num_delta}  \\right)
        \\text{{ hundreds }} + {num_delta_symb}^2
        \\\\ \\\\


        & = \\left( {m_factor_sqr_hun}
        + {latex(m50_factor)} \\cdot {num_delta}  \\right)
        \\text{{ hundreds }} + {num_delta_symb}^2
        \\\\ \\\\


        & = {term1 + term2} \\text{{ hundreds }} + {num_delta**2}
        \\\\ \\\\

        & = {num**2}
        \\\\ \\\\

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


        soln_s = f"\n\n".join(soln_stmts)
        result['soln_s'] = soln_s.strip()
        return result





    def get_problem_square_root_of_number(self, *args, **kwargs):

        n_power = 2
        n_root = Rational(1, n_power)
        root_base = random.randint(2, 30)
        a = root_base ** n_power
        a_delta = (root_base + 1) ** n_power - a
        b = random.randint(1, a_delta - 1)
        num = a + b

        actual_root = round(num ** (1/2), 2)

        problem_s = f"""
        Find approximate value of the square root of ${num}$.
        """


        answer_s = f"""
        ${actual_root}$
        """

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

        soln_stmts = []
        soln_stmt = f"""

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

        (a+b)^{{ {latex(n_root)} }}
        & =a^{{ {latex(n_root)} }} +  {latex(n_root)} \\cdot a^{{- {latex(n_root)} }} b^1
        + \\cdots
        \\\\ \\\\

        \\implies \\sqrt{{a+b}}
        & =\\sqrt{{a}}+\\frac{{b}}{{ {latex(n_power)} \\sqrt{{a}}}}

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


        soln_stmt = self._get_solution_square_root_of_number(num, n_power, n_root, root_base, a, b)
        soln_stmts.append(soln_stmt.strip())


        org_root_base = root_base

        root_base = 5 * (root_base // 5)

        if abs(root_base ** n_power - num) > abs((root_base + 5) ** n_power - num):
            root_base = root_base + 5
            pass

        # if root_base was already same, return
        if org_root_base != root_base:
            soln_stmt = f"""
            <h3>Second Method:</h3>
            """
            soln_stmts.append(soln_stmt.strip())

            a = root_base ** n_power
            b = num - a
            soln_stmt = self._get_solution_square_root_of_number(num, n_power, n_root, root_base, a, b,
                                                                 is_multiple_of_five = True)
            soln_stmts.append(soln_stmt.strip())
            pass


        soln_s = "\n\n".join(soln_stmts)
        result['soln_s'] = soln_s.strip()
        return result


    def _get_solution_square_root_of_number(self, num, n_power, n_root, root_base, a, b,
                                            is_multiple_of_five = False):
        root_frac = round(b / (2 * root_base), 2)
        num_root = root_base + root_frac

        actual_root = round(num ** (1/2), 2)


        soln_stmts = []


        if is_multiple_of_five:
            soln_stmt = f"""
            The closest perfect square number multiple of 5 is ${a} = {root_base}^{{2}}$.
            """
        else:
            soln_stmt = f"""
            The closest perfect square number is ${a} = {root_base}^{{2}}$.
            """
        soln_stmts.append(soln_stmt.strip())


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

        \\implies \\sqrt{{ {a + b} }}
        & =\\sqrt{{ {a} + {b} }}  \\\\ \\\\

        & =\\sqrt{{ {a} }}+\\frac{{ {b} }}{{ {latex(n_power)} \\sqrt{{ {a} }}}} \\\\ \\\\

        & = {root_base} + \\frac{{ {b} }}{{ {latex(n_power)} \\cdot {root_base} }} \\\\ \\\\

        & = {root_base} + \\frac{{ {b} }}{{ {n_power * root_base} }} \\\\ \\\\

        & = {root_base} + {root_frac} \\\\ \\\\

        & = {num_root}

        \\end{{aligned}}
        $

        Please note that the actual root is ${actual_root}$.

        """
        soln_stmts.append(soln_stmt.strip())

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


    def _get_approx_divisor_metric(self, divisor, tolerance = 0.2):

        simp_dict = {
            100: [100, "Round of numerator and denominator to 100"],
            50: [[2, 100], "Round of numerator and denominator to 50"],
            25: [[4, 100], "Round of numerator and denominator to 25"],
            20: [[5, 100], "Round of numerator and denominator to 20"],
            10: [10, "Round of numerator and denominator to 10"],
            }

        for simp_factor, v in simp_dict.items():
            simp_ops, simp_msg = v

            if divisor < simp_factor:
                continue

            temp_remainder = divisor % simp_factor
            min_remainder = min (temp_remainder, simp_factor - temp_remainder)
            if not ( min_remainder < simp_factor * tolerance ):
                sOut = f""" Failed condition simp_factor = {simp_factor}, divisor = {divisor}:
                min_remainder < simp_factor * tolerance
                = {min_remainder < simp_factor * tolerance }
                = {min_remainder} < {simp_factor} * {tolerance }
                = {min_remainder} < {simp_factor * tolerance }
                """
                continue


            divisor_metric = {'simp_factor': simp_factor,
                              'simp_ops': simp_ops,
                              'simp_msg': simp_msg
                              }

            return divisor_metric
            pass

        # return default 10
        simp_factor = 10
        divisor_metric = {'simp_factor': simp_factor,
                          'simp_ops': simp_dict[simp_factor][0],
                          'simp_msg': simp_dict[simp_factor][1]
                          }

        return divisor_metric
        pass

    def _round_number(self, num, roundoff):
        num = float(num)
        num = round(num, roundoff)
        num = eval(str(num))
        num_int = round(num, 0)
        if abs(num - num_int) < num * 10**-1:
            return int(num_int)
        return num
        pass

    def _get_modified_dividend_divisor_exprs(self, dividend, divisor, divisor_metric):

        simp_ops = divisor_metric['simp_ops']
        if not isinstance(simp_ops, list):
            frac_numr = latex_fraction(dividend, simp_ops)
            frac_denr = latex_fraction(divisor, simp_ops)
            mod_expr_latex = latex_fraction(frac_numr, frac_denr)

            mod_dividend = self._round_number(dividend / simp_ops, 1)
            mod_divisor = self._round_number(divisor / simp_ops, 1)


            result = {'mod_expr_latex': mod_expr_latex,
                      'mod_dividend': mod_dividend,
                      'mod_divisor': mod_divisor,
                      }
            return result
            pass

        frac_numr = latex_fraction(dividend, simp_ops[1])
        frac_denr = latex_fraction(divisor, simp_ops[1])
        step1_expr_latex = latex_fraction(f"{frac_numr} \\cdot {simp_ops[0]}",
                                          f"{frac_denr} \\cdot {simp_ops[0]}"
                                          )

        temp_dividend = self._round_number(dividend / simp_ops[1], 1)
        temp_divisor = self._round_number(divisor / simp_ops[1], 1)

        step2_expr_latex = latex_fraction(f"{temp_dividend} \\cdot {simp_ops[0]}",
                                        f"{temp_divisor} \\cdot {simp_ops[0]}"
                                        )

        mod_expr_latex = f"{step1_expr_latex} \\approx  {step2_expr_latex}"

        str_temp_divisor = str(temp_divisor)
        str_temp_divisor_without_decimal = str_temp_divisor.replace(".", "").strip("0")

        if len(str_temp_divisor_without_decimal) == 1:
            mod_dividend = self._round_number(temp_dividend, 1)
            mod_divisor = self._round_number(temp_divisor, 1)
        else:
            mod_dividend = self._round_number(temp_dividend * simp_ops[0], 1)
            mod_divisor = self._round_number(temp_divisor * simp_ops[0], 1)


        result = {'mod_expr_latex': mod_expr_latex,
                  'mod_dividend': mod_dividend,
                  'mod_divisor': mod_divisor,
                  }
        return result
        pass


    def get_approx_values(self, dividend, divisor):

        expr_latex = f"""
        \\frac{{{dividend} }}{{{divisor} }}
        """

        tolerance = 0.2
        divisor_metric = self._get_approx_divisor_metric(divisor, tolerance)
        modified_result = self._get_modified_dividend_divisor_exprs(dividend, divisor, divisor_metric)


        mod_dividend = modified_result['mod_dividend']
        mod_divisor = modified_result['mod_divisor']
        mod_expr_latex = modified_result['mod_expr_latex']

        #correct if divisor is a decimal number

        if mod_divisor != int(mod_divisor):
            str_temp_divisor = str(mod_divisor)
            if not "." in str_temp_divisor:
                mod_divisor = f"{mod_divisor}."

            step3_expr_latex = latex_fraction(f"{mod_dividend}",
                                            f"{mod_divisor}"
                                            )
            mod_expr_latex = f"{mod_expr_latex} \\approx  {step3_expr_latex}"

            decimal_digits = len(str_temp_divisor.split(".")[-1])
            mod_dividend = int(mod_dividend * 10**decimal_digits)
            mod_divisor = int(mod_divisor * 10**decimal_digits)

            pass



        simp_expr_latex = f"""
        \\frac{{{mod_dividend} }}{{{mod_divisor} }}
        """

        result = {'expr_latex': expr_latex,
                  'mod_expr_latex': mod_expr_latex,
                  'mod_dividend': mod_dividend,
                  'mod_divisor': mod_divisor,
                  'simp_expr_latex': simp_expr_latex,
                  'simp_msg': divisor_metric['simp_msg'],
                  }
        return result
        pass


    def get_problem_divide_or_multiply(self, *args, **kwargs):

        divisor = random.randint(11, 1000)
        remainder = random.randint(1, divisor - 1)
        quotient = random.randint(11, 99)
        dividend = divisor * quotient + remainder

        expr_latex = f"\\frac{{{dividend}}}{{{divisor}}}"

        problem_s = f"""
        Find approximate value of the $\\displaystyle {expr_latex}$.
        """

        ans_expr = toIntOrFloat(dividend / divisor)

        answer_s = f"""
        ${ans_expr}$
        """

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


        mod_divisor = divisor
        mod_dividend = dividend

        soln_stmts = []

        soln_stmt = f"""
        $\\displaystyle
        {expr_latex}
        $
        """
        soln_stmts.append(soln_stmt.strip())


        while True:
            if mod_divisor < 20:
                break

            step_result = self.get_approx_values(mod_dividend, mod_divisor)

            mod_dividend = step_result['mod_dividend']
            mod_divisor = step_result['mod_divisor']

            soln_stmt = f"""
            {step_result['simp_msg']}.

            $\\displaystyle
            {step_result['expr_latex']}

            \\approx {step_result['mod_expr_latex']}

            \\approx {step_result['simp_expr_latex']}
            $

            """
            soln_stmts.append(soln_stmt.strip())

            pass # while

        mod_ans_expr = toIntOrFloat(mod_dividend / mod_divisor, 1)

        soln_stmt = f"""

        $ = {mod_ans_expr} $

        Actual result is  ${ans_expr}$
        """
        soln_stmts.append(soln_stmt.strip())



        soln_s = "\n\n".join(soln_stmts)
        result['soln_s'] = soln_s.strip()
        return result



    def get_problem_multiply_any_two_numbrs(self, *args, **kwargs):

        min_num, max_num = random.choice([
            [1, 99],
            [10, 120],
            [30, 70],
            [80, 120],
            [100, 999],
            [80, 1020],
            ])

        num1 = random.randint(min_num, max_num)
        num1_hundreds = 100 * (num1 // 100)
        num1_tens = 10 * (num1 // 10)
        num1_tens_rem = num1 - num1_tens
        num1_hundreds_rem = num1 - num1_hundreds

        num2_list = [num1,
                     num1_tens + 10 - num1_tens_rem,
                     num1_hundreds + 100 - num1_hundreds_rem,
                     random.randint(min_num, max_num)
                     ]
        num2 = random.choice(num2_list)

        nums = [num1, num2]

        hundreds = Symbol('H')
        tens = Symbol('T')

        num_bases_dict = { 0: 0,
                      10: tens,
                      #25: Rational(1, 4) * hundreds,
                      50: Rational(1, 2) * hundreds,
                      100: hundreds
                    }

        num_bases = []
        for num in nums:
            num_deltas = sorted(list(num_bases_dict.keys()), key = lambda x: abs(num - x))
            num_bases.append(num_deltas[0])
            pass

        if 10 in num_bases and 50 in num_bases:
            num_bases = [10, 10]

        nums_dicts = []
        for num_idx, num in enumerate(nums):
            nums_dict = {}

            num_base = num_bases[num_idx]

            symb_base = num_bases_dict.get(num_base)

            nums_dict['num'] = num
            nums_dict['base'] = num_base
            nums_dict['symb'] = symb_base

            quotient0 = num // num_base
            quotient1 = quotient0 + 1

            if abs(num - num_base * quotient0) <= abs(num - num_base * quotient1):
                quotient = quotient0
            else:
                quotient = quotient1
                pass

            temp_part = quotient * num_base
            symb_part = quotient * symb_base

            split_num = [temp_part, num - temp_part]
            symb_split_num = [symb_part, num - temp_part]

            nums_dict['split'] = split_num
            nums_dict['symb_split'] = symb_split_num
            nums_dict['split_latex'] = f"{latex(split_num[0])} + {latex(split_num[1])}"
            nums_dict['sum_split_latex'] = f"{latex(symb_split_num[0])} + {latex(symb_split_num[1])}"

            nums_dicts.append(nums_dict)
            pass



        problem_s = f"""
        Find the value of  $\\displaystyle {nums[0]} \\times {nums[1]} $.
        """

        ans_expr = Mul(*nums)

        answer_s = f"""
        ${ans_expr}$
        """

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



        soln_stmts = []

        soln_stmt = f"""
        $\\displaystyle
        {latex(nums[0])} \\times {latex(nums[1])}
        $

        $\\displaystyle
        {latex(nums[0])} = {nums_dicts[0]['sum_split_latex']}
        $

        $\\displaystyle
        {latex(nums[1])} = {nums_dicts[1]['sum_split_latex']}
        $

        """
        soln_stmts.append(soln_stmt.strip())


        html_prod_table, nums_sum_latex = self._get_html_prod_table(nums_dicts[0]['symb_split'],
                                                                  nums_dicts[1]['symb_split'],
                                                                  hundreds, tens)

        soln_stmt = f"""
        {html_prod_table}

        $\\displaystyle
        {nums_sum_latex}
        $
        """
        soln_stmts.append(soln_stmt.strip())


        soln_s = "\n\n".join(soln_stmts)
        result['soln_s'] = soln_s.strip()
        return result

        pass


    def _get_html_prod_table(self, num1_parts, num2_parts, hundreds, tens):

        num_sets = [num1_parts, num2_parts]

        import numpy as np
        org_shape = np.array(num_sets).shape
        mod_num_sets = np.ones((org_shape[0] + 1, org_shape[1] + 1))
        mod_shape = np.array(mod_num_sets).shape

        td_style = f""" style="padding-right:20px;" """
        td_red_style = f""" style="padding-right:20px; color: red;" """

        html_table = []
        nums_sum = []
        for row in range(mod_shape[0]):
            if row != 0:
                if num_sets[1][row - 1] == 0:
                    continue
                pass

            html_row = []
            for col in range(mod_shape[1]):
                if col != 0:
                    if num_sets[0][col - 1] == 0:
                        continue

                if row == 0:
                    if col == 0:
                        td_data = f"<td {td_red_style} ></td>"
                    else:
                        td_data = f"<td {td_red_style} >$\\displaystyle {latex(num_sets[0][col - 1])}$</td>"
                    html_row.append(td_data)
                    continue

                if col == 0:
                    td_data = f"<td {td_red_style} >$\\displaystyle {latex(num_sets[1][row - 1])}$</td>"
                    html_row.append(td_data)
                    continue

                td_val_str = f"""{num_sets[0][col - 1]} $\\times$ {num_sets[1][row - 1]}"""
                td_val = num_sets[0][col - 1] * num_sets[1][row - 1]
                td_val = sympify(td_val)
                td_val = td_val.subs(hundreds**2, 100*hundreds)
                td_val = td_val.subs(hundreds*tens, 10*hundreds)
                td_val = td_val.subs(tens**2, hundreds)
                td_val = td_val.subs(hundreds/2, 50)
                td_val = td_val.subs(tens, 10)

                #td_data = f"<td>{td_val_str} = {td_val}</td>"

                td_data = f"<td {td_style} >$\\displaystyle {latex(td_val)}$</td>"

                html_row.append(td_data)
                nums_sum.append(td_val)
                pass
            html_row = "".join(html_row)
            html_row = f"<tr>{html_row}</tr>"
            html_table.append(html_row)
            pass
        html_table = "".join(html_table)
        html_table = f"<table>{html_table}</table>"


        nums_sum_latex = [f"{latex(x)}" for x in nums_sum]
        nums_sum_latex = " + ".join(nums_sum_latex)

        sum_expr1 = sum(nums_sum)
        sum_expr2 = sum_expr1.subs(hundreds, 100)

        nums_sum_latex = f"""
        \\begin{{aligned}}
        \\text {{Product }}
        & = {nums_sum_latex}
        \\\\ \\\\

        & = {latex(sum_expr1)}
        \\\\ \\\\

        & = {latex(sum_expr2)}

        \\end{{aligned}}
        """


        #max_len = len(f"{sum(nums_sum)}")


        #nums_sum_str = [ ' ' * max_len + f"{x}" for x in nums_sum]
        #nums_sum_str = [ x[-max_len:].replace(" ", "&nbsp;") for x in nums_sum_str]
        #nums_sum_str.append('-' * max_len)


        #nums_sum_str.append(f"{sum(nums_sum)}")

        #nums_sum_str = "<br/>".join(nums_sum_str)
        #nums_sum_str = f"""<div style="font-family: monospace;">{nums_sum_str}</div>"""

        return html_table, nums_sum_latex
        pass



    pass
#class FastCalculationHelper:
