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

from .const import DXFStructureError
from .tags import group_tags, DXFTag, Tags
from .extendedtags import ExtendedTags
from .validator import entity_structure_validator
from ezdxf.entities import factory
from ezdxf.options import options

if TYPE_CHECKING:
    from ezdxf.eztypes import DXFEntity, Drawing

logger = logging.getLogger('ezdxf')

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

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


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

    {
        # 1. section, HEADER section consist only of one entity
        'HEADER': [entity],
        'CLASSES': [entity, entity, ...],  # 2. section
        'TABLES': [entity, entity, ...],   # 3. section
        ...
        'OBJECTS': [entity, entity, ...],
    }

    {
        # HEADER section consist only of one entity
        'HEADER': [(0, 'SECTION'), (2, 'HEADER'), .... ],
        '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: SectionDict = OrderedDict()
    section: List[Tags] = []
    eof = False
    # The structure checking here should not be changed, ezdxf expect a valid
    # DXF file, to load messy DXF files exist an (future) add-on
    # called 'recover'.

    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'):
            # ENDSEC tag is 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
            # Collect tags outside of sections, but ignore it.
            section = []
        elif tag == (0, 'EOF'):
            # EOF tag is 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


EXCLUDE_STRUCTURE_CHECK = {
    'SECTION', 'ENDSEC', 'EOF', 'TABLE', 'ENDTAB', 'CLASS', 'ACDSRECORD',
    'ACDSSCHEMA'
}


def load_dxf_entities(entities: Iterable[Tags]) -> Iterable['DXFEntity']:
    check_tag_structure = options.check_entity_tag_structures
    for entity in entities:
        if len(entity) == 0:
            logger.debug('Ignore empty DXF entity.')
            continue
        code, dxftype = entity[0]
        if code != 0:
            raise DXFStructureError(
                f'Invalid first tag in DXF entity, group code={code}.')

        if check_tag_structure and (dxftype not in EXCLUDE_STRUCTURE_CHECK):
            entity = entity_structure_validator(entity)
        yield factory.load(ExtendedTags(entity))


def load_and_bind_dxf_content(sections: Dict, doc: 'Drawing') -> None:
    # HEADER has no database entries.
    for name in ['TABLES', 'CLASSES', 'ENTITIES', 'BLOCKS', 'OBJECTS']:
        if name in sections:
            section = sections[name]
            for index, entity in enumerate(load_dxf_entities(section)):
                # Replace Tags() by DXFEntity() objects
                section[index] = entity
                # Bind entities to the DXF document:
                factory.bind(entity, doc)
