'''
Created on 20-Jan-2022

@author: bthakur
'''

import inspect
import traceback
import random
import numpy as np

from sympy import simplify, Rational, latex, nsimplify

from xv.km.util import latex_fraction
from sympy.core.expr import UnevaluatedExpr

from xv.km.ui import SvgHelper
from xv.util.data import CacheUtil
import matplotlib.pyplot as plt

from ...util import MathUtil
from xv.util import toIntOrFloat, number_to_nth

from .._common import DataHelper

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

    def get_ops(self):
        ops = [
            "arithmetic",
            "combination",
            ]
        return ops
        pass
    

    def get_random_problems(self, count = 4, **kwargs):
        '''
        mixed = True
        advanced = True
        '''
        statements = []
        solns = []
        answers = []
        while len(statements) < count:
            row = self.get_random_problem(**kwargs)
            
            soln = f"""
            <h3>{len(statements) + 1}. Solution:</h3>
            {row['soln_statement']}
            """
            statements.append(row['statement'].strip())
            solns.append(soln.strip())
            answers.append(f"{row['answer']}".strip())
            pass
        
        solns = "\n\n".join(solns)
        
        result = {
            'statements': self._wrap_in_ol_list(statements),
            'solns': solns,
            'answers': self._wrap_in_ol_list(answers),
            }
        return result
        pass
    
    def get_random_problem(self, **kwargs):
        return self._get_problem(**kwargs)
        pass
    
    def _get_problem(self, **kwargs):
        op = kwargs.get('op', None)
        
        if op is None:
            op = random.choice(self.get_ops())
        
        method_name = f"get_problem_{op}"
        if not hasattr(self, method_name):
            print(f"Method {method_name} not found.")
            return None
        
        method = getattr(self, method_name)
        return method(**kwargs)
        pass

    def _join_lines(self, text):
        lines = text.split("\n")
        lines = [x.strip() for x in lines if x.strip()]
        text = " ".join(lines)
        return text
    
    def _wrap_in_ol_list(self, rows):
        rows_str = [f"<li>{row}<br/><br/></li>"  
                    for row in rows]
        rows_str = "".join(rows_str)
        rows_str = f"<ol>{rows_str}</ol>"
        return rows_str
        pass

    def get_problem_arithmetic(self, **kwargs):
        methods = [
            self._data_arithmetic_001_a,
            self._data_arithmetic_001_b,
            self._data_arithmetic_002,
            self._data_arithmetic_003,
            self._data_arithmetic_004,
            ]
        method = random.choice(methods[-1:])
        data = method(**kwargs)
        return self._template_arithmetic(data, **kwargs)
        pass

    def _template_arithmetic(self, data, **kwargs):
        return data
        pass
    

    def _data_arithmetic_001_a(self, **kwargs):
        dh = self.data_helper
        person_name = dh.get_person_name()
        drinks_name = dh.get_drinks_name()
        
        soln_statement = f"""
        water = 4 * sugar
        sugar = 2 * lemon_juice
        lemon_juice = 3 cups
        
        Hence:
        sugar = 2 * sugar = 2 * 3 cups = 6 cups
        water = 4 * sugar = 4 * 6 cups = 24 cups
        
        """
        
        answer = "24 cups"
        
        statement = f"""
        {person_name} is making {drinks_name} and 
        the recipe requires 4 times as much water as sugar and 
        twice as much sugar as lemon juice. 
        He uses 3 cups of lemon juice. 
        How many cups of water does he need?
        """
        statement = self._join_lines(statement)
        
        data = {
            'statement': statement,
            'soln_statement': soln_statement.strip(),
            'answer': answer,
            }
        return data
        pass

    def _data_arithmetic_001_b(self, **kwargs):
        dh = self.data_helper
        drinks_name = dh.get_drinks_name()
        
        n1 = 1
        n2 = random.randint(2, 5)
        n3 = random.randint(2, 6)
        qty = Rational(n1, n2) 
        cup_percent = nsimplify(100 * qty / n3)
        cup_percent_latex = latex_fraction(100 * qty, n3)
        
        soln_statement = f"""
        {drinks_name} $\\displaystyle 
        = 100 * {latex(qty)} \\% 
        = {latex(100 * qty)} \\% 
        $
        
        {drinks_name} in each cup $\\displaystyle
        = {cup_percent_latex} \\%
        = {latex(cup_percent)} \\%
        $
        """
        
        answer = f"$\\displaystyle  {latex(cup_percent)} \\% $"
        
        statement = f"""
        $\\displaystyle {latex(qty)}$ of a jug is filled 
        with {drinks_name}. The jug is emptied 
        by pouring an equal amount of juice into each 
        of {n3} cups. What percent of the total capacity 
        of the jug did each cup receive?
        """
        statement = self._join_lines(statement)
        
        data = {
            'statement': statement,
            'soln_statement': soln_statement.strip(),
            'answer': answer,
            }
        return data
        pass
    
    def _data_arithmetic_002(self, **kwargs):
        dh = self.data_helper
        size = random.randint(3, 6)
        nums = list(np.random.randint(
                              low = 10,high = 100, 
                              size = size))
        
        
        nums_str = ", ".join([f"\\${x}" for x in nums])
        nums_sum_str = " + ".join([f"\\${x}" for x in nums])
        
        nums_avg_latex = latex_fraction(nums_sum_str, size)
        nums_avg = toIntOrFloat(np.average(nums), 2)
        
        n1 = random.choice([x for x in nums if x >= nums_avg])
        
        n2 = n1 - nums_avg
        

        soln_statement = f"""
        Average earning $\\displaystyle 
        = {nums_avg_latex} 
        = \\${latex(nums_avg)}
        $
        
        Extra money to be given to others $\\displaystyle 
        = \\${n1} - \\${latex(nums_avg)} = \\${latex(n2)}
        $
        """
        
        answer = f"$\\displaystyle \\${n2}$"
        
        statement = f"""
        {size} friends do yardwork for their neighbors 
        over the weekend, earning 
        $\\displaystyle {nums_str}$, 
        respectively. They decide to split their earnings 
        equally among themselves. In total how much will 
        the friend who earned $\\displaystyle \\${n1}$ give to the others? 
        """
        statement = self._join_lines(statement)
        
        data = {
            'statement': statement,
            'soln_statement': soln_statement.strip(),
            'answer': answer,
            }
        return data
        pass
    
    def _data_arithmetic_003(self, **kwargs):
        dh = self.data_helper
        person_name = dh.get_person_name()
        n1, n2, n3, n4 = list(np.random.randint(
                              low = 1,high = 10, size = 4))

        soln_statement = f"""
        Strawberries
        = {n4} strawberries per plant
        * {n3} strawberry plants per square foot
        * {n1} * {n2} square feet
        
        = {n1 * n2 * n3 * n4} strawberries
        """
        
        answer = f"{n1 * n2 * n3 * n4} strawberries"
        
        statement = f"""
        {person_name} has a rectangular garden that measures 
        {n1} feet by {n2} feet. She plants the entire garden 
        with strawberry plants. {person_name} is able to 
        plant {n3} strawberry plants per square foot, 
        and she harvests an average of {n4} strawberries 
        per plant. How many strawberries can she 
        expect to harvest?
        
        """
        statement = self._join_lines(statement)
        
        data = {
            'statement': statement,
            'soln_statement': soln_statement.strip(),
            'answer': answer,
            }
        return data
        pass
    
    
    def _data_arithmetic_004(self, **kwargs):
        dh = self.data_helper
        person_name = dh.get_person_name()

        den1 = 10 ** random.randint(0, 1)
        den2 = 10 ** random.randint(2, 3)
        
        den1_count = random.randint(1, 10) 
        den2_count = random.randint(1, 10) 
        notes_count = den1_count + den2_count
        
        den1_value = den1_count * den1
        den2_value = den2_count * den2

        min_den1_value = den1 * 1
        max_den2_value = den2 * (notes_count - 1)
        max_diff = abs(min_den1_value - max_den2_value)
        
        tot_value = den1_value + den2_value
        
        
        soln_statement = f"""
        Total count of notes = {notes_count}
        
        If {person_name} has only one note of {den1} denomination:

        Value of {den1} denomination notes $\\displaystyle 
        = 1 * {den1} 
        = {min_den1_value}
        $
        Value of {den2} denomination notes $\\displaystyle 
        = ({notes_count} - 1) * {den2} 
        = {notes_count - 1} * {den2}
        = {max_den2_value}
        $
        
        Maximum possible difference $\\displaystyle 
        = abs({max_den2_value} - {min_den1_value}) 
        = abs({max_den2_value - min_den1_value}) 
        =  {max_diff}
        $
        """
        
        answer = f"""
        {max_diff}
        """
        
        statement = f"""
        {person_name} has {notes_count} notes, 
        they are {den1} and {den2} denomination notes. 
        He has at least one {den1} and at least one {den2}. 
        What is the difference in cents between 
        the greatest possible and least possible amounts
        of money that {person_name} can have?
        """
        statement = self._join_lines(statement)
        
        data = {
            'statement': statement,
            'soln_statement': soln_statement.strip(),
            'answer': answer,
            }
        return data
        pass
    
    
    def get_problem_combination(self, **kwargs):
        methods = [
            self._data_combination_001,
            self._data_combination_002,
            ]
        method = random.choice(methods[-1:])
        data = method(**kwargs)
        return self._template_combination(data, **kwargs)
        pass

    def _template_combination(self, data, **kwargs):
        return data
        pass
    
    def _data_combination_001(self, **kwargs):
        dh = self.data_helper
        person_names = dh.get_person_names(5)
        idxs = list(range(len(person_names)))
        shuffled_names = random.sample(person_names,
                                              len(person_names))
        
        person_names_str = ", ".join(person_names[:-1])
        person_names_str = f"{person_names_str} and {person_names[-1]}"
        
        shuffled_names_str = ", ".join(shuffled_names[:-1])
        shuffled_names_str = f"{shuffled_names_str} and {shuffled_names[-1]}"
        
        not_last_idx = random.choice(idxs[:-1])
        not_first_idx = random.choice([
            x for x in idxs[:-1] if not x in [not_last_idx]
            ])
        
        idx1, idx2 = random.sample([
            x for x in idxs[:-1] if not x in [not_last_idx, 
                                              not_first_idx]
            ], 2)
        
        ahead_idx = min(idx1, idx2)
        behind_idx = max(idx1, idx2)
        
        selected_idxs = [not_last_idx, not_first_idx, 
                         ahead_idx, behind_idx]
        
        other_idx1 = list(set(idxs) - set(selected_idxs))[0]
        other_idx2 = random.choice(selected_idxs)
        
        other_ahead_idx = min(other_idx1, other_idx2)
        other_behind_idx = max(other_idx1, other_idx2)
        other_idx_diff = abs(other_idx1 - other_idx2)
        
        not_last = person_names[not_last_idx]
        not_first = person_names[not_first_idx]
        ahead = person_names[ahead_idx]
        behind = person_names[behind_idx]
        other_ahead = person_names[other_ahead_idx]
        other_behind = person_names[other_behind_idx]
        
        together_idx1 = random.choice(idxs[1:-1])
        if random.choice([True, False]):
            together_idx2 = together_idx1 - 1
        else:
            together_idx2 = together_idx1 + 1
            pass
        
        together1 = person_names[together_idx1]
        together2 = person_names[together_idx2]

        random_idx = random.choice(idxs)
        random_person = person_names[random_idx]
        
        random_idx_pos = min(random_idx, abs(len(idxs) - 1 - random_idx))
        
        random_idx_pos_str = number_to_nth(random_idx_pos + 1)
        

        lines = f"""
        {not_last} is not the last person and {not_first} not the first. 
        {behind} is behind {ahead}.
        {other_ahead} is {other_idx_diff} places ahead of {other_behind}.
        {together1} and {together2} are standing with each other.
        {random_person} is {random_idx_pos_str} from start or end.
        """
        lines = lines.split("\n")
        lines = random.sample(lines, len(lines))
        lines = " ".join(lines)
        
        statement = f"""
        {shuffled_names_str} are standing in a line. 
        {lines}<br/>
        Find the order in which they are standing.
        """
        
        soln_statement = f"""
        Possible positions:
        {not_last} = {idxs[:-1]}
        {not_first} = {idxs[1:]}
        
        Order of persons: {ahead}, {behind}
        {other_ahead} = i
        {other_behind} = i + {other_idx_diff}
        
        {together1} = {together2} $\\pm$ 1 
        
        {random_person} = {random_idx_pos} or {len(idxs) - 1 - random_idx_pos}
        """
        
        answer = f"{person_names_str}"
        
        
        statement = self._join_lines(statement)
        
        data = {
            'statement': statement,
            'soln_statement': soln_statement.strip(),
            'answer': answer,
            }
        return data
        pass

    
    def _data_combination_002(self, **kwargs):
        dh = self.data_helper

        statement = f"""
        Five different letter are to be given to three students. 
        Each student will receive at least one award. 
        In how many different ways can the awards be distributed?
        """
        
        new_cmd = r"""
        $
        \newcommand*{\Perm}[2]{{}^{#1}\!P_{#2}}%
        \newcommand*{\Comb}[2]{{}^{#1}C_{#2}}%
        $
        
        $\Perm{n}{k}=\frac{n!}{(n-k)!}$
        
        """
        
        soln_statement = f"""
        {new_cmd}
        Select 1 student out of 3 x give him 3 awards
        x give one award each to rest two
        
        $\\displaystyle 
        =\\Comb[3]{{5}} 
        $
        
        """
        
        answer = f"""
        """
        
        statement = self._join_lines(statement)
        
        data = {
            'statement': statement.strip(),
            'soln_statement': soln_statement.strip(),
            'answer': answer.strip(),
            }
        return data
        pass

    
    pass
#class MidLevelMathHelper:
