# Purpose: DXF structure loader and validator
# Created: 25.01.2018
# Copyright (c) 2018, Manfred Moitzi
# License: MIT License
import logging
from typing import Callable, Dict, Iterable, List, Union, TYPE_CHECKING

from .const import DXFStructureError
from .tags import group_tags, DXFTag, Tags
from .extendedtags import ExtendedTags
from .validator import entity_structure_validator

from ezdxf.options import options

if TYPE_CHECKING:  # import forward declarations
    from ezdxf.eztypes import EntityDB

logger = logging.getLogger('ezdxf')

TagProcessor = Callable[[ExtendedTags], ExtendedTags]
modern_post_load_tag_processors = {}  # type: Dict[str, TagProcessor]
legacy_post_load_tag_processors = {}  # type: Dict[str, TagProcessor]


SectionDict = Dict[str, List[Union[Tags, ExtendedTags]]]


def is_registered(entity: str, legacy: bool = False):
    # just for testing
    processors = legacy_post_load_tag_processors if legacy else modern_post_load_tag_processors
    return entity in processors


def register(entity: str, legacy: bool = False) -> Callable:
    """
    Register (decorator) functions to process from DXF file loaded tags.

    Args:
        entity: DXF type like 'LINE' or 'VERTEX'
        legacy: use for legacy tag structure (DXF version <= AC1009) or modern tag structures

    """
    logger.debug('Register post load tag processor for DXF type: {}; legacy: {}'.format(entity, legacy))

    def decorator(processor: TagProcessor) -> TagProcessor:
        """

        Args:
            processor: function with one parameter 'tags'

        Returns: processor

        """
        processors = legacy_post_load_tag_processors if legacy else modern_post_load_tag_processors
        processors[entity] = processor
        return processor

    return decorator


def load_dxf_structure(tagger: Iterable[DXFTag], ignore_missing_eof: bool = False) -> SectionDict:
    """
    Divide input tag stream from tagger into DXF structure entities. Each DXF structure entity starts with a DXF
    structure (0, ...) tag, and ends before the next DXF structure tag.

    Generated structure:

    each entity is a Tags() object

    {
        'HEADER': [entity],                # 1. section, HEADER section contains only the SECTION head tag
        'CLASSES': [entity, entity, ...],  # 2. section
        'TABLES': [entity, entity, ...],   # 3. section
        ...
        'OBJECTS': [entity, entity, ...],
    }

    {
        'HEADER': [(0, 'SECTION'), (2, 'HEADER'), .... ],  # HEADER section contains only the SECTION head tag
        'CLASSES': [[(0, 'SECTION'), (2, 'CLASSES')], [(0, 'CLASS'), ...], [(0, 'CLASS'), ...]],
        'TABLES': [[(0, 'SECTION'), (2, 'TABLES')], [(0, 'TABLE'), (2, 'VPORT')], [(0, 'VPORT'), ...], ... , [(0, 'ENDTAB')]],
        ...
        'OBJECTS': [[(0, 'SECTION'), (2, 'OBJECTS')], ...]
    }

    Args:
        tagger: generates DXFTag() entities from input data
        ignore_missing_eof: raises DXFStructureError() if False and EOF tag is not present, set to True only in tests

    Returns:
        dict of sections, each section is a list of DXF structure entities as Tags() objects

    """

    def inside_section() -> bool:
        if len(section):
            return section[0][0] == (0, 'SECTION')  # first entity, first tag
        return False

    def outside_section() -> bool:
        if len(section):
            return section[0][0] != (0, 'SECTION')  # first entity, first tag
        return True

    sections = {}  # type: SectionDict
    section = []  # type: List[Tags]
    eof = False
    for entity in group_tags(tagger):
        tag = entity[0]
        if tag == (0, 'SECTION'):
            if inside_section():
                raise DXFStructureError("DXFStructureError: missing ENDSEC tag.")
            if len(section):
                logger.warning('DXF Structure Warning: found tags outside a SECTION, ignored by ezdxf.')
            section = [entity]
        elif tag == (0, 'ENDSEC'):  # not collected
            if outside_section():
                raise DXFStructureError("DXFStructureError: found ENDSEC tag without previous SECTION tag.")
            section_header = section[0]
            if len(section_header) < 2 or section_header[1].code != 2:
                raise DXFStructureError(
                    'DXFStructureError: missing required section NAME tag (2, name) at start of section.')
            name_tag = section_header[1]
            sections[name_tag.value] = section
            section = []  # collect tags outside of sections, but ignore it
        elif tag == (0, 'EOF'):  # not collected
            if eof:
                logger.warning('DXF Structure Warning: found more than one EOF tags.')
            eof = True
        else:
            section.append(entity)
    if inside_section():
        raise DXFStructureError("DXFStructureError: missing ENDSEC tag.")
    if not eof and not ignore_missing_eof:
        raise DXFStructureError('DXFStructureError: missing EOF tag.')
    return sections


DATABASE_EXCLUDE = frozenset(['SECTION', 'ENDSEC', 'EOF', 'TABLE', 'ENDTAB', 'CLASS', 'ACDSRECORD', 'ACDSSCHEMA'])


def load_dxf_entities_into_database(database: 'EntityDB', dxf_entities: List[Tags]) -> Iterable[ExtendedTags]:
    check_tag_structure = options.check_entity_tag_structures
    for entity in dxf_entities:
        if len(entity) == 0:
            raise DXFStructureError('Invalid empty DXF entity.')
        code, dxftype = entity[0]
        if code != 0:
            raise DXFStructureError('Invalid first tag in DXF entity, group code={} .'.format(code))
        if dxftype not in DATABASE_EXCLUDE:
            if check_tag_structure:
                entity = entity_structure_validator(entity)
            entity = ExtendedTags(entity)
            database.add_tags(entity)
        yield entity


def fill_database(database: 'EntityDB', sections: SectionDict, dxfversion: str = 'AC1009') -> None:
    post_processors = legacy_post_load_tag_processors if dxfversion <= 'AC1009' else modern_post_load_tag_processors
    for name in ['TABLES', 'ENTITIES', 'BLOCKS', 'OBJECTS']:
        if name in sections:
            section = sections[name]
            # entities stored in the database are converted from Tags() to ExtendedTags()
            for index, entity in enumerate(load_dxf_entities_into_database(database, section)):
                # entities not stored in database are still Tags() e.g. CLASS
                processor = post_processors.get(entity.dxftype())
                if processor:
                    processor(entity)
                section[index] = entity
