'''
Created on 12-Jul-2021

@author: bthakur
'''
import random
import inspect
import traceback
import re
import functools

from sympy import (symbols, latex, simplify, nsimplify, Eq,
                   pi, log, Rational,
                   sin, cos, tan,
                   csc, sec, cot
                   )
from sympy.abc import theta
import sympy

import numpy as np


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


class GridCell:
    def __init__(self, expr, **kwargs):
        self.verbose = kwargs.get('verbose', False)
        self.expr = expr
        
        self.expr_latex = kwargs.get('expr_latex', latex(self.expr))
        
        self.v = kwargs.get('v',  simplify(self.expr))
        
        self.v_latex = self._compute_v_latex(kwargs.get('v_latex', None)) 
        
        if int(abs(self.v)) != abs(self.v):
            while True:
                if self.v_latex[-1] != '0':
                    break
                self.v_latex = self.v_latex[:-1]
        
        pass

    def _compute_v_latex(self, v_latex):
        if not v_latex is None:
            #print(f"{self.v}: v_latex = {v_latex}")
            return v_latex

        if isinstance(self.v, int):
            return f"{self.v}"
        
        if hasattr(self.v, 'is_rational'):
            if self.v.is_rational: 
                return f"{latex(self.v)}"
        
        try: 
            if 'log' in str(self.v):
                return f"{toIntOrFloat(self.v, 6)}"
            else:
                return f"{latex(nsimplify(self.v))} = {toIntOrFloat(self.v, 6)}"
        except Exception as e:
            sError = f"""{inspect.stack()[0][3]} Error: {e}
            Failed to nsimplify
            v_latex: {v_latex}
            self.v: {self.v}
            """
            print(sError)
            print(traceback.format_exc())

        try: 
            if self.v == 0:
                return "0"
            if abs(self.v) < 10 ** (-3):
                v_latex = f"{self.v:.2e}"
                return v_latex
            
            v_latex = toIntOrFloat(self.v, 6, print_error = True)
            #print(f"self.v = {self.v}, v_latex = {v_latex}")
            return f"{v_latex}"
            
        except Exception as e:
            sError = f"""{inspect.stack()[0][3]} Error: {e}
            Failed round off 
            v_latex: {v_latex}
            self.v: {self.v}
            """
            print(sError)
            print(traceback.format_exc())

        try: 
            return f"{toIntOrFloat(self.v, 6)}"
        except Exception as e:
            sError = f"""{inspect.stack()[0][3]} Error: {e}
            Failed round off 
            v_latex: {v_latex}
            self.v: {self.v}
            """
            print(sError)
            print(traceback.format_exc())


        return f"{self.v}"

    def __hash__(self):
        return hash(self.expr_latex)

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, GridCell):
            return self.expr_latex == other.expr_latex
        return False
         
    def __repr__(self): 
        return self._repr_html_()
    
    def __str__(self):
        s = f"{self.expr_latex} = {self.v}"
        return s

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

    def _repr_latex_(self):
        s = f"${self.latex_expr} = {self.v_latex}$"
        return s
    pass

class Grid:
    def __init__(self, n1, n2, cells, **kwargs):
        self.verbose = kwargs.get('verbose', False)
        self.n1 = n1
        self.n2 = n2
        self.cells = cells
        self.highlighted_cells = []
        
        self.rows = len(self.cells)
        self.cols = max([len(row) for row in self.cells])
        pass
    
    
    def get_cell(self, x, y):
        if x >= self.rows:
            raise Exception(f"The x = {x} is invalid. Max value = {self.rows - 1}")

        if x >= self.rows:
            raise Exception(f"The y = {y} is invalid. Max value = {self.cols - 1}")

        return self.cells[x][y]
        pass

    def set_cell_value(self, x, y, gridCell):
        if x >= self.rows:
            raise Exception(f"The x = {x} is invalid. Max value = {self.rows - 1}")

        if x >= self.rows:
            raise Exception(f"The y = {y} is invalid. Max value = {self.cols - 1}")

        self.cells[x][y] = gridCell
        pass

    def _get_random_cell_indexes(self, count = None):
        row_col_indexes = []
        for i in range(self.rows):
            for j in range(self.cols):
                row_col_indexes.append([i, j])

        if count is None:
            count = max(4, len(row_col_indexes) // 3)
        return random.sample(row_col_indexes, count)
        pass

    def __repr__(self): 
        return self._repr_html_()
    
    def _repr_latex(self): 
        return self._repr_html_()
    
    def __str__(self):
        cls = self.__class__
        s = f"{cls.__name__}"
        s = s + "\n" + f"size = ({self.rows}, {self.cols})"
        s = s + "\n" + f"cells = {self.cells}"
        return s

    def _repr_html_(self):
        s = """<table style="border-collapse: collapse;" >"""
        for i, cellRow in enumerate(self.cells):
            s = s + "\n\t" + "<tr>"
            for j, cell in enumerate(cellRow):
                s = s + "\n\t\t" + f"""<td style="border: 1px solid black;">{cell._repr_latex_()}</td>"""
                pass
            s = s + "\n\t" + "</tr>"
            pass
        s = s + "\n" + "</table>"
        return s.replace("\n", "").replace("\t", "")
    
    def html_grid(self, 
                  expr_latex = True,
                  v_latex = True,
                  missing_values = False,
                  missing_count = None,
                  highlighted_color = 'yellow',
                  highlight_cells = False, 
                  shuffled = False
                  ):
        if not  missing_values:
            missing_cells = []
        else:
            if not missing_count is None:
                missing_cells = self._get_random_cell_indexes(count = missing_count)
            else:
                missing_cells = self._get_random_cell_indexes()
            self.highlighted_cells = missing_cells
            
        grid_cells = self.cells.copy()
        if shuffled:
            random.shuffle(grid_cells)
            map(random.shuffle, grid_cells)
        
        s = """<table style="border-collapse: collapse;" >"""
        for i, cellRow in enumerate(grid_cells):
            s = s + "\n\t" + "<tr>"
            for j, cell in enumerate(cellRow):
                if [i, j] in missing_cells:
                    td_latex = ""
                    s = s + "\n\t\t" + f"""
                    <td style="border: 1px solid black; background:{highlighted_color}">
                    {td_latex}
                    </td>
                    """
                else:
                    td_latex = []
                    if expr_latex:
                        td_latex.append(cell.expr_latex)
                    if v_latex:
                        td_latex.append(cell.v_latex)
                    
                    td_latex = '$' + " = ".join(td_latex) + '$'
                    
                    if highlight_cells and [i, j] in self.highlighted_cells:
                        highlighted = f"background:{highlighted_color}"
                    else:
                        highlighted = ""
                        
                    s = s + "\n\t\t" + f"""
                    <td style="border: 1px solid black; {highlighted}">
                    {td_latex}
                    </td>
                    """
                pass
            s = s + "\n\t" + "</tr>"
            pass
        s = s + "\n" + "</table>"
        return s.replace("\n", "").replace("\t", "")
    pass

class GridCellGenerator:
    GRID_TYPES = ['Arithmetic', 
                  'Fractional',
                  'Exponential'
                  ]
    
    @classmethod
    def GET_GENERATOR(cls, grid_type):
        if grid_type == 'Arithmetic':
            return ArithmeticGridCellGenerator()
        
        if grid_type == 'Fractional':
            return FractionalGridCellGenerator()
        
        if grid_type == 'Exponential':
            return ExponentialGridCellGenerator()
        
        raise Exception(f"Invalid grid_type = {grid_type}")
        
        pass
    
    def __init__(self, **kwargs):
        self.verbose = kwargs.get('verbose', False)
        self.value_type = kwargs.get('value_type', None)
        self.include_log = False
        self.generator_statement = None
        

    def get_var_latex(self, v):
        if len(f"{v}") == 1:
            v_latex = f"{v}"
            return v_latex
        
        v_latex = f"\\left({latex(v)}\\right)"
        if isinstance(v, int):
            if v > 0:
                v_latex = f"{v}"
            pass

        return v_latex  
    
    def _latex_add(self, n1, n2):
        if n1 > 0 and n2 < 0:
            return f"{latex(abs(n1))} - {latex(abs(n2))}"
        
        if n1 < 0 and n2 > 0:
            return f"- {latex(abs(n1))} + {latex(abs(n2))}"
        
        if n1 < 0 and n2 < 0:
            return f"- {latex(abs(n1))} - {latex(abs(n2))}"

        return f"{latex(abs(n1))} + {latex(abs(n2))}"
        pass
    
    def _latex_divide(self, n1, n2):
        if n1 > 0 and n2 < 0:
            return latex_fraction(f"{latex(abs(n1))}", f"- {latex(abs(n2))}")
        
        if n1 < 0 and n2 > 0:
            return latex_fraction(f"- {latex(abs(n1))}", f"{latex(abs(n2))}")
        
        if n1 < 0 and n2 < 0:
            return latex_fraction(f"- {latex(abs(n1))}", f"- {latex(abs(n2))}")

        return latex_fraction(f"{latex(abs(n1))}", f"{latex(abs(n2))}")
        pass
    
    def _latex_multiply(self, n1, n2):
        if n1 > 0 and n2 < 0:
            return f"{latex(abs(n1))} * (- {latex(abs(n2))})"
        
        if n1 < 0 and n2 > 0:
            return f"- {latex(abs(n1))} * {latex(abs(n2))}"
        
        if n1 < 0 and n2 < 0:
            return f"(- {latex(abs(n1))}) * (- {latex(abs(n2))})"

        return f"{latex(abs(n1))} * {latex(abs(n2))}"
        pass
    
    def _latex_power(self, n1, n2):
        if n1 > 0 and n2 < 0:
            return f"{latex(abs(n1))}^{{- {latex(abs(n2))}}}"
        
        if n1 < 0 and n2 > 0:
            return f"(- {latex(abs(n1))})^{{{latex(abs(n2))}}}"
        
        if n1 < 0 and n2 < 0:
            return f"(- {latex(abs(n1))})^{{- {latex(abs(n2))}}}"

        return f"{latex(abs(n1))}^{{{latex(abs(n2))}}}"
        pass
    
    def _latex_log(self, base, num):
        return f"log_{{{base}}}{{{num}}}"
        pass
    pass

class ArithmeticGridCellGenerator(GridCellGenerator):
    def get_generator_statement(self, n1, n2, include_log = False):
        n1_latex = self.get_var_latex(n1)
        n2_latex = self.get_var_latex(n2)

        if include_log:
            s_log = "log "
        else:
            s_log = ""
            
        return f"""
        Use one number from ${n1_latex}$, $-{n1_latex}$, and 
        another number from
        ${n2_latex}$ and $-{n2_latex}$ 
        to create real numbers using 
        addition, subtraction, multiplication, 
        division, {s_log}and exponential operators.
        """.strip().replace("\n", "").replace("\t", "")
        pass
    
    def init_cells(self, n1, n2, include_log = False):
        self.generator_statement = self.get_generator_statement(
                                    n1, n2, include_log = include_log)
        
        self.include_log = include_log
        cells = []
        self._init_cell_values(n1, n2, cells)
        self._init_cell_values(n2, n1, cells)
        
        cells = sorted(cells, key = lambda x: float(x.v))
        return cells
        pass

    def _init_cell_values(self, n1, n2, cells):
        cells.extend(self._get_n1_n2_operated_values(n1, n2))
        cells.extend(self._get_n1_n2_operated_values(-n1, n2))
        cells.extend(self._get_n1_n2_operated_values(n1, -n2))
        cells.extend(self._get_n1_n2_operated_values(-n1, -n2))
    
    def _get_n1_n2_operated_values(self, n1, n2):
        rowCells = [
            GridCell(n1 + n2, expr_latex = self._latex_add(n1, n2)),
            GridCell(n1 * n2, expr_latex = self._latex_multiply(n1, n2)),
            GridCell(n1 / n2, expr_latex = self._latex_divide(n1, n2)),
            GridCell(n1 ** n2, expr_latex = self._latex_power(n1, n2)),
            ]
        
        if self.include_log:
            if n1 > 0 and n2 > 0: 
                rowCells.append(GridCell(log(n2)/log(n1), 
                                         expr_latex = self._latex_log(n1, n2)))
            
        return rowCells
        pass
    
    pass

class FractionalGridCellGenerator(GridCellGenerator):
    def get_generator_statement(self, n1, n2, include_log = False):
        n1_latex = latex(n1)
        n2_latex = latex(n2)
        n1_inv_latex = latex_fraction(1, n1)
        n2_inv_latex = latex_fraction(1, n2)
        
        return f"""
        Use one number from ${n1_latex}$, $-{n1_latex}$, ${n1_inv_latex}$, $-{n1_inv_latex}$,
        and another number from 
        ${n2_latex}$, $-{n2_latex}$, ${n2_inv_latex}$ and $-{n2_inv_latex}$, 
        to create real numbers using addition and subtraction operators. 
        """.strip().replace("\n", "").replace("\t", "")
        pass
    
    def init_cells(self, n1, n2, include_log = False):
        self.generator_statement = self.get_generator_statement(
                                    n1, n2, include_log = include_log)
        
        self.include_log = include_log
        cells = []
        self._init_cell_values(n1, n2, cells)
        self._init_cell_values(n2, n1, cells)
        
        cells = sorted(cells, key = lambda x: float(x.v))
        return cells
        pass

    def _init_cell_values(self, n1, n2, cells):
        self._init_cell_values_set(n1, n2, cells)
        self._init_cell_values_set(Rational(1, n1), n2, cells)
        self._init_cell_values_set(n1, Rational(1, n2), cells)
        self._init_cell_values_set(Rational(1, n1), Rational(1, n2), cells)

    def _init_cell_values_set(self, n1, n2, cells):
        cells.extend(self._get_n1_n2_operated_values(n1, n2))
        cells.extend(self._get_n1_n2_operated_values(-n1, n2))
        cells.extend(self._get_n1_n2_operated_values(n1, -n2))
        cells.extend(self._get_n1_n2_operated_values(-n1, -n2))
    
    def _get_n1_n2_operated_values(self,    n1, n2):
        n1_latex = latex(n1)
        n2_latex = latex(n2)
        n2_abs_latex = latex(abs(n2))
        
        rowCells = []
        if n2 < 0:
            expr_latex = f"{n1_latex} - {n2_abs_latex}"
        else:
            expr_latex = f"{n1_latex} + {n2_abs_latex}"
            
        self._add_grid_cell(n1 + n2, expr_latex, rowCells)
    
        return rowCells
        pass
    
    def _add_grid_cell(self, expr, expr_latex, rowCells):
        if isinstance(expr, int) or isinstance(expr, float):
            rowCells.append(GridCell(expr, expr_latex = expr_latex))
        if hasattr(expr, 'is_real'):
            if expr.is_real:
                rowCells.append(GridCell(expr, expr_latex = expr_latex))
        pass
    
    pass


class ExponentialGridCellGenerator(GridCellGenerator):
    def get_generator_statement(self, n1, n2, include_log = False):
        n1_latex = self.get_var_latex(n1)
        n2_latex = self.get_var_latex(n2)
        n1_inv_latex = latex_fraction(1, n1)
        n2_inv_latex = latex_fraction(1, n2)
        
        if include_log:
            s_log = ", log "
        else:
            s_log = ""
            
        
        return f"""
        Use one number from ${n1_latex}$, $-{n1_latex}$, ${n1_inv_latex}$, $-{n1_inv_latex}$, 
        and another number from 
        ${n2_latex}$, $-{n2_latex}$, ${n2_inv_latex}$ and $-{n2_inv_latex}$, 
        to create real numbers using multiplication {s_log}and exponent operators. 
        """.strip().replace("\n", "").replace("\t", "")
        pass
    
    def init_cells(self, n1, n2, include_log = False):
        self.generator_statement = self.get_generator_statement(
                                    n1, n2, include_log = include_log)
        
        self.include_log = include_log
        cells = []
        self._init_cell_values(n1, n2, cells)
        self._init_cell_values(n2, n1, cells)
        
        cells = sorted(cells, key = lambda x: float(x.v))
        return cells
        pass

    def _init_cell_values(self, n1, n2, cells):
        self._init_cell_values_set(n1, n2, cells)
        self._init_cell_values_set(Rational(1, n1), n2, cells)
        self._init_cell_values_set(n1, Rational(1, n2), cells)

    def _init_cell_values_set(self, n1, n2, cells):
        cells.extend(self._get_n1_n2_operated_values(n1, n2))
        cells.extend(self._get_n1_n2_operated_values(-n1, n2))
        cells.extend(self._get_n1_n2_operated_values(n1, -n2))
        cells.extend(self._get_n1_n2_operated_values(-n1, -n2))
    
    def _get_n1_n2_operated_values(self,    n1, n2):
        n1_latex = self.get_var_latex(n1)
        n2_latex = self.get_var_latex(n2)
        
        rowCells = []
        self._add_grid_cell(n1 * n2,
            f"""
            {n1_latex} * {n2_latex}
            """.strip(),
            rowCells)
    
        self._add_grid_cell(n1 ** n2,
            f"""
            {n1_latex}^{{{n2_latex}}}  
            """.strip(),
            rowCells)
        
        if self.include_log:
            self._add_grid_cell(log(n2)/log(n1),
                f"""
                {self._latex_log(n1_latex, n2_latex)}  
                """.strip(),
                rowCells)
            
        return rowCells
        pass
    
    def _add_grid_cell(self, expr, expr_latex, rowCells):
        if isinstance(expr, int) or isinstance(expr, float):
            rowCells.append(GridCell(expr, expr_latex = expr_latex))
        if hasattr(expr, 'is_real'):
            if expr.is_real:
                rowCells.append(GridCell(expr, expr_latex = expr_latex))
        pass
    
    pass

class InequalityGridHelper:
    def __init__(self, **kwargs):
        self.verbose = kwargs.get('verbose', False)
        self.implies = symbols(u'\U000021D2')
        
        self.n1, self.n2 = self._init_n1_n2(kwargs.get('n1', None),
                                            kwargs.get('n2', None))
        
        self.grid_type = self._init_grid_type(kwargs.get('grid_type', None)) 
        self.include_log = kwargs.get('include_log', False) 
        self.unique_cells = kwargs.get('unique_cells', True)
        
        self.shuffled = kwargs.get('shuffled', False)
        
        self.cells, self.generator_statement = self._init_cells()
        
        self.keys, self.duplicate_keys = self._init_keys() 
        self.grid = None
        pass

    def _init_grid_type(self, grid_type):
        if not grid_type is None:
            return grid_type
        grid_type = random.choice(GridCellGenerator.GRID_TYPES)
        return grid_type
        pass

    def _init_cells(self):
        grid_cell_generator = GridCellGenerator.GET_GENERATOR(self.grid_type)
        grid = grid_cell_generator.init_cells(self.n1, 
                                            self.n2,
                                            self.include_log)
        generator_statement = grid_cell_generator.generator_statement
        return grid, generator_statement
        pass

    def rule_top_right_highest(self):
        rule = f"""
        Every cell to the left or bottom of a given cell with value v must have have a lower or equal value than v.
        Similarly, every cell to the right or bottom of a given cell with value v must have have a higher or equal value than v.
        
        In short:
        Right cell = Equal or Higher value
        Above cell = Equal or Higher value
        Left cell = Equal or Lower value
        Bottom cell = Equal or Lower value
        """
        return rule.strip()


    def get_unique_value_cells(self, cells):
        cell_values = []
        unique_value_cells = []
        for cell in cells:
            if cell.v in cell_values:
                continue
            unique_value_cells.append(cell)
            cell_values.append(cell.v)
            pass
        return unique_value_cells

    
    def get_grid(self):
        if self.grid is None:
            self.init_grid()
            
        return self.grid


    def init_grid(self, row_count = None, col_count = None, 
                  ):
        if row_count is None:
            row_count = random.choice([3, 4, 5, 6, 7])
        if col_count is None:
            col_count = random.choice([3, 4, 5, 6, 7])
            
        sOut = f"""
        row_count = {row_count}
        col_count = {col_count}
        """
        #print(sOut)
        
        if self.unique_cells:
            cells = self.get_unique_value_cells(self.cells)
            row_count = min(row_count, 4)
            col_count = min(col_count, 4)
        else:
            cells = self.cells
            

        if self.shuffled:
            random.shuffle(cells)
            map(random.shuffle, cells)
        else:
            cells = self._get_sorted_cells(cells, row_count, col_count)
            
        self.grid = self._get_zero_grid(row_count, col_count)

        cells_data = cells.copy()
        self._populate_cells(row_count, col_count, cells_data)
       
    def _populate_cells(self, 
                        row_count,
                        col_count,
                        cells_data,
                        ):

        lowest_x = row_count - 1
        lowest_y = 0
        self.grid.set_cell_value(lowest_x, lowest_y, cells_data[0])
        del cells_data[0]

        row_col_indexes = []
        for i in range(row_count):
            for j in range(col_count):
                row_col_indexes.append([-i, j])
        
        row_col_indexes = sorted(row_col_indexes, key = lambda x: float(x[0] + x[1]))

        for row_index, col_index in row_col_indexes:
            row_index = -row_index
            #print(f"Populating {row_index}, {col_index}")
            self._populate_neighbours(row_index, col_index, cells_data)
            pass
        pass
        
    def _populate_neighbours(self, x, y,  cells_data):
        if x < 0 or x >= self.grid.rows:
            #print(f"Invalid central_cell x: {x}, rows: {self.grid.rows}")
            return 
             
        if y < 0 or y >= self.grid.cols:
            #print(f"Invalid central_cell y: {y}, cols: {self.grid.cols}")
            return 
        
        central_cell = self.grid.get_cell(x, y)
        #left bottom
        self._populate_cell(central_cell.v, x+1, y-1, cells_data, is_lower = True)
        #bottom
        self._populate_cell(central_cell.v, x+1, y, cells_data, is_lower = True)
        #left
        self._populate_cell(central_cell.v, x, y-1, cells_data, is_lower = True)

        #left-top
        self._populate_cell(central_cell.v, x-1, y-1, cells_data, is_lower = False)
        #right-bottom
        self._populate_cell(central_cell.v, x+1, y+1, cells_data, is_lower = False)
        #right
        self._populate_cell(central_cell.v, x, y+1, cells_data, is_lower = False)
        #top
        self._populate_cell(central_cell.v, x-1, y, cells_data, is_lower = False)
        #right-top
        self._populate_cell(central_cell.v, x-1, y+1, cells_data, is_lower = False)
        
        
        pass
    
    def _populate_cell(self, v, x, y, cells_data, is_lower = True):
        if x < 0 or x >= self.grid.rows:
            #print(f"Invalid x: {x}, rows: {self.grid.rows}")
            return False
             
        if y < 0 or y >= self.grid.cols:
            #print(f"Invalid y: {y}, cols: {self.grid.cols}")
            return False
             
        
        current_cell = self.grid.get_cell(x, y)
        if  current_cell.v != 0:
            #print(f"value already populated: {x}, {y}: {current_cell}")
            return True
        
        if is_lower:
            cell = self._get_lower_value_cell(v, cells_data)
        else:
            cell = self._get_higher_value_cell(v, cells_data)
        
        if not cell is None:
            self.grid.set_cell_value(x, y, cell)
            del cells_data[cells_data.index(cell)]
            return True
        
        return False
        pass
        
    
    def _get_lower_value_cell(self, v, cells_data):
        lower_value_cells = [cell for cell in cells_data
                             if cell.v <= v]
        if len(lower_value_cells) < 1:
            return None
        return lower_value_cells[-1]
        pass

    def _get_higher_value_cell(self, v, cells_data):
        higher_value_cells = [cell for cell in cells_data
                             if cell.v >= v]
        if len(higher_value_cells) < 1:
            return None
        return higher_value_cells[0]
        pass

    def _get_sorted_cells(self, input_cells, row_count, col_count):
        cell_count = row_count * col_count
        if len(input_cells) < cell_count:
            sOut = f"""
            Invalid count: Max cells {len(input_cells)}
            Desired: {row_count} * {col_count}
            """
            #print(sOut)
            
        cells = random.sample(input_cells, min(cell_count, len(input_cells)))
        cells = sorted(cells, key = lambda x: float(x.v))
        cells_str = "\n".join([f"{cell.v}"
                              for cell in cells])
        sOut = f"cells_str = {cells_str}"
        #print(sOut)

        return cells


    def _get_zero_grid(self, row_count, col_count):
        grid_data = []
        for row_index in range(row_count):
            grid_data.append([])
            for col_index in range(col_count):
                grid_data[row_index].append(GridCell(0, 
                                                     v_latex = "None", 
                                                     expr_latex = "None"
                                                     ))
        return Grid(self.n1, self.n2, grid_data)


    def _init_keys(self):
        keys = []
        duplicate_keys = []
        for cell in self.cells:
            if cell.expr_latex in keys:
                print(f"Duplicate: {cell}")
                duplicate_keys.append(cell.expr_latex)
            else:
                keys.append(cell.expr_latex)
        return keys, duplicate_keys
        pass
     
    
    def _init_n1_n2(self, n1 = None, n2 = None):
        m1, m2 = random.sample(range(2, 9), 2)
        if n1 is None:
            n1 = m1
            
        if n2 is None:
            n2 = m2
            
        return (n1, n2)
        pass
    
    pass
#class InequalityGridHelper:
