# Copyright (c) 2020, Manfred Moitzi
# License: MIT License
from typing import Tuple, List, Iterable
from collections import namedtuple
from .const import DXFStructureError
from ezdxf.tools.codepage import toencoding

IndexEntry = namedtuple('IndexEntry', field_names='code value location line')


class FileStructure:
    """
    DXF file structure representation stored as file locations.

    Store all DXF structure tags and some other tags as :class:`IndexEntry` tuples:

        - code: group code
        - value: tag value as string
        - location: file location as int
        - line: line number as int

    Indexed tags:

        - structure tags, every tag with group code 0
        - section names, (2, name) tag following a (0, SECTION) tag
        - entity handle tags with group code 5, the DIMSTYLE handle group code 105
          is also stored as group code 5

    """

    def __init__(self, filename: str):
        # stores the file system name of the DXF document.
        self.filename = filename
        # DXF version if header variable $ACADVER is present, default is DXFR12
        self.version = 'AC1009'
        # Python encoding required to read the DXF document as text file.
        self.encoding = 'cp1252'
        self.index: List[IndexEntry] = []

    def print(self):
        print(f'Filename: {self.filename}')
        print(f'DXF Version: {self.version}')
        print(f'encoding: {self.encoding}')
        for entry in self.index:
            print(f'Line: {entry.line} - ({entry.code}, {entry.value})')

    def get(self, code: int, value: str, start: int = 0) -> int:
        """ Returns index of first entry matching `code` and `value`. """
        self_index = self.index
        index = start
        count = len(self_index)
        while index < count:
            entry = self_index[index]
            if entry.code == code and entry.value == value:
                return index
            index += 1
        raise ValueError(f'No entry for tag ({code}, {value}) found.')

    def fetchall(self, code: int, value: str, start: int = 0) -> Iterable[IndexEntry]:
        """ Iterate over all specified entities.

        e.g. fetchall(0, 'LINE') returns an iterator for all LINE entities.

        """
        for entry in self.index[start:]:
            if entry.code == code and entry.value == value:
                yield entry


def load(filename: str) -> FileStructure:
    """
    Load DXF file structure for file `filename`, the file has to be seekable.

    Args:
        filename: file system file name

    Raises:
        DXFStructureError: Invalid or incomplete DXF file.

    """
    file_structure = FileStructure(filename)
    file = open(filename, mode='rb')
    line: int = 1
    eof = False
    header = False
    index: List[IndexEntry] = []
    prev_code: int = -1
    prev_value: bytes = b''
    structure = None  # the actual structure tag: 'SECTION', 'LINE', ...

    def load_tag() -> Tuple[int, bytes]:
        nonlocal line
        try:
            code = int(file.readline())
        except ValueError:
            raise DXFStructureError(f'Invalid group code in line {line}')

        if code < 0 or code > 1071:
            raise DXFStructureError(f'Invalid group code {code} in line {line}')
        value = file.readline().rstrip(b'\r\n')
        line += 2
        return code, value

    def load_header_var() -> str:
        _, value = load_tag()
        return value.decode()

    while not eof:
        location = file.tell()
        tag_line = line
        try:
            code, value = load_tag()
            if header and code == 9:
                if value == b'$ACADVER':
                    file_structure.version = load_header_var()
                elif value == b'$DWGCODEPAGE':
                    file_structure.encoding = toencoding(load_header_var())
                continue
        except IOError:
            break

        if code == 0:
            # All structure tags have group code == 0, store file location
            structure = value
            index.append(IndexEntry(0, value.decode(), location, tag_line))
            eof = (value == b'EOF')

        elif code == 2 and prev_code == 0 and prev_value == b'SECTION':
            # Section name is the tag (2, name) following the (0, SECTION) tag.
            header = (value == b'HEADER')
            index.append(IndexEntry(2, value.decode(), location, tag_line))

        elif code == 5 and structure != b'DIMSTYLE':
            # Entity handles have always group code 5.
            index.append(IndexEntry(5, value.decode(), location, tag_line))

        elif code == 105 and structure == b'DIMSTYLE':
            # Except the DIMSTYLE table entry has group code 105.
            index.append(IndexEntry(5, value.decode(), location, tag_line))

        prev_code = code
        prev_value = value

    file.close()
    if not eof:
        raise DXFStructureError(f'Unexpected end of file.')

    if file_structure.version >= 'AC1021':  # R2007 and later
        file_structure.encoding = 'utf-8'
    file_structure.index = index
    return file_structure
