"""loader
flattened XML specific

Created by:
EOLE (http://eole.orion.education.fr)
Copyright (C) 2005-2018

Forked by:
Cadoles (http://www.cadoles.com)
Copyright (C) 2019-2021

Silique (https://www.silique.fr)
Copyright (C) 2022-2026

This program is free software: you can redistribute it and/or modify it
under the terms of the GNU Lesser General Public License as published by the
Free Software Foundation, either version 3 of the License, or (at your
option) any later version.

This program 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 Lesser General Public License for more
details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

from typing import Optional, Union
from json import dumps
from os.path import isfile, basename

from ..i18n import _
from ..error import DictConsistencyError, VariableCalculationDependencyError
from ..tiramisu import normalize_family, CONVERT_OPTION
from .object_model import Calculation


class BaseElt:  # pylint: disable=R0903
    """Base element"""

    path = "."
    type = "family"


def sorted_func_name(func_name):
    s_func_name = func_name.split("/")
    s_func_name.reverse()
    return "/".join(s_func_name)


class TiramisuReflector:
    """Convert object to tiramisu representation"""

    def __init__(
        self,
        objectspace,
        funcs_paths,
    ):
        self.informations_idx = -1
        self.reflector_objects = {}
        self.text = {
            "header": [],
            "option": [],
        }
        self.objectspace = objectspace
        if self.objectspace.export_with_import:
            if self.objectspace.internal_functions:
                for func in self.objectspace.internal_functions:
                    self.text["header"].append(f"func[func] = func")
            self.text["header"].extend(
                [
                    "from tiramisu import *",
                    "from tiramisu.setting import ALLOWED_LEADER_PROPERTIES",
                    "from re import compile as re_compile",
                    "from rougail.tiramisu import func, dict_env, load_functions, ConvertDynOptionDescription",
                ]
            )
        if funcs_paths:
            for funcs_path in sorted(funcs_paths, key=sorted_func_name):
                if not isfile(funcs_path):
                    continue
                self.text["header"].append(f"load_functions('{funcs_path}')")
        if self.objectspace.export_with_import:
            #            if self.objectspace.has_namespace:
            self.text["header"].extend(
                [
                    "try:",
                    "    groups.namespace",
                    "except:",
                    "    groups.addgroup('namespace')",
                ]
            )
            for mode in self.objectspace.modes_level:
                self.text["header"].append(f'ALLOWED_LEADER_PROPERTIES.add("{mode}")')
        self.make_tiramisu_objects()
        for key, value in self.objectspace.jinja.items():
            self.add_jinja_to_function(key, value)

    def add_jinja_to_function(
        self,
        variable_name: str,
        jinja: str,
    ) -> None:
        jinja_text = dumps(jinja, ensure_ascii=False)
        self.text["header"].append(f"dict_env['{variable_name}'] = {jinja_text}")

    def make_tiramisu_objects(self) -> None:
        """make tiramisu objects"""
        baseelt = BaseElt()
        self.objectspace.reflector_names[baseelt.path] = (
            f"option_0{self.objectspace.suffix}"
        )
        basefamily = Family(
            baseelt,
            self,
        )
        for elt in self.objectspace.paths.get():
            if elt.path in self.objectspace.families:
                Family(
                    elt,
                    self,
                )
            else:
                Variable(
                    elt,
                    self,
                )
        baseelt.name = normalize_family(self.objectspace.base_option_name)
        baseelt.description = self.objectspace.base_option_name
        self.reflector_objects[baseelt.path].get(
            [], baseelt.description
        )  # pylint: disable=E1101

    def set_name(
        self,
        elt,
    ):
        """Set name"""
        if elt.path not in self.objectspace.reflector_names:
            self.objectspace.set_name(elt, "optiondescription_")
        return self.objectspace.reflector_names[elt.path]

    def get_information_name(self):
        self.informations_idx += 1
        return f"information_{self.informations_idx}"

    def get_text(self):
        """Get text"""
        return "\n".join(self.text["header"] + self.text["option"])


class Common:
    """Common function for variable and family"""

    def __init__(
        self,
        elt,
        tiramisu,
    ):
        self.objectspace = tiramisu.objectspace
        self.elt = elt
        self.option_name = None
        self.tiramisu = tiramisu
        tiramisu.reflector_objects[elt.path] = self
        self.object_type = None
        self.informations = []

    def get(self, calls, parent_name):
        """Get tiramisu's object"""
        if self.elt.path in calls:
            msg = f'"{self.elt.path}" will make an infinite loop'
            raise DictConsistencyError(msg, 80, self.elt.xmlfiles)
        self_calls = calls.copy()
        self_calls.append(self.elt.path)
        self.calls = self_calls
        if self.option_name is None:
            self.option_name = self.objectspace.reflector_names[self.elt.path]
            self.populate_attrib()
            if self.informations:
                for information in self.informations:
                    self.tiramisu.text["option"].append(
                        f"{information}.set_option({self.option_name})"
                    )
        return self.option_name

    def populate_attrib(self):
        """Populate attributes"""
        keys = {"name": self.convert_str(self.elt.name)}
        if self.elt.type != "symlink":
            keys["doc"] = self.convert_str(self.elt.description)
        self._populate_attrib(keys)
        if self.elt.path in self.objectspace.properties:
            keys["properties"] = self.properties_to_string(
                self.objectspace.properties[self.elt.path]
            )
        self.populate_informations(keys)
        attrib = ", ".join([f"{key}={value}" for key, value in keys.items()])
        self.tiramisu.text["option"].append(
            f"{self.option_name} = {self.object_type}({attrib})"
        )

    def _populate_attrib(
        self,
        keys: dict,
    ) -> None:  # pragma: no cover
        raise NotImplementedError()

    @staticmethod
    def convert_str(value):
        """convert string"""
        if value is None:
            return "None"
        return dumps(value, ensure_ascii=False)

    def properties_to_string(
        self,
        values: list,
    ) -> None:
        """Change properties to string"""
        properties = []
        calc_properties = []
        for property_, value in values.items():
            if not isinstance(value, list):
                value = [value]
            for val in value:
                ret = self.calculation_property(val)
                if isinstance(ret, bool):
                    if ret:
                        properties.append(self.convert_str(property_))
                elif ret is not None:
                    calc_properties.append(ret)
        if properties or calc_properties:
            return (
                "frozenset({" + ", ".join(sorted(properties) + calc_properties) + "})"
            )
        raise Exception("ca existe alors ...")

    def calculation_property(
        self,
        value: Union[Calculation, bool],
    ) -> Optional[bool]:
        if isinstance(value, Calculation):
            try:
                return self.calculation_value(value)
            except VariableCalculationDependencyError:
                return None
        return value

    def calc_properties(
        self,
        prop,
        calculation,
    ) -> str:
        """Populate properties"""
        option_name = self.tiramisu.reflector_objects[child.source.path].get(
            self.calls, self.elt.path
        )
        kwargs = (
            f"'condition': ParamOption({option_name}, notraisepropertyerror=True), "
            f"'expected': {self.populate_param(child.expected)}"
        )
        if child.inverse:
            kwargs += ", 'reverse_condition': ParamValue(True)"
        return (
            f"Calculation(func['calc_value'], Params(ParamValue('{child.name}'), "
            f"kwargs={{{kwargs}}}), func['calc_value_property_help'])"
        )

    def populate_informations(self, keys):
        """Populate Tiramisu's informations"""
        informations = self.objectspace.informations.get(self.elt.path)
        if not informations:
            return
        keys["informations"] = informations

    def populate_param(
        self,
        param,
    ):
        """Populate variable parameters"""
        if not isinstance(param, dict):
            param = {
                "type": "any",
                "value": param,
            }
        if param["type"] == "value":
            return f"ParamValue({param['value']})"
        if param["type"] == "information":
            return self.build_information_param(param)
        if param["type"] == "identifier":
            if "identifier" in param and param["identifier"] != None:
                return f"ParamIdentifier(identifier_index={param['identifier']})"
            return "ParamIdentifier()"
        if param["type"] == "index":
            return "ParamIndex()"
        if param["type"] == "variable":
            return self.build_variable_param(param)
        if param["type"] == "any":
            if isinstance(param["value"], str):
                value = self.convert_str(param["value"])
            else:
                value = str(param["value"])
            return "ParamValue(" + value + ")"
        raise Exception("pfff")

    def build_information_param(self, param: dict) -> str:
        # default? really?
        if self.elt.multi:
            default = []
        else:
            default = None
        if "variable" in param:
            information_variable_path = param["variable"].path
            if information_variable_path == self.elt.path:
                return f'ParamSelfInformation("{param["information"]}", {default})'
            information_variable = self.tiramisu.reflector_objects[
                information_variable_path
            ]
            if information_variable_path not in self.calls:
                option_name = information_variable.get(self.calls, self.elt.path)
                return f'ParamInformation("{param["information"]}", {default}, option={option_name})'
            else:
                # if we want to get information from the a parent family
                information = f'ParamInformation("{param["information"]}", {default})'
                information_name = self.tiramisu.get_information_name()
                self.tiramisu.text["option"].append(
                    f"{information_name} = {information}"
                )
                information_variable.informations.append(information_name)
                return information_name
        return f'ParamInformation("{param["information"]}", {default})'

    def build_variable_param(
        self,
        param: dict,
    ) -> str:
        """build variable parameters"""
        variable = param["variable"]
        whole = param.get("whole", False)
        dynamic = param.get("dynamic", True)
        if variable.path == self.elt.path:
            ret = f"ParamSelfOption(whole={whole}"
            if not dynamic:
                ret += ", dynamic=False"
            return ret + ")"
        if whole:
            msg = _(
                'variable param "{0}" has whole attribute but it\'s not allowed for external variable'
            )
            raise DictConsistencyError(msg.format(variable.path), 34, self.elt.xmlfiles)
        if not dynamic:
            msg = _(
                'variable param "{0}" has dynamic attribute but it\'s not allowed for external variable'
            )
            raise DictConsistencyError(msg.format(variable.path), 34, self.elt.xmlfiles)
        option_name = self.tiramisu.reflector_objects[variable.path].get(
            self.calls, self.elt.path
        )
        params = [f"{option_name}"]
        identifier = param.get("identifier")
        if identifier is not None:
            if not self.objectspace.paths.is_dynamic(variable.path):
                msg = _("internal error, {0} is not a dynamic variable").format(
                    variable.path
                )
                raise DictConsistencyError(msg, 49, self.elt.xmlfiles)
            param_type = "ParamDynOption"
            if isinstance(identifier, Calculation):
                identifiers = self.populate_calculation(identifier)
            else:
                identifiers = []
                for ident in identifier:
                    if isinstance(ident, str):
                        ident = self.convert_str(ident)
                    identifiers.append(str(ident))
                identifiers = "[" + ", ".join(identifiers) + "]"
            params.append(identifiers)
            if param.get("optional", False):
                params.append("optional=True")
        else:
            param_type = "ParamOption"
        propertyerror = param.get("propertyerror", True)
        if not propertyerror:
            params.append("notraisepropertyerror=True")
        elif propertyerror == "transitive":
            params.append("raisepropertyerror=True")
        return f'{param_type}({", ".join(params)})'

    def calculation_value(
        self,
        function,
    ) -> str:
        """Generate calculated value"""
        child = function.to_function(self.objectspace, self.elt.path)
        if isinstance(child, str):
            return self.convert_str(child)
        elif not isinstance(child, dict):
            return child
        new_args = []
        kwargs = []
        if "params" in child:
            for key, value in child["params"].items():
                if key is None:
                    for val in value:
                        new_args.append(self.populate_param(val))
                else:
                    kwargs.append(f"'{key}': " + self.populate_param(value))
        ret = (
            f"Calculation(func['{child['function']}'], Params(("
            + ", ".join(new_args)
            + ")"
        )
        if kwargs:
            ret += ", kwargs={" + ", ".join(kwargs) + "}"
        ret += ")"
        if "warnings_only" in child:
            ret += f", warnings_only={child['warnings_only']}"
        if "help" in child:
            ret += f", help_function=func['{child['help']}']"
        ret = ret + ")"
        return ret

    def populate_calculation(
        self,
        datas: Union[Calculation, str, list],
        return_a_tuple: bool = False,
    ) -> str:
        if isinstance(datas, Calculation):
            datas = self.calculation_value(datas)
        elif isinstance(datas, str):
            datas = self.convert_str(datas)
        if not isinstance(datas, list):
            return datas
        params = []
        for idx, data in enumerate(datas):
            if isinstance(data, Calculation):
                try:
                    params.append(self.calculation_value(data))
                except VariableCalculationDependencyError:
                    continue
            elif isinstance(data, str):
                params.append(self.convert_str(data))
            elif isinstance(data, dict):
                params.append(data)
            else:
                params.append(str(data))
        if return_a_tuple:
            ret = "("
        else:
            ret = "["
        ret += ", ".join(params)
        if return_a_tuple:
            if len(params) <= 1:
                ret += ","
            ret += ")"
        else:
            ret += "]"
        return ret


class Variable(Common):
    """Manage variable"""

    def __init__(
        self,
        elt,
        tiramisu,
    ):
        super().__init__(elt, tiramisu)
        if elt.type in self.tiramisu.objectspace.custom_types:
            self.object_type = self.tiramisu.objectspace.custom_types[elt.type].__name__
        else:
            self.object_type = CONVERT_OPTION[elt.type]["opttype"]

    def _populate_attrib(
        self,
        keys: dict,
    ):
        if self.elt.type == "symlink":
            keys["opt"] = self.tiramisu.reflector_objects[self.elt.opt.path].get(
                self.calls, self.elt.path
            )
            return
        if self.elt.type == "choice":
            try:
                keys["values"] = self.populate_calculation(
                    self.elt.choices, return_a_tuple=True
                )
                if keys["values"] == "(,)":
                    keys["values"] = tuple()
            except VariableCalculationDependencyError:
                keys["values"] = tuple()
        if self.elt.type == "regexp":
            self.object_type = "Regexp_" + self.option_name
            self.tiramisu.text["header"].append(
                f"""class {self.object_type}(RegexpOption):
    __slots__ = tuple()
    _type = 'value'
{self.object_type}._regexp = re_compile(r"{self.elt.regexp}")
"""
            )
        if self.elt.path in self.objectspace.multis:
            keys["multi"] = self.objectspace.multis[self.elt.path]
        if hasattr(self.elt, "default") and self.elt.default is not None:
            try:
                keys["default"] = self.populate_calculation(self.elt.default)
            except VariableCalculationDependencyError:
                pass
        if self.elt.path in self.objectspace.default_multi:
            try:
                keys["default_multi"] = self.populate_calculation(
                    self.objectspace.default_multi[self.elt.path]
                )
            except VariableCalculationDependencyError:
                pass
        if self.elt.validators:
            keys["validators"] = self.populate_calculation(self.elt.validators)
        if self.elt.warnings:
            keys["warnings_only"] = True
        for key, value in (
            CONVERT_OPTION.get(self.elt.type, {}).get("initkwargs", {}).items()
        ):
            if isinstance(value, str):
                value = self.convert_str(value)
            keys[key] = value
        if self.elt.params:
            for param in self.elt.params:
                value = param.value
                if isinstance(value, str):
                    value = self.convert_str(value)
                keys[param.key] = value


class Family(Common):
    """Manage family"""

    def __init__(
        self,
        elt,
        tiramisu,
    ):
        super().__init__(elt, tiramisu)
        if self.elt.type == "dynamic":
            self.tiramisu.objectspace.has_dyn_option = True
            self.object_type = "ConvertDynOptionDescription"
        elif self.elt.type == "sequence":
            self.object_type = "Leadership"
        else:
            self.object_type = "OptionDescription"
        if hasattr(self.elt, "name") and self.elt.name == self.elt.namespace:
            self.group_type = "groups.namespace"
        else:
            self.group_type = None
        self.children = []

    def add(self, child):
        """Add a child"""
        self.children.append(child)

    def _populate_attrib(
        self,
        keys: list,
    ) -> None:
        if self.group_type:
            keys["group_type"] = self.group_type
        if self.elt.type == "dynamic":
            try:
                keys["identifiers"] = self.populate_calculation(self.elt.dynamic)
            except VariableCalculationDependencyError:
                keys["identifiers"] = []
        children = []
        for path in self.objectspace.parents[self.elt.path]:
            children.append(self.objectspace.paths[path])
        keys["children"] = (
            "["
            + ", ".join(
                [
                    self.tiramisu.reflector_objects[child.path].get(
                        self.calls, self.elt.path
                    )
                    for child in children
                ]
            )
            + "]"
        )
