'''
Created on 01-Apr-2021
@author: bthakur

TODO:
DISCRETE AND CONTINUOS VARIABLES

'''
import random
import inspect
import traceback


from sympy.core.expr import UnevaluatedExpr

from sympy import ( Symbol, latex, solve, simplify, factorial,
                    pi, Rational,
                    exp, sin, cos, tan,
                    asin, acos, atan,
                    sinh, cosh, tanh,
                    log, exp, sqrt,
                    Eq)

from numpy import linspace
import numpy as np

from xv.util import number_to_nth, list_to_str
from xv.util import run_with_timeout

from ..helper import BaseHelper

from xv.km.util import latex_fraction, latex_exponent, wrap_bracket


from xv.km.managers import BaseManager
from xv.km.kelements import KProblemTemplate

# _probability_overview_manager
        
class ProbabilityOverviewManager(BaseManager):
    '''
    ProbabilityOverviewManager
    Example:
        ke = ProbabilityOverviewManager()
        ke.getRandomProblem()
        ke.getRandomProblem(problem_type = 0)
        ...
        
        ke.printProblem()
        ke.printAnswer()
        ke.printSolution()
        
        ke.printProblemTypes()
        
    '''    
    _OPR_TYPE_COUNT = 10
    
    def __new__(cls, *args, **kwargs):
        instance = super(ProbabilityOverviewManager, cls).__new__(cls, *args, **kwargs)
        for base_class in ProbabilityOverviewManager.__bases__:
            base_class.__init__(instance, *args, **kwargs)
        return instance
    
    def __init__(self, *args, **kwargs):
        self.base_helper = BaseHelper(**kwargs)
        pass
    

    def _init_problem_types(self, *args, **kwargs):
        self._append_problem_types([
            '_problem_relative_chance',
            '_problem_count_chance',
            '_problem_single_dice',
            '_problem_factorial_expression',
            '_problem_factorial_expression_advanced',
            '_problem_simple_arrangement',
            '_problem_permutation_term',
            '_problem_permutation_expr',
            '_problem_selection_of_items',
            '_problem_combination_expr',
            '_problem_with_select_one',
            '_problem_with_select_multiple',
            '_problem_with_select_multiple_items',
            '_problem_form_words',
            '_problem_form_words_repeated_letters',
            '_problem_form_shorter_words_repeated_letters',
            '_problem_combination_as_pegion_hole',
            #'_problem_a_coin_tossing_surprise',
            #'_problem_a_fair_game_of_chance',
            #'_problem_a_problem_of_three_liars',
            #'_problem_a_question_about_the_median',
            #'_problem_a_search_for_heads_and_its_consequences'
            #'_problem_four_color_theorem',
            #'_problem_right_angle_similar_triangles',
            #'_problem_triangle_with_angles_more_than_2_pi',
            ])
        pass


    def _problem_relative_chance(self, *args, **kwargs):
        prob_helper = self.base_helper.get_probability_helper().get_probability_intro_helper()
        item_names, item_counts = prob_helper.get_random_chance_data()
        s_names_counts = prob_helper.get_names_counts_str(item_names, item_counts)
        
        if random.choice([True, False]):
            answer_qty = max(item_counts)
            s_op = "most"
        else:
            s_op = "least"
            answer_qty = min(item_counts)
            
            
        items_dict = dict(zip(item_names, item_counts))
        
        answer_names = [k for k, v in items_dict.items() if v == answer_qty]
        answer_names_str = list_to_str(answer_names)
        
        if len(answer_names) > 1:
            is_are = "are"
        else:
            is_are = "is"
            
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        
        statement = f"""
        There following items are kept in boxes:
        {s_names_counts}
        If you pick one box at random, what is the {s_op} likely item you are going to get?
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        As {answer_names_str} {is_are} the {s_op} in number ({answer_qty}), you are {s_op} likely going to get it.
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        {answer_names_str}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate

    def _problem_count_chance(self, *args, **kwargs):
        prob_helper = self.base_helper.get_probability_helper().get_probability_intro_helper()
        result = prob_helper.get_random_chance_count()

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        There following items are kept in boxes:
        {result['s_names_counts']}
        If you pick one box at random, what is probability that you get {result['answer_name']}?
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        Probability of selecting one {result['answer_name']}
        
        $\\displaystyle = {result['prob_latex1']}$
        
        $\\displaystyle = {result['prob_latex2']}$
        
        $\\displaystyle = {latex(result['prob'])}$
        
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        $\\displaystyle  {latex(result['prob'])} $
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate


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

        prob_helper = self.base_helper.get_probability_helper().get_probability_intro_helper()
        result = prob_helper.get_single_dice_probability()
        
        population = result['population']
        desired_outcome = result['desired_outcome']
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        You throw a dice. What is probability that you get {desired_outcome[0]}?
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = result['soln_s']
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = result['answer_s']
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate


    def _problem_factorial_expression(self, *args, **kwargs):
        """
        Minimum Grade: 6
        Maximum Grade: 12

        doc_style: xv_doc
        """
        binomial_helper = self.base_helper.get_algebra_helper().get_binomial_helper()
        n = random.randint(5, 100)
        result = binomial_helper.get_factorial_statements(n)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        problemTemplate.add_problem(result['prob_s'], 'text/html')
        problemTemplate.add_solution(result['soln_s'], 'text/html')
        problemTemplate.add_answer(result['answer_s'], 'text/html')
        
        return problemTemplate
        pass

    def _problem_factorial_expression_advanced(self, *args, **kwargs):
        """
        Minimum Grade: 6
        Maximum Grade: 12

        doc_style: xv_doc
        """
        binomial_helper = self.base_helper.get_algebra_helper().get_binomial_helper()
        n = random.choice([binomial_helper.n,
                           binomial_helper.n ** random.randint(1, 5),
                           binomial_helper.n + random.randint(1, 5),
                           binomial_helper.n - random.randint(1, 5)
                           ])
        result = binomial_helper.get_factorial_statements(n)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        problemTemplate.add_problem(result['prob_s'], 'text/html')
        problemTemplate.add_solution(result['soln_s'], 'text/html')
        problemTemplate.add_answer(result['answer_s'], 'text/html')
        
        return problemTemplate
        pass


    def _problem_permutation_term(self, *args, **kwargs):
        """
        Minimum Grade: 6
        Maximum Grade: 12

        doc_style: xv_doc
        """
        binomial_helper = self.base_helper.get_algebra_helper().get_binomial_helper()
        n = binomial_helper.n
        k = binomial_helper.k
            
        nk_sets = [(n, k)]   
        nk_sets = [(k * random.randint(2, 5), k)]   
    
        n1 = random.randint(6, 10)
        n2 = random.randint(2, n1)
        nk_sets.append((n1, n2))
        
        n, k = random.choice(nk_sets)
            
        nPk = binomial_helper.get_permutation_latex(n, k)
        exprP = binomial_helper.get_permutation_expr(n , k)
        
        permutation_formula = binomial_helper.get_permutation_formula_latex(n, k)
        permutation_formula = list_to_str(permutation_formula, sep = "\n\n")

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        

        problem_s = f"""
        Write expression for arranging ${latex(k)}$ items from a collection of ${latex(n)}$ items
        
        $\\displaystyle {nPk}$
        
        Note: $\\displaystyle {nPk}
        $ is read as ${latex(n)}$ permutation ${latex(k)}$.
        """
        problemTemplate.add_problem(problem_s, 'text/html')

        soln_s = f"""
        {permutation_formula}
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        $\\displaystyle 
        {latex(exprP)}
        $
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        
        return problemTemplate
        pass


    def _problem_selection_of_items(self, *args, **kwargs):
        """
        Minimum Grade: 6
        Maximum Grade: 12

        doc_style: xv_doc
        """
        binomial_helper = self.base_helper.get_algebra_helper().get_binomial_helper()
        n = binomial_helper.n
        k = binomial_helper.k
        nCk = binomial_helper.get_combination_latex(n , k)
        exprC = binomial_helper.get_combination_expr(n , k)
        
        combination_formula = binomial_helper.get_combination_formula_latex(n, k)
        combination_formula = list_to_str(combination_formula, sep = "\n\n")

        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        

        problem_s = f"""
        Write expression for selecting ${latex(k)}$ items from a collection of ${latex(n)}$ items
        
        $\\displaystyle {nCk}$
        
        Note: $\\displaystyle {nCk}
        $ is read as ${latex(n)}$ combination ${latex(k)}$.
        """
        problemTemplate.add_problem(problem_s, 'text/html')

        soln_s = f"""
        {combination_formula}
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        $\\displaystyle 
        {latex(exprC)}
        $
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        
        return problemTemplate
        pass
    

    def _problem_combination_expr(self, *args, **kwargs):
        binomial_helper = self.base_helper.get_algebra_helper().get_binomial_helper()
        
        n = random.randint(0, 10)
        r = random.randint(0, 10) 
        
        result = binomial_helper.get_combination_statements(n, r)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        Write expression for ways of selecting {r} from {n} items.
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = result['soln_s']
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        $\\displaystyle {latex(result['nCr'])}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate


    def _problem_with_select_one(self, *args, **kwargs):
        prob_helper = self.base_helper.get_probability_helper().get_probability_intro_helper()
        
        item_names, item_counts = prob_helper.get_random_chance_data()
        s_names_counts = prob_helper.get_names_counts_str(item_names, item_counts)
        
        item_name = random.choice(item_names)
        
        desired_selections = {item_name: 1}
        result = prob_helper.get_prob_statement(item_names, 
                                                item_counts, 
                                                desired_selections)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        There following items are kept in boxes:
        {s_names_counts}
        
        What is probability that a random selection turns out to be {item_name}?
        
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        count of {item_name} = {result['item_count']}
        total count = {result['total_count']}
        
        probability that a random selection turns out to be {item_name}
        
        $\\displaystyle = {result['prob_statement_latex']}$
        
        $\\displaystyle = {result['prob_latex']}$
        
        $\\displaystyle = {result['prob_factorial_latex']}$
        
        $\\displaystyle = {latex(result['prob'])}$
        
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        $\\displaystyle {latex(result['prob'])}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate

    def _problem_with_select_multiple(self, *args, **kwargs):
        prob_helper = self.base_helper.get_probability_helper().get_probability_intro_helper()
        
        item_names, item_counts = prob_helper.get_random_chance_data()
        s_names_counts = prob_helper.get_names_counts_str(item_names, item_counts)
        
        item_name = random.choice(item_names)
        item_count = item_counts[item_names.index(item_name)]
        count = random.randint(1, item_count)
        
        desired_selections = {item_name: count}
        result = prob_helper.get_prob_statement(item_names, 
                                                item_counts, 
                                                desired_selections)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        There following items are kept in boxes:
        {s_names_counts}
        
        What is probability that first {count} random selection turns out to be {item_name}?
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        count of {item_name} = {result['item_count']}
        total count = {result['total_count']}
        
        probability that first {count} random selection turns out to be {item_name}
        
        $\\displaystyle = {result['prob_statement_latex']}$
        
        $\\displaystyle = {result['prob_latex']}$
        
        $\\displaystyle = {result['prob_factorial_latex']}$
        
        $\\displaystyle = {latex(result['prob'])}$
        
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        $\\displaystyle {latex(result['prob'])}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate

    def _problem_with_select_multiple_items(self, *args, **kwargs):
        prob_helper = self.base_helper.get_probability_helper().get_probability_intro_helper()
        
        item_names, item_counts = prob_helper.get_random_chance_data()
        s_names_counts = prob_helper.get_names_counts_str(item_names, item_counts)
        
        pop_dict = dict(zip(item_names, item_counts))
        desired_selections = {k: random.randint(0, v)
                              for k, v in pop_dict.items()}
        desired_selections = {k:v 
                              for k, v in desired_selections.items()
                              if v > 0}
        
        result = prob_helper.get_prob_statement(item_names, 
                                                item_counts, 
                                                desired_selections)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        There following items are kept in boxes:
        {s_names_counts}
        
        What is probability of selecting {result['desired_selections_str']}?
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        total count = {result['total_count']}
        
        probability of selecting {result['desired_selections_str']}
        
        $\\displaystyle = {result['prob_statement_latex']}$
        
        $\\displaystyle = {result['prob_latex']}$
        
        $\\displaystyle = {result['prob_factorial_latex']}$
        
        $\\displaystyle = {result['prob_unevaluated']}$
        
        $\\displaystyle = {latex(result['prob'])}$
        
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        $\\displaystyle {latex(result['prob'])}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate


    def _problem_simple_arrangement(self, *args, **kwargs):
        binomial_helper = self.base_helper.get_algebra_helper().get_binomial_helper()
        
        result = binomial_helper.get_simple_arrangement_statements()
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        problemTemplate.add_problem(result['prob_s'].strip(), "text/html")
        problemTemplate.add_solution(result['soln_s'].strip(), 'text/html')
        problemTemplate.add_answer(result['answer_s'].strip(), 'text/html')
        
        return problemTemplate


    def _problem_permutation_expr(self, *args, **kwargs):
        binomial_helper = self.base_helper.get_algebra_helper().get_binomial_helper()
        
        n = random.randint(0, 10)
        r = random.randint(0, 10) 
        
        result = binomial_helper.get_permutation_statements(n, r)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        Write expression for ways of arranging {r} from {n} items.
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = result['soln_s']
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        $\\displaystyle {latex(result['nPr'])}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate


    def _problem_form_words(self, *args, **kwargs):
        binomial_helper = self.base_helper.get_algebra_helper().get_binomial_helper()
        
        letters = list("abcdefghijklmnopqrstuvwxyz")
        
        r = random.randint(1, 5) 
        n = random.randint(0, 5) + r
        letters = random.sample(letters, n)
        
        result = binomial_helper.get_permutation_statements(n, r)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        Form {r}-letter words from letters {list_to_str(letters)}. The words need not be meaningful.
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        Count of all letters = {len(letters)}
        Therefore, {r}-letter words from letters {list_to_str(letters)}
        = {result['soln_s'].strip()}
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        #soln_s = result['soln_s']
        #problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        $\\displaystyle {latex(result['nPr'])}$
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate
    
    
    def _problem_form_words_repeated_letters(self, *args, **kwargs):
        prob_helper = self.base_helper.get_probability_helper().get_probability_intro_helper()
        binomial_helper = self.base_helper.get_algebra_helper().get_binomial_helper()
        
        
        letters = prob_helper.get_random_letter_set()
        n = len(letters)
        
        result = prob_helper.form_words_from_letters(letters)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        
        statement = f"""
        Form {n}-letter words from letters {{{list_to_str(letters)}}}. The words need not be meaningful.
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        {result['soln_s'].strip()}
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        #soln_s = result['soln_s']
        #problemTemplate.add_solution(soln_s.strip(), 'text/html')

        answer_s = f"""
        $\\displaystyle {latex(result['nPr'])}$
        
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate


    def _problem_form_shorter_words_repeated_letters(self, *args, **kwargs):
        prob_helper = self.base_helper.get_probability_helper().get_probability_intro_helper()
        binomial_helper = self.base_helper.get_algebra_helper().get_binomial_helper()
        
        
        letters = prob_helper.get_random_letter_set()
        n = len(letters)
        r = random.randint(n-2, n)
        
        result = prob_helper.form_words_from_letters(letters, r = r)
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')

        if result is None:
            statement = f"""
            Not implemented: {r}-letter words from {n} letters: {{{list_to_str(letters)}}}.
            """
            problemTemplate.add_problem(statement.strip(), "text/html")
            return problemTemplate
        
        
        statement = f"""
        Form {r}-letter words from {n} letters: {{{list_to_str(letters)}}}. The words need not be meaningful.
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        {result['soln_s'].strip()}
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')
        
        answer_s = f"""
        {result['answer_s']}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate



    def _problem_combination_as_pegion_hole(self, *args, **kwargs):
        count = random.randint(2, 5)
        sets = []
        
        while len(sets) < count:
            n1 = random.randint(1, 90)
            n2 = n1 + random.randint(2, 5)
            sets.append(list(range(n1, n2 + 1)))
            pass
        
        sets_strs = [list_to_str(x) for x in sets]
        sets_strs = "\n".join(sets_strs)
        
        min_set = [min(x) for x in sets]
        max_set = [max(x) for x in sets]
        
        min_set_str = list_to_str(min_set, sep = " + ")
        max_set_str = list_to_str(max_set, sep = " + ")

        distict_sums_str = list_to_str(list(range(sum(min_set), sum(max_set) + 1)))
        
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        If we select one number from each of the following sets, how many distinct sums are possible?
        {sets_strs}
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        Please note that sets are continuous. 
        So, we can calculate the range of all possible distinct sums by finding minimum and maximum sums possible.
        
        Minimum sum 
        = Sum of minimum numbers of each set
        = {min_set_str} 
        = {sum(min_set)}
        
        Maximum sum 
        = Sum of maximum numbers of each set
        = {max_set_str} 
        = {sum(max_set)}
        
        As the sets are continuous, all values within this range are possible.
        {distict_sums_str}
        
        Hence, number of possible distinct sums
        = (Maximum sum) - (Minimum sum) + 1
        =  {sum(max_set)} - {sum(min_set)} + 1
        =  {sum(max_set) - sum(min_set) + 1}
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        {sum(max_set) - sum(min_set) + 1}
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate


    def _problem_a_coin_tossing_surprise(self, *args, **kwargs):
                
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        


        A fair coin is tossed repeatedly.

        1. What is the expected number of tosses before HT shows up for the first time.
        2. What is the expected number of tosses before TT shows up for the first time.
        3. What is the probability that HT shows up before TT?
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        Since all four possible outcomes of two coin tosses are equiprobable, 
        a couple of facts may seem surprising and, as such, have been discussed 
        in a number of publications, e.g., by Martin Gardner in his The Colossal
        Book of Mathematics ( W. W. Norton & Company, 2005, p. 303).

        First, as the transition diagram (Markov chain) below shows,
        except for an appearance at the first two tosses, TT may only show up after HT.
        <img src = "https://www.cut-the-knot.org/Probability/CoinTossingSurprise1G.jpg"></img>

        So perhaps the diagram makes it less surprising that the expected number of 
        tosses to the first appearance of HT is less than that for TT. Below this
        phenomenon will acquire a more accurate estimate.

        <h3>Part 1</h3>
        In the diagram below we only focus on achieving HT:
        <img src = "https://www.cut-the-knot.org/Probability/CoinTossingSurprise1HT.jpg"></img>

        Assign a variable to each of the three nodes, say, x,y,z
        that express the expected number of tosses in reaching our 
        present destination, namely, HT. Naturally, z=0 and was 
        only added for the sake of uniformity. The three are combined 
        into two equations:

        $\\displaystyle y = 1 + \\frac{{1}}{{2}} y + \\frac{{1}}{{2}} z$
        $\\displaystyle x = 1 + \\frac{{1}}{{2}} x + \\frac{{1}}{{2}} y$
        
        The inclusion of 1 in the equations is due to the fact that they 
        describe the number of coin tosses and each tells us how these 
        expectations are connected after one toss. The first equation 
        yields y=2, so that from the second x=4.

        <h3>Part 2</h3>
        As in part 1, we introduce the variables x,y,z, with z=0, again:
        <img src = "https://www.cut-the-knot.org/Probability/CoinTossingSurprise1TT.jpg"></img>

        $\\displaystyle y=1+\\frac{{1}}{{2}} x+\\frac{{1}}{{2}} z$
        $\\displaystyle x=1+\\frac{{1}}{{2}} x+\\frac{{1}}{{2}} y$
        The second equation reduces to $\\displaystyle x=y+\\frac{{1}}{{2}} y$, so that $\\displaystyle y=\\frac{{2}}{{3}} x$. 
        Then the first equation becomes $\\displaystyle \\frac{{2}}{{3}} x=1+\\frac{{1}}{{2}} x$, from which $x=6$.
        
        <h3>Part 3</h3>
        Each of the four possible outcomes of two coin tosses is 
        equiprobable and comes with the probability of 14. This is
        the only opportunity for TT to come before HT. It follows 
        that HT is three times more likely than TT to show up first.
        In other words, the probability of HT showing first is  34.

        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = soln_s
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate

    def _problem_a_fair_game_of_chance(self, *args, **kwargs):
                
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        Alice and Bob play a fair game repeatedly for one nickel each game.
        If originally Alice has a nickels and Bob has b nickels, what is
        Alice's chances of winning all of Bob's money, assuming the play 
        goes on until one person has lost all her or his money?
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        Let p(n) be Alice's chances of winning the total amount of a + b, provided she has
        n nickels in her possession. Obviously p(0) = 0. If she is left with a non-zero capital,
        Alice may, at every trial, win or lose one nickel, both with the probability of 1/2,
            p(n) = p(n + 1)/2 + p(n - 1)/2, n > 0.
        
        In other words, 2p(n) = p(n + 1) + p(n - 1), or p(n + 1) - p(n) = p(n) - p(n - 1).
        From here, recursively,
            p(n + 1) - p(n)	= p(n) - p(n - 1)
            = p(n - 1) - p(n - 2)
            = p(n - 2) - p(n - 3)
            ...
            = p(2) - p(1)
            = p(1) - p(0)
            = p(1).
        
        It follows that p(n) = n p(1) and, since, p(a + b) = 1, p(1) = 1 / (a + b). It follows that p(a) = a / (a + b).
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        a / (a + b)
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate

    def _problem_a_problem_of_three_liars(self, *args, **kwargs):
                
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        A, B and C tell the truth with the probability of $\\frac{{1}}{{3}}$ and lie with the probability of $\\frac{{2}}{{3}}$.
        A makes a statement and $B$ an observation whether A's statement was true or not. C tells us that B confirmed that A was telling the truth.

        What is the probability that A was actually telling the truth?
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        <h3>Solution 1</h3>
        The tree below represents all possible cases wherein C tells us that B confirmed A's statement.
        <img src = "https://www.cut-the-knot.org/Probability/ThreeLiarsI.jpg"></img>

        Counting the probabilities gives for the total
        $\\displaystyle P =\\frac{{1}}{{3}} \\cdot\\left(\\frac{{1}}{{3}} \\cdot \\frac{{1}}{{3}}+\\frac{{2}}{{3}} \\cdot \\frac{{2}}{{3}}\\right)+\\frac{{2}}{{3}} \\cdot\\left(\\frac{{1}}{{3}} \\cdot \\frac{{2}}{{3}}+\\frac{{2}}{{3}} \\cdot \\frac{{1}}{{3}}\\right)$
        $\\displaystyle =\\frac{{13}}{{27}}$

        This corresponds to 13 cases out of 27 wherein C tells us that B confirmed A's statement. 
        The first addend in the above corresponds to $1 \\cdot t1 + 2 \\cdot 2 = 5$ cases where in 
        A's statement was indeed true. So, with the information at hand, we may conclude that the 
        probability that A was telling the truth is $\\displaystyle \\frac{{\\frac{{5}}{{27}}}}{{\\frac{{13}}{{27}}}} = \\frac{{5}}{{13}}$.
        
        <h3>Solution 2</h3>
        Let t=1/3 and f=2/3.
        $$
        \\begin{{array}}{{c|c|c||c||c}}
        \\textbf{{A speaks}} & \\textbf{{B speaks}} & \\textbf{{C speaks}} & \\textbf{{C can}} & \\textbf{{Probability}} \\\\
        \\textbf{{truth}} & \\textbf{{truth}} & \\textbf{{truth}} & \\textbf{{make the}} & \\\\
        &  & & \\textbf{{statement}} & \\\\
        \\hline
        0 & 0 & 0 & 0 & f^3 \\\\
        0 & 0 & 1 & 1 & tf^2 \\\\
        0 & 1 & 0 & 1 & tf^2 \\\\
        0 & 1 & 1 & 0 & t^2f \\\\
        1 & 0 & 0 & 1 & tf^2 \\\\
        1 & 0 & 1 & 0 & t^2f \\\\
        1 & 1 & 0 & 0 & t^2f \\\\
        1 & 1 & 1 & 1 & t^3
        \\end{{array}}
        $$

        $\\displaystyle P(\\text{{A speaks truth}}|\\text{{C can make the statement}}) $

        $\\displaystyle =\\frac{{P(\\text{{A speaks truth}}\\cap\\text{{C can make the statement}})}}{{P(\\text{{C can make the statement}})}}$
        $\\displaystyle =\\frac{{tf^2+t^3}}{{3tf^2+t^3}}=\\frac{{t^2+f^2}}{{t^2+3f^2}}=\\frac{{1^2+2^2}}{{1^2+3\\cdot 2^2}}=\\frac{{5}}{{13}}$
        
        If the properties in the first four columns of the above truth table are labelled by $P_k$
        where k is the column number, then the above truth table gives $P_4$ as
        $\\displaystyle \\overline{{P_1}} \\cdot \\overline{{P_2}} \\cdot P_3+\\overline{{P_2}} \\cdot \\overline{{P_3}} \\cdot P_1+\\overline{{P_3}} \\cdot \\overline{{P_1}} \\cdot P_2+P_1 \\cdot P_2 \\cdot P_3$

        using logic gates or as 
        $\\displaystyle \\small{{((\\lnot{{P_1}})\\land(\\lnot{{P_2}})\\land P_3)\\lor((\\lnot{{P_2}})\\land(\\lnot{{P_3}})\\land P_1)\\lor((\\lnot{{P_3}})\\land(\\lnot{{P_1}})\\land P_2)\\lor(P_1\\land P_2\\land P_3)}}$
        using propositional logic.
        

        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate

    def _problem_a_question_about_the_median(self, *args, **kwargs):
                
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        Given 100 numbers. One of these was removed and
        it was found that the median of the remaining 99
        was 74 . The number was put back and another
        removed. The median became 68.
        What is the median of the whole set of 100 numbers.
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        <h3>Solution 1</h3>
        Place the numbers in increasing order, say, $a_1\\le a_2\\le\\ldots\\le a_{{100}}$. The median of 
        the set is $\\displaystyle \\frac{{a_{{50}}+a_{{51}}}}{{2}}.$
        If a number, say $a_j$, was removed with $j \le 50$ then $a_{{51}}$ becomes the median; with $j > 50$,
        $a_{{50}}$ becomes a median. It follows that $a_{{50}}$ =68 and $a_{{51}}$ =74.
        The median of all 100 numbers is then $\\displaystyle \\frac{{68+74}}{{2}}=71$.

        <h3>Solution 2</h3>
        Let $\\displaystyle S=\\{{a_1,a_2,\\ldots,a_{{50}},a_{{51}},\\ldots,a_{{100}}\\}}$, arranged 
        by rank. Let M(T) be the median of set T. Note that the removal of any term above the median 
        has the same effect; same as below. It follows that in the two experiments the items have 
        been removed from different halves of the set S.
        $\\displaystyle M(S\\setminus \\{{a_{{\\gt 50}}\\}}=a_{{50}}=68,$
        $\\displaystyle M(S\\setminus \\{{a_{{\\le 50}}\\}}=a_{{51}}=74.$
        The median M(S) is therefore  71.
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate

    def _problem_a_search_for_heads_and_its_consequences(self, *args, **kwargs):
                
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        Jack flips a coin three times, and writes down the 
        outcomes. Next Jack circles one of those that is immediately 
        preceded by heads. What is the probability that the 
        marked flip is a heads, Conditional on
        a. its being the second flip,
        b. its being the third flip,
        c. there being no additional information on the selection.
        """
        problemTemplate.add_problem(statement.strip(), "text/html")
        
        soln_s = f"""
        <h3>Solution 1</h3>
        There are four possible outcomes for the first two flips: TT, TH, HT, HH. Since Jack 
        selects a flip immediately preceded by a heads, the first two sequences could not 
        have led to his selection of the second flip. Two possibilities remain: HT and HH 
        which are equiprobable. However, in the second case, Jack had a choice between selecting 
        the second or the third flips. Thus the probability of selecting heads is split. 
        Hence, the answer is $\\displaystyle\\frac{{1}}{{3}}$.
        
        <h3>Solution 2</h3>
        Since the third flip was selected, the second one is bound to be heads. Thus we 
        have four possibilities: THT, THH, HHT, HHH. All equiprobable, so that the answer 
        in this case is also $\\displaystyle \\frac{{1}}{{2}}$.
        
        <h3>Solution 3</h3>
        We do not know what was Jack's selection but we do know that there a heads came up in 
        the first flip or the second (or both). Thus we have six possible sequences (i.e., 
        excluding TTT and TTH): HTT, HTH, THT, THH, HHT, HHH. Each of these has the probability 
        of $\\displaystyle \\frac{{1}}{{6}}$ of occurring. In the first four Jack has no choice and is bound to selecting a 
        unique one. His election is a heads in just one case, THH, i.e., with the probability of $\\displaystyle \\frac{{1}}{{6}}$. 
        Of the remaining two cases, where Jack has a choice to make, a heads would be selected in 
        three out of four possibilities: $H\\textbf{{H}}T, HH\\textbf{{T}}, H\\textbf{{H}}H, H\\textbf{{H}}H$. Thus, the probability of a heads flip 
        being selected is $\\displaystyle \\frac{{1}}{{2}}\\cdot\\frac{{3}}{{6}}=\\frac{{1}}{{4}}$.
        
        Combining the two cases, a heads would be selected with the probability of $\\displaystyle \\frac{{1}}{{6}}+\\frac{{1}}{{4}}=\\frac{{5}}{{12}}.$
        
        A shorter way is to combine the answers to the first two questions, since it is equally 
        likely to be the first or second flip, so $\\displaystyle \\frac{{1}}{{2}}\\cdot\\frac{{1}}{{2}}+\\frac{{1}}{{2}}\\cdot\\frac{{1}}{{3}}=\\frac{{5}}{{12}}.$

        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html')


        answer_s = f"""
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html')
        
        return problemTemplate


    def _problem_four_color_theorem(self, *args, **kwargs):
                
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        Take picture of a map and color all different countries of a region
        with minimum number of colors. How many colors you need?
        
        """
        problemTemplate.add_problem(statement.strip(), 'text/html',
                                     add_html_line_break = False)

        soln_s = f"""
        It is called Four Colour Theorem. Try to draw a network map of all countries
        you are coloring. 
        <br /><br />
        Draw a network graph of countries with edges for direct contact.
        Color the nodes with different colors. You will find that all you need
        is four different colors.
        <br /><br />
        <h4>Resources:</h4>
        <ol>
            <li>
                <a target='_blank' href='https://en.wikipedia.org/wiki/Four_color_theorem'>
                    Four color theorem - Wikipedia
                </a>
            </li>
        </ol>
        
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html',
                                     add_html_line_break = False)


        answer_s = f"""
        You need four colors.
        """
        problemTemplate.add_answer(answer_s.strip(), 'text/html',
                                     add_html_line_break = False)
        
        return problemTemplate

    def _problem_right_angle_similar_triangles(self, *args, **kwargs):
                
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        If you create two right angled triangles by dividing a right angled triangle,
        all triangles are similar. Use a rectangular piece of paper to show it.
        """
        problemTemplate.add_problem(statement.strip(), "text/html",
                                     add_html_line_break = False)
        
        soln_s = f"""
        Take a rectangular piece of paper and divide it into two 
        identical triangles by cutting it diagonally. 
        <br/>
        Take one of the triangles and from the right angle, draw a normal (or altitude) to 
        the hypotenuse. Cut along this normal line to get two triangles.
        <br/>
        Now, you have three triangles, Arrange them one over another
        to show that they are similar.
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html',
                                     add_html_line_break = False)


        answer_s = soln_s
        problemTemplate.add_answer(answer_s.strip(), 'text/html',
                                     add_html_line_break = False)
        
        return problemTemplate

    def _problem_triangle_with_angles_more_than_2_pi(self, *args, **kwargs):
                
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        How can you show that triangles can have its sum of angles more than pi.
        """
        problemTemplate.add_problem(statement.strip(), "text/html",
                                     add_html_line_break = False)
        
        soln_s = f"""
        Take a ball and draw a triangle from pole (imagine one).
        
        <br /><br />
        <h4>Resources:</h4>
        <ol>
            <li>
                <a target='_blank' href='https://www.youtube.com/watch?v=_xpgOOXYOdE'>
                    Triangle on Sphere
                </a>
            </li>
        </ol>
        
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html',
                                     add_html_line_break = False)


        answer_s = soln_s
        problemTemplate.add_answer(answer_s.strip(), 'text/html',
                                     add_html_line_break = False)
        
        return problemTemplate

    def _problem_TEMPLATE(self, *args, **kwargs):
                
        problemTemplate = KProblemTemplate()
        problemTemplate.problem_type = kwargs.get('problem_type')
        
        statement = f"""
        """
        problemTemplate.add_problem(statement.strip(), "text/html",
                                     add_html_line_break = False)
        
        soln_s = f"""
        """
        problemTemplate.add_solution(soln_s.strip(), 'text/html',
                                     add_html_line_break = False)


        answer_s = f""""""
        problemTemplate.add_answer(answer_s.strip(), 'text/html',
                                     add_html_line_break = False)
        
        return problemTemplate

