#  Copyright (c) 2021-2022, Manfred Moitzi
#  License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Optional, Iterator
from collections import Counter

from ezdxf.lldxf.types import POINTER_CODES
from ezdxf.protocols import referenced_blocks

if TYPE_CHECKING:
    from ezdxf.document import Drawing
    from ezdxf.lldxf.tags import Tags
    from ezdxf.entities import DXFEntity, BlockRecord

__all__ = ["BlockDefinitionIndex", "BlockReferenceCounter"]

""" 
Where are block references located:

- HEADER SECTION: $DIMBLK, $DIMBLK1, $DIMBLK2, $DIMLDRBLK 
- DIMENSION: arrows referenced in the associated anonymous BLOCK, covered by 
  the INSERT entities in that BLOCK
- ACAD_TABLE: has an anonymous BLOCK representation, covered by the 
  INSERT entities in that BLOCK
- LEADER: DIMSTYLE override "dimldrblk" is stored as handle in XDATA

Entity specific block references, returned by the "ReferencedBlocks" protocol:
- INSERT: "name"
- DIMSTYLE: "dimblk", "dimblk1", "dimblk2", "dimldrblk"
- MLEADER: arrows, blocks - has no anonymous BLOCK representation
- MLEADERSTYLE: arrows
- DIMENSION: "geometry", the associated anonymous BLOCK
- ACAD_TABLE: currently managed as generic DXFTagStorage, that should return 
  the group code 343 "block_record" 

Possible unknown or undocumented block references:
- DXFTagStorage - all handle group codes
- XDATA - only group code 1005
- APPDATA - all handle group codes
- XRECORD - all handle group codes

Contains no block references as far as known:
- DICTIONARY: can only references DXF objects like XRECORD or DICTIONARYVAR
- Extension Dictionary is a DICTIONARY object
- REACTORS - used for object messaging, a reactor does not establish 
  a block reference

Block references are stored as handles to the BLOCK_RECORD entity!

Testing DXF documents with missing BLOCK definitions:

- INSERT without an existing BLOCK definition does NOT crash AutoCAD/BricsCAD
- HEADER variables $DIMBLK, $DIMBLK2, $DIMBLK2 and $DIMLDRBLK can reference 
  non existing blocks without crashing AutoCAD/BricsCAD  

"""


class BlockDefinitionIndex:
    """Index of all :class:`~ezdxf.entities.BlockRecord` entities representing
    real BLOCK definitions, excluding all :class:`~ezdxf.entities.BlockRecord`
    entities defining model space or paper space layouts. External references
    (XREF) and XREF overlays are included.

    """
    def __init__(self, doc: Drawing):
        self._doc = doc
        # mapping: handle -> BlockRecord entity
        self._handle_index: dict[str, BlockRecord] = dict()
        # mapping: block name -> BlockRecord entity
        self._name_index: dict[str, BlockRecord] = dict()
        self.rebuild()

    @property
    def block_records(self) -> Iterator[BlockRecord]:
        """Returns an iterator of all :class:`~ezdxf.entities.BlockRecord`
        entities representing BLOCK definitions.
        """
        return iter(self._doc.tables.block_records)  # type: ignore

    def rebuild(self):
        """Rebuild index from scratch."""
        handle_index = self._handle_index
        name_index = self._name_index
        handle_index.clear()
        name_index.clear()
        for block_record in self.block_records:
            if block_record.is_block_layout:
                handle_index[block_record.dxf.handle] = block_record
                name_index[block_record.dxf.name] = block_record

    def has_handle(self, handle: str) -> bool:
        """Returns ``True`` if a :class:`~ezdxf.entities.BlockRecord` for the
        given block record handle exist.
        """
        return handle in self._handle_index

    def has_name(self, name: str) -> bool:
        """Returns ``True`` if a :class:`~ezdxf.entities.BlockRecord` for the
        given block name exist.
        """
        return name in self._name_index

    def by_handle(self, handle: str) -> Optional[BlockRecord]:
        """Returns the :class:`~ezdxf.entities.BlockRecord` for the given block
        record handle or ``None``.
        """
        return self._handle_index.get(handle)

    def by_name(self, name: str) -> Optional[BlockRecord]:
        """Returns :class:`~ezdxf.entities.BlockRecord` for the given block name
        or ``None``.
        """
        return self._name_index.get(name)


class BlockReferenceCounter:
    """
    Counts all block references in a DXF document.

    Check if a block is referenced by any entity or any resource (DIMSYTLE,
    MLEADERSTYLE) in a DXF document::

        import ezdxf
        from ezdxf.blkrefs import BlockReferenceCounter

        doc = ezdxf.readfile("your.dxf")
        counter = BlockReferenceCounter(doc)
        count = counter.by_name("XYZ")
        print(f"Block 'XYZ' if referenced {count} times.")

    """

    def __init__(self, doc: Drawing, index: Optional[BlockDefinitionIndex] = None):
        # mapping: handle -> BlockRecord entity
        self._block_record_index = (
            index if index is not None else BlockDefinitionIndex(doc)
        )

        # mapping: handle -> reference count
        self._counter = count_references(
            doc.entitydb.values(), self._block_record_index
        )
        self._counter.update(header_section_handles(doc))

    def by_handle(self, handle: str) -> int:
        """Returns the block reference count for a given
        :class:`~ezdxf.entities.BlockRecord` handle.
        """
        return self._counter[handle]

    def by_name(self, block_name: str) -> int:
        """Returns the block reference count for a given block name."""
        handle = ""
        block_record = self._block_record_index.by_name(block_name)
        if block_record is not None:
            handle = block_record.dxf.handle
        return self._counter[handle]


def count_references(
    entities: Iterable[DXFEntity], index: BlockDefinitionIndex
) -> Counter:
    from ezdxf.entities import XRecord, DXFTagStorage

    def update(handles: Iterable[str]):
        # only count references to existing blocks:
        counter.update(h for h in handles if index.has_handle(h))

    counter: Counter = Counter()
    for entity in entities:
        # add handles stored in XDATA and APP data
        update(generic_handles(entity))
        # add entity specific block references
        update(referenced_blocks(entity))
        # special entity types storing arbitrary raw DXF tags:
        if isinstance(entity, XRecord):
            update(all_pointer_handles(entity.tags))
        elif isinstance(entity, DXFTagStorage):
            # XDATA and APP data is already done!
            for tags in entity.xtags.subclasses[1:]:
                update(all_pointer_handles(tags))
            # ignore embedded objects: special objects for MTEXT and ATTRIB
    return counter


def generic_handles(entity: DXFEntity) -> Iterable[str]:
    handles: list[str] = []
    if entity.xdata is not None:
        for tags in entity.xdata.data.values():
            handles.extend(value for code, value in tags if code == 1005)
    if entity.appdata is not None:
        for tags in entity.appdata.data.values():
            handles.extend(all_pointer_handles(tags))
    return handles


def all_pointer_handles(tags: Tags) -> Iterable[str]:
    return (value for code, value in tags if code in POINTER_CODES)


def header_section_handles(doc: "Drawing") -> Iterable[str]:
    header = doc.header
    for var_name in ("$DIMBLK", "$DIMBLK1", "$DIMBLK2", "$DIMLDRBLK"):
        blk_name = header.get(var_name, None)
        if blk_name is not None:
            block = doc.blocks.get(blk_name, None)
            if block is not None:
                yield block.block_record.dxf.handle
