1# Copyright (c) 2020, Manfred Moitzi
2# License: MIT License
3from typing import TYPE_CHECKING, Iterable, Callable, List, Optional
4
5from ezdxf.entities import factory, DXFGraphic, SeqEnd, DXFEntity
6from ezdxf.lldxf import const
7
8if TYPE_CHECKING:
9    from ezdxf.eztypes import DXFEntity, EntityDB, Drawing
10
11__all__ = ['entity_linker', 'LinkedEntities']
12
13
14class LinkedEntities(DXFGraphic):
15    """ Super class for common features of the INSERT and the POLYLINE entity.
16    Both have linked entities like the VERTEX or ATTRIB entity and a
17    SEQEND entity.
18
19    """
20
21    def __init__(self):
22        super().__init__()
23        self._sub_entities: List[DXFGraphic] = []
24        self.seqend: Optional['SeqEnd'] = None
25
26    def _copy_data(self, entity: 'LinkedEntities') -> None:
27        """ Copy all sub-entities ands SEQEND. (internal API) """
28        entity._sub_entities = [e.copy() for e in self._sub_entities]
29        if self.seqend:
30            entity.seqend = self.seqend.copy()
31
32    def link_entity(self, entity: 'DXFGraphic') -> None:
33        """ Link VERTEX ot ATTRIB entities. """
34        entity.set_owner(self.dxf.owner, self.dxf.paperspace)
35        self._sub_entities.append(entity)
36
37    def link_seqend(self, seqend: 'DXFEntity') -> None:
38        """ Link SEQEND entity. (internal API) """
39        seqend.dxf.owner = self.dxf.owner
40        self.seqend = seqend
41
42    def post_bind_hook(self):
43        """ Create always a SEQEND entity. """
44        if self.seqend is None:
45            self.new_seqend()
46
47    def all_sub_entities(self) -> Iterable['DXFEntity']:
48        """ Yields all sub-entities ans SEQEND. (internal API) """
49        yield from self._sub_entities
50        if self.seqend:
51            yield self.seqend
52
53    def process_sub_entities(self, func: Callable[['DXFEntity'], None]):
54        """ Call `func` for all sub-entities and SEQEND. (internal API)
55        """
56        for entity in self.all_sub_entities():
57            if entity.is_alive:
58                func(entity)
59
60    def add_sub_entities_to_entitydb(self, db: 'EntityDB') -> None:
61        """ Add sub-entities (VERTEX, ATTRIB, SEQEND) to entity database `db`,
62        called from EntityDB. (internal API)
63        """
64
65        def add(entity: 'DXFEntity'):
66            entity.doc = self.doc  # grant same document
67            db.add(entity)
68
69        if not self.seqend or not self.seqend.is_alive:
70            self.new_seqend()
71        self.process_sub_entities(add)
72
73    def new_seqend(self):
74        """ Create and bind new SEQEND. (internal API) """
75        attribs = {'layer': self.dxf.layer}
76        if self.doc:
77            seqend = factory.create_db_entry('SEQEND', attribs, self.doc)
78        else:
79            seqend = factory.new('SEQEND', attribs)
80        self.link_seqend(seqend)
81
82    def set_owner(self, owner: str, paperspace: int = 0):
83        """ Set owner of all sub-entities and SEQEND. (internal API) """
84        # Loading from file: POLYLINE/INSERT will be added to layout before
85        # vertices/attrib entities are linked, so set_owner() of POLYLINE does
86        # not set owner of vertices at loading time.
87        super().set_owner(owner, paperspace)
88
89        def set_owner(entity):
90            if isinstance(entity, DXFGraphic):
91                entity.set_owner(owner, paperspace)
92            else:  # SEQEND
93                entity.dxf.owner = owner
94
95        self.process_sub_entities(set_owner)
96
97    def remove_dependencies(self, other: 'Drawing' = None):
98        """ Remove all dependencies from current document to bind entity to
99        `other` document. (internal API)
100        """
101        self.process_sub_entities(lambda e: e.remove_dependencies(other))
102        super().remove_dependencies(other)
103
104    def destroy(self) -> None:
105        """ Destroy all data and references. """
106        if not self.is_alive:
107            return
108
109        self.process_sub_entities(func=lambda e: e.destroy())
110        del self._sub_entities
111        del self.seqend
112        super().destroy()
113
114
115# This attached MTEXT is a limited MTEXT entity, starting with (0, 'MTEXT')
116# therefore separated entity, but without the base class: no handle, no owner
117# nor AppData, and a limited AcDbEntity subclass.
118# Detect attached entities (more than MTEXT?) by required but missing handle and
119# owner tags use DXFEntity.link_entity() for linking to preceding entity,
120# INSERT & POLYLINE do not have attached entities, so reuse of API for
121# ATTRIB & ATTDEF should be safe.
122
123LINKED_ENTITIES = {
124    'INSERT': 'ATTRIB',
125    'POLYLINE': 'VERTEX'
126}
127
128
129def entity_linker() -> Callable[[DXFEntity], bool]:
130    """ Create an DXF entities linker. """
131    main_entity: Optional[DXFEntity] = None
132    prev: Optional[DXFEntity] = None
133    expected_dxftype = ""
134
135    def entity_linker_(entity: DXFEntity) -> bool:
136        """ Collect and link entities which are linked to a parent entity:
137
138        - VERTEX -> POLYLINE
139        - ATTRIB -> INSERT
140        - attached MTEXT entity
141
142        Args:
143             entity: examined DXF entity
144
145        Returns:
146             True if `entity` is linked to a parent entity
147
148        """
149        nonlocal main_entity, expected_dxftype, prev
150        dxftype: str = entity.dxftype()
151        # INSERT & POLYLINE are not linked entities, they are stored in the
152        # entity space.
153        are_linked_entities = False
154        if main_entity is not None:
155            # VERTEX, ATTRIB & SEQEND are linked tags, they are NOT stored in
156            # the entity space.
157            are_linked_entities = True
158            if dxftype == 'SEQEND':
159                main_entity.link_seqend(entity)
160                # Marks also the end of the main entity
161                main_entity = None
162            # Check for valid DXF structure:
163            #   VERTEX follows POLYLINE
164            #   ATTRIB follows INSERT
165            elif dxftype == expected_dxftype:
166                main_entity.link_entity(entity)
167            else:
168                raise const.DXFStructureError(
169                    f"Expected DXF entity {dxftype} or SEQEND"
170                )
171
172        elif dxftype in LINKED_ENTITIES:
173            # Only INSERT and POLYLINE have a linked entities structure:
174            if dxftype == 'INSERT' and not entity.dxf.get('attribs_follow', 0):
175                # INSERT must not have following ATTRIBS, ATTRIB can be a stand
176                # alone entity:
177                #
178                #   INSERT with no ATTRIBS, attribs_follow == 0
179                #   ATTRIB as stand alone entity
180                #   ....
181                #   INSERT with ATTRIBS, attribs_follow == 1
182                #   ATTRIB as connected entity
183                #   SEQEND
184                #
185                # Therefore a ATTRIB following an INSERT doesn't mean that
186                # these entities are linked.
187                pass
188            else:
189                main_entity = entity
190                expected_dxftype = LINKED_ENTITIES[dxftype]
191
192        # Attached MTEXT entity:
193        elif (dxftype == 'MTEXT') and (entity.dxf.handle is None):
194            if prev:
195                prev.link_entity(entity)
196                are_linked_entities = True
197            else:
198                raise const.DXFStructureError(
199                    "Found attached MTEXT entity without a preceding entity."
200                )
201        prev = entity
202        return are_linked_entities
203
204    return entity_linker_
205