
#################################################
# HolAdo (Holistic Automation do)
#
# (C) Copyright 2021-2025 by Eric Klumpp
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

# The Software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software.
#################################################



from builtins import range
from builtins import super

from holado_core.common.tools.comparators.comparator import Comparator
from holado_core.common.exceptions.verify_exception import VerifyException
from holado_core.common.exceptions.technical_exception import TechnicalException
from holado_core.common.exceptions.functional_exception import FunctionalException
import logging
from holado_core.common.tables.comparators.string_table_row_comparator import StringTableRowComparator
from holado_core.common.exceptions.exceptions import Exceptions
from holado_core.common.tools.tools import Tools
from holado_core.common.tables.comparators.table_row_comparator import TableRowComparator

logger = logging.getLogger(__name__)


class TableComparator(Comparator):
    def __init__(self, row_comparator=None, cell_comparator=None):
        super().__init__("table")
        
        if row_comparator is not None:
            self.__row_comparator = row_comparator
        elif cell_comparator is not None:
            self.__row_comparator = TableRowComparator(cell_comparator)
        else:
            self.__row_comparator = StringTableRowComparator()

    @property
    def row_comparator(self):
        return self.__row_comparator
    
    def equals(self, table_1, table_2, is_obtained_vs_expected=True, raise_exception=True, do_log=True, return_diffs=False):
        """ Return if tables are equal.
        If return_diffs=True and raise_exception=False, also return the indexes of rows with differences.
        """
        try:
            res = True
            res_diffs = []
            
            # Verify number of rows
            if table_1.nb_rows != table_2.nb_rows:
                if raise_exception:
                    msg = f"Tables have not same number of rows (table {self._get_name_1(is_obtained_vs_expected)}: {table_1.nb_rows} rows ; table {self._get_name_2(is_obtained_vs_expected)}: {table_2.nb_rows} rows)"
                    raise VerifyException(self._build_table_compare_error_message(msg, input_1=table_1, input_2=table_2, is_obtained_vs_expected=is_obtained_vs_expected))
                else:
                    return False, None if return_diffs else False

            # Compare rows content
            for i in range(table_1.nb_rows):
                try:
                    res = self.__row_comparator.equals(table_1.get_row(i),
                                                       table_2.get_row(i),
                                                       is_obtained_vs_expected=is_obtained_vs_expected,
                                                       raise_exception=raise_exception,
                                                       do_log=do_log)
                except FunctionalException as exc:
                    msg = f"Difference exists at line of index {i}"
                    raise VerifyException(self._build_table_compare_error_message(msg, exception=exc, input_1=table_1, input_2=table_2, is_obtained_vs_expected=is_obtained_vs_expected)) from exc
                except Exception as exc:
                    msg = f"Error while comparing line of index {i}"
                    raise TechnicalException(self._build_table_compare_error_message(msg, exception=exc, input_1=table_1, input_2=table_2, is_obtained_vs_expected=is_obtained_vs_expected)) from exc

                if not res:
                    res_diffs.append(i)
                    if raise_exception:
                        raise TechnicalException("This case should have already raised an exception")
                    elif not res_diffs:
                        if do_log and Tools.do_log(logger, logging.DEBUG):
                            msg = f"Difference exists at line of index {i}"
                            logger.debug(self._build_table_compare_error_message(msg, input_1=table_1, input_2=table_2, is_obtained_vs_expected=is_obtained_vs_expected))
                        break
                    
            return res, res_diffs if return_diffs else res
        except (FunctionalException, TechnicalException) as exc:
            raise exc
        except Exception as exc:
            msg = "Error while comparing tables"
            raise TechnicalException(self._build_table_compare_error_message(msg, exception=exc, input_1=table_1, input_2=table_2, is_obtained_vs_expected=is_obtained_vs_expected)) from exc

    def contains_rows(self, table_1, table_2, check_row_order = True, is_obtained_vs_expected = True, raise_exception = True):
        try:
            res = True
            
            # Compare rows content
            start_index = 0
            for i in range(table_2.nb_rows):
                try:
                    res, row_index = self.contains_row(table_1, table_2.get_row(i), start_row_index = start_index,
                                                       is_obtained_vs_expected=is_obtained_vs_expected, raise_exception=raise_exception)
                    if check_row_order:
                        start_index = row_index + 1
                except FunctionalException as exc:
                    msg = f"Table {self._get_name_1(is_obtained_vs_expected)} doesn't contain row {self._get_name_2(is_obtained_vs_expected)} of index {i}"
                    raise VerifyException(self._build_table_compare_error_message(msg, exception=exc, input_1=table_1, input_2=table_2, is_obtained_vs_expected=is_obtained_vs_expected)) from exc
                except Exception as exc:
                    msg = f"Error while checking if table {self._get_name_1(is_obtained_vs_expected)} contains row {self._get_name_2(is_obtained_vs_expected)} of index {i}"
                    raise TechnicalException(self._build_table_compare_error_message(msg, exception=exc, input_1=table_1, input_2=table_2, is_obtained_vs_expected=is_obtained_vs_expected)) from exc
                if not res:
                    break
                    
            return res
        except (FunctionalException, TechnicalException) as exc:
            raise exc
        except Exception as exc:
            msg = f"Error while checking if table {self._get_name_1(is_obtained_vs_expected)} contains rows of table {self._get_name_2(is_obtained_vs_expected)}"
            raise TechnicalException(self._build_table_compare_error_message(msg, exception=exc, input_1=table_1, input_2=table_2, is_obtained_vs_expected=is_obtained_vs_expected)) from exc

    def doesnt_contain_rows(self, table_1, table_2, is_obtained_vs_expected = True, raise_exception = True):
        try:
            res = True
            
            # Compare rows content
            for i in range(table_2.nb_rows):
                try:
                    row_res, _ = self.contains_row(table_1, table_2.get_row(i),
                                                       is_obtained_vs_expected=is_obtained_vs_expected, raise_exception=False)
                except Exception as exc:
                    msg = f"Error while checking if table {self._get_name_1(is_obtained_vs_expected)} contains row {self._get_name_2(is_obtained_vs_expected)} of index {i}"
                    raise TechnicalException(self._build_table_compare_error_message(msg, exception=exc, input_1=table_1, input_2=table_2, is_obtained_vs_expected=is_obtained_vs_expected)) from exc

                if row_res:
                    if raise_exception:
                        msg = f"Table {self._get_name_1(is_obtained_vs_expected)} contains row {self._get_name_2(is_obtained_vs_expected)} of index {i}"
                        raise VerifyException(self._build_table_compare_error_message(msg, input_1=table_1, input_2=table_2, is_obtained_vs_expected=is_obtained_vs_expected))
                    else:
                        res = False
                        break
                
            return res
        except (FunctionalException, TechnicalException) as exc:
            raise exc
        except Exception as exc:
            msg = f"Error while checking if table {self._get_name_1(is_obtained_vs_expected)} contains rows of table {self._get_name_2(is_obtained_vs_expected)}"
            raise TechnicalException(self._build_table_compare_error_message(msg, exception=exc, input_1=table_1, input_2=table_2, is_obtained_vs_expected=is_obtained_vs_expected)) from exc

    def contains_row(self, table_1, row_2, start_row_index = 0, is_obtained_vs_expected=True, raise_exception=True):
        try:
            res = True
            res_index = -1
            
            # Compare rows content
            for i in range(start_row_index, table_1.nb_rows):
                row_1 = None
                try:
                    row_1 = table_1.get_row(i)
                    res = self.__row_comparator.equals(row_1, row_2,
                                                       is_obtained_vs_expected=is_obtained_vs_expected,
                                                       raise_exception=False,
                                                       do_log=False)
                except Exception as exc:
                    msg = f"Error while comparing table {self._get_name_1(is_obtained_vs_expected)} row of index {i}"
                    raise TechnicalException(self._build_table_compare_error_message(msg, exception=exc, input_1=row_1, input_2=row_2, is_obtained_vs_expected=is_obtained_vs_expected)) from exc

                if res:
                    res_index = i
                    break
                
            if not res and raise_exception:
                msg = f"Table {self._get_name_1(is_obtained_vs_expected)} doesn't contain the row {self._get_name_2(is_obtained_vs_expected)}"
                raise VerifyException(self._build_table_compare_error_message(msg, input_1=table_1, input_2=row_2, is_obtained_vs_expected=is_obtained_vs_expected))

            return res, res_index
        except (FunctionalException, TechnicalException) as exc:
            raise exc
        except Exception as exc:
            msg = f"Error while searching row {self._get_name_2(is_obtained_vs_expected)} in table {self._get_name_1(is_obtained_vs_expected)}"
            raise TechnicalException(self._build_table_compare_error_message(msg, exception=exc, input_1=table_1, input_2=row_2, is_obtained_vs_expected=is_obtained_vs_expected)) from exc

    def _represent_table(self, table, indent):
        try:
            return table.represent(indent)
        except Exception as exc:
            return f"[ERROR while representing table: {Exceptions.exception_message(exc)}]"
    
    def _represent_row(self, row, indent):
        try:
            return row.represent(indent)
        except Exception as exc:
            return f"[ERROR while representing table row: {Exceptions.exception_message(exc)}]"

    def __get_input_type_name(self, input):
        from holado_core.common.tables.table import Table
        from holado_core.common.tables.table_row import TableRow

        if isinstance(input, Table):
            return 'table'
        elif isinstance(input, TableRow):
            return 'table'
        else:
            raise TypeError(f"Unsupported input type: {type(input)}")

    def __represent_input(self, input, indent):
        from holado_core.common.tables.table import Table
        from holado_core.common.tables.table_row import TableRow

        if isinstance(input, Table):
            return self._represent_table(input, indent)
        elif isinstance(input, TableRow):
            return self._represent_row(input, indent)
        else:
            raise TypeError(f"Unsupported input type: {type(input)}")

    def _build_table_compare_error_message(self, msg, error_msg=None, exception=None, input_1=None, input_2=None, is_obtained_vs_expected=True):
        msg_lines = [msg]

        if error_msg is not None:
            msg_lines.append(f" -> {error_msg}")
        elif exception is not None:
            msg_lines.append(f" -> {Tools.indent_string(4, Exceptions.exception_message(exception), do_indent_first_line=False)}")

        if input_1 is not None or input_2 is not None:
            msg_lines.append("---- PARAMETERS:")
            if input_1 is not None:
                msg_lines.append(f"  {self.__get_input_type_name(input_1)} {self._get_name_1(is_obtained_vs_expected)}:"),
                msg_lines.append(self.__represent_input(input_1, 4))
            if input_2 is not None:
                msg_lines.append(f"  {self.__get_input_type_name(input_2)} {self._get_name_2(is_obtained_vs_expected)}:"),
                msg_lines.append(self.__represent_input(input_2, 4))

        return "\n".join(msg_lines)


