"""
Silique (https://www.silique.fr)
Copyright (C) 2024-2025
            
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 Tuple, List, Optional

from io import BytesIO
from ruamel.yaml import YAML
import tabulate as tabulate_module
from tabulate import tabulate

from rougail.tiramisu import normalize_family
from tiramisu import undefined
from tiramisu.error import PropertiesOptionError, display_list
try:
    from tiramisu_cmdline_parser.api import gen_argument_name
except:
    gen_argument_name = None

from .i18n import _
from .config import Tables, ROUGAIL_VARIABLE_TYPE


ENTER = "\n\n"


_yaml = YAML()
_yaml.indent(mapping=2, sequence=4, offset=2)


def dump(informations):
    """Dump variable, means transform bool, ... to yaml string"""
    with BytesIO() as ymlfh:
        _yaml.dump(informations, ymlfh)
        ret = ymlfh.getvalue().decode("utf-8").strip()
    if ret.endswith("..."):
        ret = ret[:-3].strip()
    return ret


def to_phrase(msg, type_="variable"):
    """Add maj for the first character and ends with dot"""
    if not msg:
        # replace None to empty string
        return ""
    msg = str(msg).strip()
    # a phrase must ends with a dot
    if type_ == "variable":
        if not msg.endswith("."):
            msg += "."
    elif type_ in ["family", "description"]:
        if msg.endswith("."):
            msg = msg[:-1]
    else:
        raise Exception("unknown type")
    # and start with a maj
    return msg[0].upper() + msg[1:]


class CommonTable:
    """Class with common function for table"""

    def __init__(self, formatter: "CommonFormatter") -> None:
        self.formatter = formatter
        self.clear()

    def clear(self):
        self.columns = []

    def get(self):
        columns = self.get_columns()
        self.clear()
        return columns

    def add(self, informations: dict, modified_attributes: dict, force_identifiers: Optional[str]) -> tuple:
        self.informations = informations
        self.modified_attributes = modified_attributes
        self.force_identifiers = force_identifiers
        self.calculated_properties = []
        self.set_description()
        self.set_type()
        self.set_choices()
        self.set_properties()
        self.set_default()
        self.set_examples()
        self.set_paths()
        self.set_commandline()
        self.set_environment()
        self.set_validators()
        self.set_tags()
        self.columns.append(self._add())

    def set_description(self):
        if "description" in self.informations:
            self.description = self.formatter.get_description(
                "variable", self.informations, self.modified_attributes, self.force_identifiers,
            )
        else:
            self.description = None
        self.help_ = self.formatter.convert_list_to_string("help", self.informations, self.modified_attributes)

    def set_choices(self):
        self.default_is_already_set, self.choices = self.formatter.convert_choices_to_string(
            self.informations, self.modified_attributes
        )

    def set_type(self):
        self.multi = self.informations.get("multiple", True)
        self.type = self.informations["variable_type"]

    def set_default(self):
        if "default" in self.informations:
            self.default = self.formatter.convert_section_to_string(
                    "default", self.informations, self.modified_attributes, multi=self.multi
                )
        else:
            self.default = None

    def set_examples(self):
        self.examples = self.formatter.convert_section_to_string(
            "examples", self.informations, self.modified_attributes, multi=True
        )

    def set_paths(self):
        self.paths = self.formatter.join(self.formatter.display_paths(self.informations, self.modified_attributes, self.force_identifiers, is_variable=True))

    def set_commandline(self):
        if self.formatter.with_commandline and not self.informations.get('not_for_commandline', False):
            commandlines = self.formatter.display_paths(self.informations, self.modified_attributes, self.force_identifiers, is_variable=True, is_bold=False, variable_prefix="--")
            if self.type == 'boolean':
                for path in list(commandlines):
                    commandlines.append("--" + gen_argument_name(path[2:], False, True, False))
            if "alternative_name" in self.informations:
                alternative_name = self.informations["alternative_name"]
                commandlines[0] = f"-{alternative_name}, {commandlines[0]}"
                if self.type == 'boolean':
                    commandlines[1] = "-" + gen_argument_name(alternative_name, True, True, False) + f", {commandlines[1]}"
            self.commandlines = self.formatter.section(_("Command line"), commandlines)
        else:
            self.commandlines = None

    def set_environment(self):
        if self.formatter.with_environment:
            environments = self.formatter.display_paths(self.informations, self.modified_attributes, self.force_identifiers, is_variable=True, is_bold=False, is_upper=True, variable_prefix=self.formatter.prefix)
            self.environments = self.formatter.section(_("Environment variable"), environments)
        else:
            self.environments = None

    def set_properties(self):
        self.properties = self.formatter.property_to_string(
            self.informations, self.calculated_properties, self.modified_attributes,
        )

    def set_validators(self):
        self.validators = self.formatter.convert_section_to_string(
            "validators", self.informations, self.modified_attributes, multi=True
        )

    def set_tags(self):
        self.tags = self.formatter.convert_section_to_string(
            "tags", self.informations, self.modified_attributes, multi=True
        )

    def get_column(self, *args):
        contents = [arg for arg in args if arg]
        self.formatter.columns(contents)
        return self.formatter.join(contents)


class CommonFormatter:
    """Class with common function for formatter"""

    enter_table = "\n"
    # tabulate module name
    name = None

    def __init__(self, rougailconfig, support_namespace, **kwargs):
        tabulate_module.PRESERVE_WHITESPACE = True
        self.header_setted = False
        self.rougailconfig = rougailconfig
        self.support_namespace = support_namespace

    def run(
        self, informations: dict, *, dico_is_already_treated=False
    ) -> str:
        """Transform to string"""
        if informations:
            level = self.rougailconfig["doc.title_level"]
            self.options()
            if self.root:
                current = informations
                for path in self.root.split('.'):
                    info = current[path]['informations']
                    current = current[path]["children"]
                informations = {"informations": info, "children": current}
            return self._run(informations, level, dico_is_already_treated)
        return ""

    def options(self):
        self.with_commandline = self.rougailconfig["doc.with_commandline"]
        self.with_environment = self.rougailconfig["doc.with_environment"]
        if self.with_environment and not gen_argument_name:
            raise Exception('please install tiramisu_cmdline_parser')
        if not self.rougailconfig["main_namespace"] and self.with_environment:
            if self.support_namespace:
                self.prefix = ""
            else:
                self.prefix = self.rougailconfig["doc.environment_default_environment_name"] + "_"
        self.with_family = not self.rougailconfig["doc.without_family"]
        self.root = self.rougailconfig["doc.root"]
        self.other_root_filenames = None
        if self.root:
            try:
                other_root_filenames = self.rougailconfig["doc.other_root_filenames"]
            except PropertiesOptionError:
                pass
            else:
                if other_root_filenames:
                    self.other_root_filenames = dict(zip(other_root_filenames["root_path"], other_root_filenames["filename"]))
        table_model = self.rougailconfig["doc.table_model"]
        self.table_datas = Tables().get()[table_model](self)

    def compute(self, data):
        return "".join([d for d in data if d])

    # Class you needs implement to your Formatter
    def title(
        self,
        title: str,
        level: int,
    ) -> str:
        """Display family name as a title"""
        raise NotImplementedError()

    def join(
        self,
        lst: List[str],
    ) -> str:
        """Display line in table from a list"""
        raise NotImplementedError()

    def bold(
        self,
        msg: str,
    ) -> str:
        """Set a text to bold"""
        raise NotImplementedError()

    def underline(
        self,
        msg: str,
    ) -> str:
        """Set a text to underline"""
        raise NotImplementedError()

    def anchor(self,
               path: str,
               true_path: str,
               ) -> str:
        """Set a text to a link anchor"""
        return path

    def link_variable(self,
                      path: str,
                      true_path: str,
                      description: str,
                      filename: Optional[str],
                      ) -> str:
        """Set a text link to variable anchor"""
        return path

    def stripped(
        self,
        text: str,
    ) -> str:
        """Return stripped text (as help)"""
        raise NotImplementedError()

    def list(
        self,
        choices: list,
        *,
        inside_table: bool=True,
        type_: str="variable",
    ) -> str:
        """Display a liste of element"""
        raise NotImplementedError()

    def prop(
        self,
        prop: str,
        italic: bool,
        delete: bool,
        underline: bool,
    ) -> str:
        """Display property"""
        raise NotImplementedError()

    def link(
        self,
        comment: str,
        link: str,
        underline: bool,
    ) -> str:
        """Add a link"""
        raise NotImplementedError()

    ##################

    def family_informations(self) -> str:
        return ""

    def end_family_informations(self) -> str:
        return ENTER

    def display_paths(
        self,
        informations: dict,
        modified_attributes: dict,
        force_identifiers: Optional[str],
        *,
        is_variable=False,
        variable_prefix: str="",
        is_bold: bool=True,
        is_upper: bool=False,
    ) -> str:
        ret_paths = []
        path = informations["path"]
        if is_bold:
            bold = self.bold
        else:
            def bold(value):
                return value
        if is_upper:
            def upper(value):
                return value.upper()
        else:
            def upper(value):
                return value
        if "identifiers" in modified_attributes:
            name, previous, new = modified_attributes["identifiers"]
            ret_paths.extend(
                [
                    bold(self.delete(upper(variable_prefix + calc_path(path, self, identifier))))
                    for identifier in previous
                ]
            )
        else:
            new = []
        if "identifiers" in informations:
            for idx, identifier in enumerate(informations["identifiers"]):
                if force_identifiers and identifier != force_identifiers:
                    continue
                path_ = calc_path(path, self, identifier)
                if variable_prefix:
                    path_ = variable_prefix + upper(path_)
                if not idx:
                    path_ = self.anchor(path_, path)
                if identifier in new:
                    path_ = self.underline(path_)
                ret_paths.append(bold(path_))
        else:
            ret_paths.append(bold(self.anchor(variable_prefix + upper(path), path)))
        return ret_paths

    def table_header(
        self,
        lst: list,
    ) -> tuple:
        """Manage the header of a table"""
        return lst

    def _run(self, dico: dict, level: int, dico_is_already_treated: bool) -> str:
        """Parse the dict to transform to dict"""
        if dico_is_already_treated:
            return "".join(dico)
        return "".join([msg for msg in self.dict_to_dict(dico, level, init=True)])

    def dict_to_dict(
        self,
        dico: dict,
        level: int,
        *,
        init: bool = False,
    ) -> str:
        """Parse the dict to transform to dict"""
        msg = []
        ori_level = None
        if init and self.root:
            if self.with_family:
                msg.extend(self.family_to_string(dico["informations"], level, False))
            dico = dico["children"]
        for value in dico.values():
            if value["type"] == "variable":
                self.variable_to_string(value)
            elif self.with_family:
                if value["type"] == "namespace":
                    namespace = ori_level is None
                else:
                    namespace = False
                if self.table_datas.columns:
                    msg.append(self.table())
                msg.extend(self.family_to_string(value["informations"], level, namespace))
                msg.extend(self.dict_to_dict(value["children"], level + 1))
                msg.append(self.end_family(level))
            else:
                self.dict_to_dict(
                    value["children"], level + 1,
                )
        if self.table_datas.columns and (init or self.with_family):
            msg.append(self.table())
        return msg

    # FAMILY
    def namespace_to_title(self, informations: dict, level: int) -> str:
        """manage namespace family"""
        return self.title(
            self.get_description("family", informations, {}, None),
            level,
        )

    def family_to_string(self, informations: dict, level: int, namespace: bool) -> str:
        """manage other family type"""
        if namespace:
            ret = [self.namespace_to_title(informations, level)]
        else:
            ret = [self.title(self.get_description("family", informations, {}, None), level)]
        fam_info = self.family_informations()
        if fam_info:
            ret.append(fam_info)
        msg = self.display_paths(informations, {}, None)
        helps = informations.get("help")
        if helps:
            for help_ in helps:
                msg.append(to_phrase(help_.strip()))
        calculated_properties = []
        property_str = self.property_to_string(informations, calculated_properties, {})
        if property_str:
            msg.append(property_str)
        if calculated_properties:
            msg.append(self.join(calculated_properties)
            )
        if "identifier" in informations:
            msg.append(
                self.section(_("Identifiers"), informations["identifier"], type_="family")
            )
        starts_line = self.family_informations_starts_line()
        ret.append(self.family_informations_ends_line().join([starts_line + m for m in msg]) + self.end_family_informations())
        return ret

    def family_informations_starts_line(self) -> str:
        return ""

    def family_informations_ends_line(self) -> str:
        return ENTER

    def end_family(self, level: int) -> str:
        return ""

    def convert_list_to_string(
        self, attribute: str, informations: dict, modified_attributes: dict
    ) -> str():
        datas = []
        if attribute in modified_attributes:
            name, previous, new = modified_attributes[attribute]
            for data in previous:
                datas.append(self.delete(self.to_phrase(data)))
        else:
            new = []
        if attribute in informations:
            for data in informations[attribute]:
                if isinstance(data, dict):
                    if attribute.endswith("s"):
                        attr = attribute[:-1]
                    else:
                        attr = attribute
                    data = data[attr].replace(
                        "{{ identifier }}", self.italic(data["identifier"])
                    )
                data = self.to_phrase(data)
                if data in new:
                    data = self.underline(data)
                datas.append(data)
        return self.stripped(self.join(datas))

    def get_description(
        self, type_: str, informations: dict, modified_attributes: dict, force_identifiers: Optional[str]
    ) -> str():
        def _get_description(description, identifiers, delete=False, new=[]):
            if identifiers and "{{ identifier }}" in description:
                if type_ == "variable":
                    identifiers_text = display_list(
                        [self.italic(i[-1]) for i in identifiers if not force_identifiers or i == force_identifiers], separator="or"
                    )
                    description = description.replace(
                        "{{ identifier }}", identifiers_text
                    )
                else:
                    d = []
                    for i in identifiers:
                        if force_identifiers and i != force_identifiers:
                            continue
                        new_description = description.replace(
                            "{{ identifier }}", self.italic(i[-1])
                        )
                        if new_description not in d:
                            d.append(self.to_phrase(new_description))
                    description = display_list(d, separator="or")
            else:
                description = self.to_phrase(description)
            if description in new:
                description = self.underline(description)
            if delete:
                description = self.delete(description)
            return description

        if "description" in modified_attributes:
            name, previous, new = modified_attributes["description"]
            if previous:
                modified_description = _get_description(
                    previous[0], modified_attributes.get("identifiers", []), delete=True
                )
            else:
                modified_description = None
        else:
            modified_description = None
            new = []
        description = _get_description(
            informations["description"], informations.get("identifiers"), new=new
        )
        if modified_description:
            if description:
                description = self.join([modified_description, description])
            else:
                description = modified_description
        if not description:
            return None
        return self.stripped(description)

    # VARIABLE
    def variable_to_string(
        self, informations: dict, modified_attributes: dict = {}, force_identifiers: Optional[str]=None
    ) -> None:
        """Manage variable"""
        self.table_datas.add(informations, modified_attributes, force_identifiers)

    def convert_section_to_string(
            self, attribute: str, informations: dict, modified_attributes: dict, multi: bool, *, section_name: bool = True,
    ) -> str():
        values = []
        submessage = ""
        if modified_attributes and attribute in modified_attributes:
            name, previous, new = modified_attributes[attribute]
            if isinstance(previous, list):
                for p in previous:
                    submessage, m = self.message_to_string(p, submessage)
                    values.append(self.delete(m))
            else:
                submessage, old_values = self.message_to_string(previous, submessage)
                values.append(self.delete(old_values))
        else:
            new = []
        if attribute in informations:
            old = informations[attribute]
            name = old["name"]
            if isinstance(old["values"], list):
                for value in old["values"]:
                    submessage, old_value = self.message_to_string(value, submessage)
                    if value in new:
                        old_value = self.underline(old_value)
                    values.append(old_value)
                if multi:
                    values = self.list(values)
                else:
                    values = self.join(values)
            elif values:
                old_values = old["values"]
                submessage, old_values = self.message_to_string(old_values, submessage)
                if old["values"] in new:
                    old_values = self.underline(old_values)
                values.append(old_values)
                values = self.join(values)
            else:
                submessage, values = self.message_to_string(old["values"], submessage)
                if old["values"] in new:
                    values = self.underline(values)
        if values != []:
            return self.section(name, values, submessage=submessage, section_name=section_name)

    def convert_choices_to_string(
            self, informations: dict, modified_attributes: dict, with_default: bool = True,
    ) -> str():
        default_is_already_set = False
        if "choices" in informations:
            choices = informations["choices"]
            choices_values = choices["values"]
            if not isinstance(choices_values, list):
                choices_values = [choices_values]
                default_is_a_list = False
            else:
                default_is_a_list = True
            for idx, choice in enumerate(choices_values.copy()):
                if isinstance(choice, dict):
                    choices_values[idx] = self.message_to_string(choice, None)[1]
            if "default" in modified_attributes:
                name, old_default, new_default = modified_attributes["default"]
                if not old_default:
                    old_default = [None]
                if not isinstance(old_default, list):
                    old_default = [old_default]
                for value in old_default.copy():
                    if (
                        isinstance(value, str)
                        and value.endswith(".")
                        and value not in choices_values
                    ):
                        old_default.remove(value)
                        old_default.append(value[:-1])
            else:
                old_default = new_default = []
            # check if all default values are in choices (could be from a calculation)
            if "default" in informations:
                default = informations["default"]["values"]
            else:
                default = [None]
            if not isinstance(default, list):
                default = [default]
            for idx, value in enumerate(default.copy()):
                if isinstance(value, dict):
                    default[idx] = self.message_to_string(value, None)[1]
            default_value_not_in_choices = set(default) - set(choices_values)
            if default_value_not_in_choices:
                default_is_changed = False
                for val in default_value_not_in_choices.copy():
                    if (
                        isinstance(val, str)
                        and val.endswith(".")
                        and val[:-1] in choices_values
                    ):
                        default.remove(val)
                        default.append(val[:-1])
                        default_is_changed = True
                        if val in new_default:
                            new_default.remove(val)
                            new_default.append(val[:-1])
                if default_is_changed:
                    default_value_not_in_choices = set(default) - set(choices_values)
            if default_value_not_in_choices:
                old_default = []
                new_default = []
                default = []
            else:
                default_is_already_set = True
            if "choices" in modified_attributes:
                name, previous, new = modified_attributes["choices"]
                for choice in reversed(previous):
                    if isinstance(choice, dict):
                        choice = self.message_to_string(choice, None)[1]
                    if with_default and choice in old_default:
                        choices_values.insert(
                            0, self.delete(dump(choice) + " ← " + _("(default)"))
                        )
                    else:
                        choices_values.insert(0, self.delete(dump(choice)))
            else:
                new = []
            for idx, val in enumerate(choices_values):
                if with_default and val in old_default:
                    choices_values[idx] = (
                        dump(val) + " " + self.delete("← " + _("(default)"))
                    )
                elif with_default and val in default:
                    if val in new_default:
                        if val in new:
                            choices_values[idx] = self.underline(
                                dump(val) + " " + self.bold("← " + _("(default)"))
                            )
                        else:
                            choices_values[idx] = (
                                dump(val)
                                + " "
                                + self.underline(self.bold("← " + _("(default)")))
                            )
                    else:
                        choices_values[idx] = (
                            dump(val) + " " + self.bold("← " + _("(default)"))
                        )
                elif val in new:
                    choices_values[idx] = self.underline(dump(val))
            # if old value and new value is a list, display a list
            if not default_is_a_list and len(choices_values) == 1:
                choices_values = choices_values[0]
            return default_is_already_set, self.section(choices["name"], choices_values)
        return default_is_already_set, None

    # OTHERs
    def to_phrase(self, text: str) -> str:
        return text

    def property_to_string(
        self,
        informations: dict,
        calculated_properties: list,
        modified_attributes: dict,
        contents: str="all"
    ) -> str:
        """Transform properties to string"""
        properties = []
        modified_properties = []
        if contents in ["all", "type"]:
            if "type" in modified_attributes:
                modified_properties.append(self.prop(modified_attributes["type"], italic=False, delete=True, underline=False))
            if "multi" in modified_attributes:
                modified_properties.append(self.prop("multiple", italic=False, delete=True, underline=False))
        if "properties" in modified_attributes and contents in ["all", "properties"]:
            modified_properties.extend(modified_attributes["properties"])
        local_calculated_properties = {}
        if modified_properties:
            previous, new = self.get_modified_properties(
                *modified_properties
            )
            for p, annotation in previous.items():
                if p not in new:
                    properties.append(self.prop(p, italic=False, delete=True, underline=False))
                if annotation is not None:
                    local_calculated_properties[p] = [{"annotation": annotation, "delete": True}]
        else:
            previous = new = []
        others = []
        if contents in ["all", "type"] and "variable_type" in informations:
            others.append({"name": informations["variable_type"]})
            if informations.get("multiple"):
                others.append({"name": "multiple"})
        if contents in ["all", "properties"] and "properties" in informations:
            others.extend(informations["properties"])
        for prop in others:
            prop_name = prop["name"]
            if prop_name not in previous and prop_name in new:
                underline = True
            else:
                underline = False
            if "annotation" in prop:
                italic = True
                prop_annotation = prop["annotation"]
                if prop_name in new and (
                    prop_name not in previous
                    or new[prop_name] != previous[prop_name]
                ):
                    underline_ = True
#                        prop_annotation = self.underline(prop_annotation)
                else:
                    underline_ = False
                local_calculated_properties.setdefault(prop["name"], []).append(
                        {"annotation": prop_annotation, "underline": underline_}
                )
            else:
                italic = False
            properties.append(self.prop(prop_name, italic=italic, delete=False, underline=underline))
        if local_calculated_properties:
            for (
                calculated_property_name,
                calculated_property,
            ) in local_calculated_properties.items():
#                calculated_property = calculated_property_data["annotation"]
                data = []
                for calc in calculated_property:
                    annotation = self.message_to_string(calc["annotation"], None)[1]
                    if calc.get("underline", False):
                        annotation = self.underline(annotation)
                    if calc.get("delete", False):
                        annotation = self.delete(annotation)
                    data.append(annotation)
                if len(calculated_property) > 1:
                    calculated_property = self.join(data)
                else:
                    calculated_property = data[0]
                calculated_properties.append(
                    self.section(
                        calculated_property_name.capitalize(), calculated_property
                    )
                )
        if not properties:
            return ""
        return " ".join(properties)

    def get_modified_properties(
        self, previous: List[dict], new: List[dict]
    ) -> Tuple[dict, dict]:
        def modified_properties_parser(dico):
            return {d["name"]: d.get("annotation") for d in dico}

        return modified_properties_parser(previous), modified_properties_parser(new)

    def columns(
        self,
        col: List[str],  # pylint: disable=unused-argument
    ) -> None:
        """Manage column"""
        return

    def table(self, with_header: bool = True) -> str:
        """Transform list to a table in string format"""
        if with_header:
            headers = self.table_header(self.table_datas.headers())
        else:
            headers = ()
        msg = (
            tabulate(
                self.table_datas.get(),
                headers=headers,
                tablefmt=self._table_name,
            )
            + "\n\n"
        )
        return msg

    def message_to_string(self, msg, ret, identifiers=[]):
        if isinstance(msg, dict):
            if "submessage" in msg:
                ret += msg["submessage"]
                msg = msg["values"]
            elif "message" in msg:
                filename = None
                if self.other_root_filenames:
                    path = msg["path"]["path"]
                    for root in self.other_root_filenames:
                        if path == root or path.startswith(f'{root}.'):
                            filename = self.other_root_filenames[root]
                            break
                if "identifiers" in msg["path"]:
                    msg["identifiers"] = [msg["path"]["identifiers"]]
                path = self.link_variable(calc_path(msg["path"], self, identifiers), msg["path"]["path"], self.get_description("variable", msg, {}, None), filename=filename)
                msg = msg["message"].format(path)
            elif "description" in msg:
                if "variables" in msg:
                    paths = []
                    for variable in msg["variables"]:
                        filename = None
                        if self.other_root_filenames:
                            path = msg["path"]
                            for root in self.other_root_filenames:
                                if path == root or path.startswith(f'{root}.'):
                                    filename = self.other_root_filenames[root]
                                    break
                        identifiers = variable.get("identifiers")
                        path = calc_path(variable, self, identifiers)
                        paths.append(self.link_variable(path, variable["path"], self.get_description("variable", variable, {}, force_identifiers=identifiers), filename=filename))
                    msg = msg["description"].format(*paths)
                else:
                    msg = msg["description"]
        return ret, msg

    def section(
        self,
        name: str,
        msg: str,
        submessage: str = "",
        type_ = "variable",
        section_name=True,
    ) -> str:
        """Return something like Name: msg"""
        submessage, msg = self.message_to_string(msg, submessage)
        if isinstance(msg, list):
            if len(msg) == 1:
                submessage, elt = self.message_to_string(msg[0], submessage)
                if isinstance(elt, list):
                    submessage += self.list(elt, type_=type_)
                else:
                    submessage += elt
            else:
                lst = []
                for p in msg:
                    submessage, elt = self.message_to_string(p, submessage)
                    lst.append(elt)
                submessage += self.list(lst, type_=type_)
            msg = ""
        if not isinstance(msg, str):
            submessage += dump(msg)
        else:
            submessage += msg
        if section_name:
            return _("{0}: {1}").format(self.bold(name), submessage)
        return submessage


def calc_path(path, formatter=None, identifiers: List[str] = None) -> str:
    def _path_with_identifier(path, identifier):
        identifier = normalize_family(str(identifier))
        if formatter:
            identifier = formatter.italic(identifier)
        return path.replace("{{ identifier }}", identifier, 1)

    if isinstance(path, dict):
        path_ = path["path"]
        if "identifiers" in path:
            for identifier in path["identifiers"]:
                path_ = _path_with_identifier(path_, identifier)
    elif identifiers:
        path_ = path
        for identifier in identifiers:
            path_ = _path_with_identifier(path_, identifier)
    else:
        path_ = path
    return path_
