1# Copyright (c) 2020, Manfred Moitzi
2# License: MIT License
3from typing import Tuple, List, Iterable
4from collections import namedtuple
5from .const import DXFStructureError
6from ezdxf.tools.codepage import toencoding
7
8IndexEntry = namedtuple('IndexEntry', field_names='code value location line')
9
10
11class FileStructure:
12    """
13    DXF file structure representation stored as file locations.
14
15    Store all DXF structure tags and some other tags as :class:`IndexEntry` tuples:
16
17        - code: group code
18        - value: tag value as string
19        - location: file location as int
20        - line: line number as int
21
22    Indexed tags:
23
24        - structure tags, every tag with group code 0
25        - section names, (2, name) tag following a (0, SECTION) tag
26        - entity handle tags with group code 5, the DIMSTYLE handle group code 105
27          is also stored as group code 5
28
29    """
30
31    def __init__(self, filename: str):
32        # stores the file system name of the DXF document.
33        self.filename = filename
34        # DXF version if header variable $ACADVER is present, default is DXFR12
35        self.version = 'AC1009'
36        # Python encoding required to read the DXF document as text file.
37        self.encoding = 'cp1252'
38        self.index: List[IndexEntry] = []
39
40    def print(self):
41        print(f'Filename: {self.filename}')
42        print(f'DXF Version: {self.version}')
43        print(f'encoding: {self.encoding}')
44        for entry in self.index:
45            print(f'Line: {entry.line} - ({entry.code}, {entry.value})')
46
47    def get(self, code: int, value: str, start: int = 0) -> int:
48        """ Returns index of first entry matching `code` and `value`. """
49        self_index = self.index
50        index = start
51        count = len(self_index)
52        while index < count:
53            entry = self_index[index]
54            if entry.code == code and entry.value == value:
55                return index
56            index += 1
57        raise ValueError(f'No entry for tag ({code}, {value}) found.')
58
59    def fetchall(self, code: int, value: str, start: int = 0) -> Iterable[IndexEntry]:
60        """ Iterate over all specified entities.
61
62        e.g. fetchall(0, 'LINE') returns an iterator for all LINE entities.
63
64        """
65        for entry in self.index[start:]:
66            if entry.code == code and entry.value == value:
67                yield entry
68
69
70def load(filename: str) -> FileStructure:
71    """
72    Load DXF file structure for file `filename`, the file has to be seekable.
73
74    Args:
75        filename: file system file name
76
77    Raises:
78        DXFStructureError: Invalid or incomplete DXF file.
79
80    """
81    file_structure = FileStructure(filename)
82    file = open(filename, mode='rb')
83    line: int = 1
84    eof = False
85    header = False
86    index: List[IndexEntry] = []
87    prev_code: int = -1
88    prev_value: bytes = b''
89    structure = None  # the actual structure tag: 'SECTION', 'LINE', ...
90
91    def load_tag() -> Tuple[int, bytes]:
92        nonlocal line
93        try:
94            code = int(file.readline())
95        except ValueError:
96            raise DXFStructureError(f'Invalid group code in line {line}')
97
98        if code < 0 or code > 1071:
99            raise DXFStructureError(f'Invalid group code {code} in line {line}')
100        value = file.readline().rstrip(b'\r\n')
101        line += 2
102        return code, value
103
104    def load_header_var() -> str:
105        _, value = load_tag()
106        return value.decode()
107
108    while not eof:
109        location = file.tell()
110        tag_line = line
111        try:
112            code, value = load_tag()
113            if header and code == 9:
114                if value == b'$ACADVER':
115                    file_structure.version = load_header_var()
116                elif value == b'$DWGCODEPAGE':
117                    file_structure.encoding = toencoding(load_header_var())
118                continue
119        except IOError:
120            break
121
122        if code == 0:
123            # All structure tags have group code == 0, store file location
124            structure = value
125            index.append(IndexEntry(0, value.decode(), location, tag_line))
126            eof = (value == b'EOF')
127
128        elif code == 2 and prev_code == 0 and prev_value == b'SECTION':
129            # Section name is the tag (2, name) following the (0, SECTION) tag.
130            header = (value == b'HEADER')
131            index.append(IndexEntry(2, value.decode(), location, tag_line))
132
133        elif code == 5 and structure != b'DIMSTYLE':
134            # Entity handles have always group code 5.
135            index.append(IndexEntry(5, value.decode(), location, tag_line))
136
137        elif code == 105 and structure == b'DIMSTYLE':
138            # Except the DIMSTYLE table entry has group code 105.
139            index.append(IndexEntry(5, value.decode(), location, tag_line))
140
141        prev_code = code
142        prev_value = value
143
144    file.close()
145    if not eof:
146        raise DXFStructureError(f'Unexpected end of file.')
147
148    if file_structure.version >= 'AC1021':  # R2007 and later
149        file_structure.encoding = 'utf-8'
150    file_structure.index = index
151    return file_structure
152