from typing import Dict, List, Optional, Set, Union

from reqif.models.reqif_data_type import ReqIFDataTypeDefinitionEnumeration
from reqif.models.reqif_spec_object import ReqIFSpecObject
from reqif.models.reqif_spec_object_type import (
    ReqIFSpecObjectType,
    SpecAttributeDefinition,
)
from reqif.models.reqif_specification import ReqIFSpecification
from reqif.models.reqif_types import SpecObjectAttributeType
from reqif.reqif_bundle import ReqIFBundle

from strictdoc.backend.reqif.sdoc_reqif_fields import (
    DEFAULT_SDOC_GRAMMAR_FIELDS,
    REQIF_MAP_TO_SDOC_FIELD_MAP,
    ReqIFChapterField,
    ReqIFRequirementReservedField,
)
from strictdoc.backend.sdoc.models.document import SDocDocument
from strictdoc.backend.sdoc.models.document_config import DocumentConfig
from strictdoc.backend.sdoc.models.document_grammar import (
    DocumentGrammar,
    GrammarElement,
)
from strictdoc.backend.sdoc.models.free_text import FreeText
from strictdoc.backend.sdoc.models.node import SDocNode, SDocNodeField
from strictdoc.backend.sdoc.models.reference import ParentReqReference
from strictdoc.backend.sdoc.models.section import SDocSection
from strictdoc.backend.sdoc.models.type_system import (
    GrammarElementFieldMultipleChoice,
    GrammarElementFieldSingleChoice,
    GrammarElementFieldString,
)
from strictdoc.helpers.string import unescape


class P01_ReqIFToSDocConverter:  # pylint: disable=invalid-name
    @staticmethod
    def convert_reqif_bundle(reqif_bundle: ReqIFBundle) -> List[SDocDocument]:
        # TODO: Should we rather show an error that there are no specifications?
        if (
            reqif_bundle.core_content is None
            or reqif_bundle.core_content.req_if_content is None
            or len(reqif_bundle.core_content.req_if_content.specifications) == 0
        ):
            return [P01_ReqIFToSDocConverter.create_document(title=None)]

        documents: List[SDocDocument] = []
        for (
            specification
        ) in reqif_bundle.core_content.req_if_content.specifications:
            document = P01_ReqIFToSDocConverter._create_document_from_reqif_specification(
                specification=specification,
                reqif_bundle=reqif_bundle,
            )
            documents.append(document)
        return documents

    @staticmethod
    def is_spec_object_requirement(_):
        return True

    @staticmethod
    def is_spec_object_section(
        spec_object: ReqIFSpecObject, reqif_bundle: ReqIFBundle
    ):
        spec_object_type = reqif_bundle.lookup.get_spec_type_by_ref(
            spec_object.spec_object_type
        )
        attribute_map: Dict[str, SpecAttributeDefinition] = (
            spec_object_type.attribute_map
        )
        for attribute in spec_object.attributes:
            long_name_or_none: Optional[str] = attribute_map[
                attribute.definition_ref
            ].long_name
            if long_name_or_none is None:
                raise NotImplementedError(attribute_map)
            field_name: str = long_name_or_none
            if field_name == ReqIFChapterField.CHAPTER_NAME:
                return True
        return False

    @staticmethod
    def convert_requirement_field_from_reqif(field_name: str) -> str:
        if field_name in ReqIFRequirementReservedField.SET:
            return REQIF_MAP_TO_SDOC_FIELD_MAP[field_name]
        return field_name

    @staticmethod
    def _create_document_from_reqif_specification(
        *,
        specification: ReqIFSpecification,
        reqif_bundle: ReqIFBundle,
    ):
        document = P01_ReqIFToSDocConverter.create_document(
            title=specification.long_name
        )
        elements: List[GrammarElement] = []
        document.section_contents = []
        section_stack = [document]
        used_spec_object_types_ids: Set[str] = set()

        for current_hierarchy in reqif_bundle.iterate_specification_hierarchy(
            specification
        ):
            current_section = section_stack[-1]

            spec_object = reqif_bundle.get_spec_object_by_ref(
                current_hierarchy.spec_object
            )
            used_spec_object_types_ids.add(spec_object.spec_object_type)
            if P01_ReqIFToSDocConverter.is_spec_object_section(
                spec_object,
                reqif_bundle=reqif_bundle,
            ):
                section = (
                    P01_ReqIFToSDocConverter.create_section_from_spec_object(
                        spec_object,
                        current_hierarchy.level,
                        reqif_bundle=reqif_bundle,
                    )
                )
                if current_hierarchy.level > current_section.ng_level:
                    current_section.section_contents.append(section)
                    section_stack.append(section)
                elif current_hierarchy.level < current_section.ng_level:
                    for _ in range(
                        0,
                        (current_section.ng_level - current_hierarchy.level)
                        + 1,
                    ):
                        assert len(section_stack) > 0
                        section_stack.pop()
                        current_section = section_stack[-1]
                    current_section.section_contents.append(section)
                    section_stack.append(section)
                else:
                    section_stack.pop()
                    current_section = section_stack[-1]
                    current_section.section_contents.append(section)
                    section_stack.append(section)
            elif P01_ReqIFToSDocConverter.is_spec_object_requirement(
                spec_object
            ):
                requirement: SDocNode = P01_ReqIFToSDocConverter.create_requirement_from_spec_object(
                    spec_object=spec_object,
                    parent_section=section_stack[-1],
                    reqif_bundle=reqif_bundle,
                    level=current_hierarchy.level,
                )
                for _ in range(
                    0, (current_section.ng_level - current_hierarchy.level) + 1
                ):
                    assert len(section_stack) > 0
                    section_stack.pop()
                    current_section = section_stack[-1]
                current_section.section_contents.append(requirement)
            else:
                raise NotImplementedError(spec_object) from None

        # See SDOC_IMPL_1.
        if (
            len(document.section_contents) > 0
            and isinstance(document.section_contents[0], SDocSection)
            and document.section_contents[0].title == "Abstract"
        ):
            assert len(document.section_contents[0].free_texts)
            document.free_texts = document.section_contents[0].free_texts
            document.section_contents.pop(0)

        for used_spec_object_type_id in used_spec_object_types_ids:
            spec_object_type_or_none: Optional[ReqIFSpecObjectType] = (
                reqif_bundle.get_spec_object_type_by_ref(
                    ref=used_spec_object_type_id,
                )
            )
            assert (
                spec_object_type_or_none is not None
            ), "Expect the spec object type to be present."
            spec_object_type: ReqIFSpecObjectType = spec_object_type_or_none
            attributes = list(spec_object_type.attribute_map.keys())
            if attributes != DEFAULT_SDOC_GRAMMAR_FIELDS:
                grammar_element = P01_ReqIFToSDocConverter.create_grammar_element_from_spec_object_type(
                    spec_object_type=spec_object_type,
                    reqif_bundle=reqif_bundle,
                )
                elements.append(grammar_element)
        if len(elements) > 0:
            grammar = DocumentGrammar(parent=document, elements=elements)
            grammar.is_default = False
        else:
            grammar = DocumentGrammar.create_default(parent=document)
        document.grammar = grammar

        return document

    @staticmethod
    def create_grammar_element_from_spec_object_type(
        *,
        spec_object_type: ReqIFSpecObjectType,
        reqif_bundle: ReqIFBundle,
    ):
        fields = []
        for attribute in spec_object_type.attribute_definitions:
            field_name = (
                P01_ReqIFToSDocConverter.convert_requirement_field_from_reqif(
                    attribute.long_name
                )
            )
            # Chapter name is a reserved field for sections.
            if field_name == ReqIFChapterField.CHAPTER_NAME:
                continue
            if attribute.attribute_type == SpecObjectAttributeType.STRING:
                fields.append(
                    GrammarElementFieldString(
                        parent=None,
                        title=field_name,
                        human_title=None,
                        required="False",
                    )
                )
            elif attribute.attribute_type == SpecObjectAttributeType.XHTML:
                fields.append(
                    GrammarElementFieldString(
                        parent=None,
                        title=field_name,
                        human_title=None,
                        required="False",
                    )
                )
            elif (
                attribute.attribute_type == SpecObjectAttributeType.ENUMERATION
            ):
                enum_data_type: ReqIFDataTypeDefinitionEnumeration = (
                    reqif_bundle.lookup.get_data_type_by_ref(
                        attribute.datatype_definition
                    )
                )
                options = list(map(lambda v: v.key, enum_data_type.values))
                if attribute.multi_valued is True:
                    fields.append(
                        GrammarElementFieldMultipleChoice(
                            parent=None,
                            title=field_name,
                            human_title=None,
                            options=options,
                            required="False",
                        )
                    )
                else:
                    fields.append(
                        GrammarElementFieldSingleChoice(
                            parent=None,
                            title=field_name,
                            human_title=None,
                            options=options,
                            required="False",
                        )
                    )
            elif attribute.attribute_type == SpecObjectAttributeType.DATE:
                # Recognize the DATE attributes but do nothing about them,
                # since StrictDoc has no concept of "date" for its grammar
                # fields.
                pass
            else:
                raise NotImplementedError(attribute) from None
        requirement_element = GrammarElement(
            parent=None, tag="REQUIREMENT", fields=fields, relations=[]
        )
        return requirement_element

    @staticmethod
    def create_document(title: Optional[str]) -> SDocDocument:
        document_config = DocumentConfig.default_config(None)
        document_title = title if title else "<No title>"
        document = SDocDocument(
            None, document_title, document_config, None, None, None, [], []
        )
        document.grammar = DocumentGrammar.create_default(document)
        # FIXME: One day this will go away.
        document.ng_at_least_one_relations_field = False
        return document

    @staticmethod
    def create_section_from_spec_object(
        spec_object: ReqIFSpecObject, level, reqif_bundle: ReqIFBundle
    ) -> SDocSection:
        spec_object_type = reqif_bundle.lookup.get_spec_type_by_ref(
            spec_object.spec_object_type
        )
        attribute_map: Dict[str, SpecAttributeDefinition] = (
            spec_object_type.attribute_map
        )
        assert attribute_map is not None
        for attribute in spec_object.attributes:
            field_name_or_none: Optional[str] = attribute_map[
                attribute.definition_ref
            ].long_name
            if field_name_or_none is None:
                raise NotImplementedError
            field_name: str = field_name_or_none
            if field_name == ReqIFChapterField.CHAPTER_NAME:
                section_title = attribute.value
                break
        else:
            raise NotImplementedError(attribute_map)

        free_texts = []
        if ReqIFChapterField.TEXT in spec_object.attribute_map:
            free_text = unescape(
                spec_object.attribute_map[ReqIFChapterField.TEXT].value
            )
            free_texts.append(
                FreeText(
                    parent=None,
                    parts=[free_text],
                )
            )
        # Sanitize the title. Titles can also come from XHTML attributes with
        # custom newlines such as:
        #             <ATTRIBUTE-VALUE-XHTML>
        #               <THE-VALUE>
        #                 Some value
        #               </THE-VALUE>
        section_title = section_title.strip().replace("\n", " ")
        section = SDocSection(
            parent=None,
            mid=None,
            uid=None,
            custom_level=None,
            title=section_title,
            requirement_prefix=None,
            free_texts=free_texts,
            section_contents=[],
        )
        section.ng_level = level
        return section

    @staticmethod
    def create_requirement_from_spec_object(
        spec_object: ReqIFSpecObject,
        parent_section: Union[SDocSection, SDocDocument],
        reqif_bundle: ReqIFBundle,
        level,
    ) -> SDocNode:
        fields = []
        spec_object_type = reqif_bundle.lookup.get_spec_type_by_ref(
            spec_object.spec_object_type
        )
        attribute_map: Dict[str, SpecAttributeDefinition] = (
            spec_object_type.attribute_map
        )

        foreign_key_id_or_none: Optional[str] = None
        for attribute in spec_object.attributes:
            long_name_or_none = attribute_map[
                attribute.definition_ref
            ].long_name
            if long_name_or_none is None:
                raise NotImplementedError
            field_name: str = long_name_or_none
            if attribute.attribute_type == SpecObjectAttributeType.ENUMERATION:
                sdoc_field_name = P01_ReqIFToSDocConverter.convert_requirement_field_from_reqif(
                    field_name,
                )
                enum_values_resolved = []
                for (
                    attribute_definition_
                ) in spec_object_type.attribute_definitions:
                    if (
                        attribute.definition_ref
                        == attribute_definition_.identifier
                    ):
                        datatype_definition = (
                            attribute_definition_.datatype_definition
                        )

                        datatype: ReqIFDataTypeDefinitionEnumeration = (
                            reqif_bundle.lookup.get_data_type_by_ref(
                                datatype_definition
                            )
                        )

                        enum_values_list = list(attribute.value)
                        for enum_value in enum_values_list:
                            enum_values_resolved.append(
                                datatype.values_map[enum_value].key
                            )

                        break
                else:
                    raise NotImplementedError

                enum_values = ", ".join(enum_values_resolved)
                fields.append(
                    SDocNodeField(
                        parent=None,
                        field_name=sdoc_field_name,
                        field_value=enum_values,
                        field_value_multiline=None,
                        field_value_references=None,
                    )
                )
                continue
            assert isinstance(attribute.value, str)
            if long_name_or_none == "ReqIF.ForeignID":
                foreign_key_id_or_none = attribute.definition_ref
            attribute_value: Optional[str] = unescape(attribute.value)
            attribute_multiline_value = None
            if (
                "\n" in attribute_value
                or field_name == ReqIFRequirementReservedField.TEXT
                or field_name == ReqIFRequirementReservedField.COMMENT_NOTES
            ):
                attribute_multiline_value = attribute_value.lstrip()
                attribute_value = None

            sdoc_field_name = (
                P01_ReqIFToSDocConverter.convert_requirement_field_from_reqif(
                    field_name,
                )
            )
            fields.append(
                SDocNodeField(
                    parent=None,
                    field_name=sdoc_field_name,
                    field_value=attribute_value,
                    field_value_multiline=attribute_multiline_value,
                    field_value_references=None,
                )
            )
        requirement = SDocNode(
            parent=parent_section,
            requirement_type="REQUIREMENT",
            mid=None,
            fields=fields,
        )
        requirement.ng_level = level
        # FIXME: One day this will go away.
        requirement.ng_uses_old_refs_field = True

        if foreign_key_id_or_none is not None:
            spec_object_parents = reqif_bundle.get_spec_object_parents(
                spec_object.identifier
            )
            parent_refs = []
            for spec_object_parent in spec_object_parents:
                parent_spec_object_parent = (
                    reqif_bundle.lookup.get_spec_object_by_ref(
                        spec_object_parent
                    )
                )

                parent_refs.append(
                    ParentReqReference(
                        requirement,
                        parent_spec_object_parent.attribute_map[
                            foreign_key_id_or_none
                        ].value,
                        role=None,
                    )
                )
            if len(parent_refs) > 0:
                requirement_field = SDocNodeField(
                    parent=requirement,
                    field_name="REFS",
                    field_value=None,
                    field_value_multiline=None,
                    field_value_references=parent_refs,
                )
                fields.append(requirement_field)
                requirement.ordered_fields_lookup["REFS"] = [requirement_field]
        return requirement
