#########################################################################
# MacSyLib - Python library to detect macromolecular systems            #
#            in prokaryotes protein dataset using systems modelling     #
#            and similarity search.                                     #
#                                                                       #
# Authors: Sophie Abby, Bertrand Neron                                  #
# Copyright (c) 2014-2025  Institut Pasteur (Paris) and CNRS.           #
# See the COPYRIGHT file for details                                    #
#                                                                       #
# This file is part of MacSyLib package.                                #
#                                                                       #
# MacSyLib is free software: you can redistribute it and/or modify      #
# it under the terms of the GNU General Public License as published by  #
# the Free Software Foundation, either version 3 of the License, or     #
# (at your option) any later version.                                   #
#                                                                       #
# MacSyLib is distributed in the hope that it will be useful,           #
# but WITHOUT ANY WARRANTY; without even the implied warranty of        #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the          #
# GNU General Public License for more details .                         #
#                                                                       #
# You should have received a copy of the GNU General Public License     #
# along with MacSyLib (COPYING).                                        #
# If not, see <https://www.gnu.org/licenses/>.                          #
#########################################################################

import xml.etree.ElementTree as Et
import logging
from typing import Any, Callable

from .error import MacsylibError
_log = logging.getLogger(__name__)


class ModelConfParser:
    """
    Handle model_conf.xml configuration file.
    """

    def __init__(self, path: str) -> None:
        """

        :param str path: The path to the configuration file
        """
        self._path = path


    def parse(self) -> dict[str: Any]:
        """
        Parse the xml 'model_conf' file set at the root of a data package

        :return: The specific configuration for a model family
        :rtype: dict with the name of variables as keys and value as values
        """
        model_conf_node = self._get_model_conf_node()
        weights_node = model_conf_node.find("./weights")

        filtering_opt = {}
        weights = {}
        if weights_node is not None:
            weights = self.parse_weights(weights_node)

        filtering_node = model_conf_node.find("./filtering")
        if filtering_node is not None:
            filtering_opt = self.parse_filtering(filtering_node)

        model_conf = {k: v for conf_part in (weights, filtering_opt) for k, v in conf_part.items()}
        return model_conf


    def _get_model_conf_node(self) -> Et.ElementTree:
        """
        Find the root of the document

        :return: the document root of model_conf
        """
        try:
            tree = Et.parse(self._path)
            model_node = tree.getroot()
        except Exception as err:
            msg = f"unable to parse model configuration '{self._path}' : {err}"
            _log.critical(msg)
            raise MacsylibError(msg) from None
        return model_node


    def parse_weights(self, weights_node: Et.ElementTree) -> dict[str: float]:
        """
        Parse the node 'weights' containing the scoring weight configuration

        :param weights_node: the node 'weights'
        :return: the configuration option/value about the scores
        """
        elements = {'itself': float,
                    'exchangeable': float,
                    'mandatory': float,
                    'accessory': float,
                    'neutral': float,
                    'out_of_cluster': float,
                    'redundancy_penalty': float}
        weights_conf = self._parse_section(weights_node, elements)
        # rename options as in the other part of MSF
        weights_conf = {(f"{k}_weight"if k != 'redundancy_penalty' else k): v for k, v in weights_conf.items()}
        return weights_conf


    def parse_filtering(self, filtering_node: Et.ElementTree) -> dict[str: Any]:
        """
        Parse the node 'filtering' containing the filtering options configuration

        :param filtering_node: the node 'filtering'
        :return: the configuration option/value about the filtering
        """
        def parse_cut_ga(value):
            if value.lower() in ('true', 1):
                return True
            elif value.lower() in ('false', 0):
                return False
            else:
                msg = f"cannot parse 'cut_ga' element in '{self._path}' expect True, 1, False, 0 got : '{value}'"
                _log.critical(msg)
                raise MacsylibError(msg)

        elements = {'e_value_search': float,
                    'i_evalue_sel': float,
                    'coverage_profile': float,
                    'cut_ga': parse_cut_ga,
                    }
        fiter_conf = self._parse_section(filtering_node, elements)
        return fiter_conf


    def _parse_section(self, section_node: Et.ElementTree, allowed_elements: dict[str: Callable]) -> dict[str: Any]:
        """
        Parse a node containing configurations options and value

        :param section_node:
        :param allowed_elements: The elements allowed in this section
                                 Only these elements are parsed and in the final dictionnary
        :type allowed_elements: a dict with options name as keys and function to parse the element
        :return: dict
        """
        section = {}
        for child in section_node:
            element = child.tag
            if element in allowed_elements:
                value = child.text
                try:
                    value = allowed_elements[element](value)
                except (TypeError, ValueError) as err:
                    msg = f"The model configuration file '{self._path}' is not valid: {err}"
                    _log.critical(msg)
                    raise MacsylibError(msg) from None
            else:
                _log.warning(f"unknown element '{element}' in '{self._path}' ignore it.")
                continue
            section[element] = value
        return section
