""" Binary DOC file configuration provider for doc_file_processing """

from __future__ import annotations

from datetime import datetime
from typing import Any, Generator, TYPE_CHECKING
if TYPE_CHECKING: from _typeshed import StrPath

from docfile import DOCFile
from document import *


class DOCReader(Reader):
    """ Configuration reader implementation for binary DOC files """

    _doc: DOCFile

    def __init__(self, path: StrPath) -> None:
        self._doc = DOCFile(path)

    def parse(self) -> Document:
        return _Parser(self._doc).parse_document()
    
    def write_image(self, key: Any, dst: StrPath) -> None:
        data = self._doc.get_picture(key).data
        with open(dst, 'wb') as outfile:
            outfile.write(data)

    def timestamp(self) -> datetime:
        return self._doc.properties.ctime
    
    def is_config(self) -> bool:
        return self._doc.properties.template_name.startswith('ABB_ProjectDoc')


class MARK:
    """ DOC format special character definitions """
    EOF    = '\x00'
    OBJ    = '\x01'
    SEP1   = '\x03'
    SEP2   = '\x04'
    TAB    = '\x09'
    FF     = '\x0c'
    CELL   = '\x07'
    PARA   = '\x0D'
    FIELD_START = '\x13'
    FIELD_ALT   = '\x14'
    FIELD_END   = '\x15'

class _Parser:
    BUF_SIZE = 4096

    _src: DOCFile
    """ Document being parsed """

    _buf: str
    """ Parsing buffer """

    _buf_left: int
    """ Start index of buffer (relative to document) """

    _buf_right: int
    """ End index of buffer (relative to document) """

    _left: int
    """ Start index of next token (relative to document) """

    _right: int
    """ End index of next token (relative to document) """


    def __init__(self, src: DOCFile):
        self._left = 0
        self._right = 0
        self._buf_left = 0
        self._buf_right = self.BUF_SIZE
        self._src = src
        self._buf = self._src.read(self.BUF_SIZE)

    @property
    def _next(self) -> str:
        try:
            return self._buf[self._right - self._buf_left]
        except IndexError:
            return MARK.EOF
        
    def _pprop(self, propid: int) -> int | None:
        return self._src.get_pprop_at(self._right, propid)

    def _cprop(self, propid: int) -> int | None:
        return self._src.get_cprop_at(self._right, propid)
    
    def _advance(self) -> str:
        """ Advance `_right` and return the character added to the next token """
        c = self._buf[self._right - self._buf_left]
        self._right += 1

        if self._right >= self._buf_right:
            if self._buf_right + self.BUF_SIZE >= self._src.len_text:
                read_ct = self._src.len_text - self._buf_right
            else:
                read_ct = self.BUF_SIZE
            self._buf_right += read_ct
            self._buf += self._src.read(read_ct)

        return c
    
    def _consume(self) -> str:
        """ Slice off `_left:_right` as the next token and start a new token """
        s = self._buf[self._left - self._buf_left : self._right - self._buf_left]
        self._left = self._right

        if self._left >= self._buf_left + self.BUF_SIZE:
            self._buf_left += self.BUF_SIZE
            self._buf = self._buf[self.BUF_SIZE:]

        return s
    
    def _expect(self, x: str) -> str:
        """ Advance if the next character is `x`, otherwise raise an error """
        c = self._advance()
        if c != x: raise SyntaxError(f'expected {ord(x):02x}, found {ord(c):02x}')
        return self._consume()

    def parse_document(self) -> Document:
        """ Parse the document. """
        content = list(self.parse_content())
        if self._next not in [MARK.EOF, MARK.SEP1, MARK.SEP2]:
            raise SyntaxError(f"Fatal - unexpected character: 0x{ord(self._next):02x}")
        return Document(content)

    def parse_content(self, heading_limit: int = 0) -> Generator[Content, None, None]:
        """ Parse a stream of complex content (tables, paragraphs, figures, and/or subchapters).
        
        `heading_limit` sets the current heading level -- deeper headings will initiate a
        subchapter in the content stream, while higher level headings will end the content
        stream. """
        while True:
            if self._next == MARK.FIELD_START:  # Embedded field
                yield from self.parse_field()
            elif self._next == MARK.OBJ:  # Figure with following caption
                key = self._cprop(DOCFile.PROP_CHAR_EMBED)
                if key is None: raise SyntaxError(f"Failed to find source for figure")
                self._advance()
                self._consume()
                caption_text = self.parse_text_content().strip()
                if match := CAPTION_TEXT_RE.match(caption_text):
                    index = int(match['index'])
                    caption = match['name']
                else:
                    index = None
                    caption = caption_text
                yield Figure(index, caption, key)
            elif self._next in [MARK.PARA, MARK.FF]:  # Break
                self._advance()
                self._consume()
            elif self._next in [MARK.FIELD_ALT, MARK.FIELD_END, MARK.EOF, MARK.SEP1, MARK.SEP2]:  # Not valid content
                return
            elif ord(self._next) >= 0x20 or self._next == '\t' or self._next == MARK.CELL:  # Text
                if bool(self._pprop(DOCFile.PROP_PARA_IN_TABLE)):  # Table
                    yield self.parse_table()
                elif self._next == MARK.CELL:
                    raise SyntaxError(f"Cell mark outside table")
                elif (heading_level := self._pprop(DOCFile.PROP_PARA_HEADING_LEVEL)) is not None:  # Heading
                    if heading_level < heading_limit: return
                    heading_text = self.parse_plain_text()
                    content = list(self.parse_content(heading_limit=heading_level+1))
                    yield Chapter(heading_text, heading_level, content)
                else:  # Plain text
                    text = ''
                    while (self._pprop(DOCFile.PROP_PARA_HEADING_LEVEL) is None
                        and not bool(self._pprop(DOCFile.PROP_PARA_IN_TABLE))
                        and (more := self.parse_text_content())
                    ):
                        if self._next == MARK.OBJ:
                            # Figure with preceding caption
                            if text.strip('\n'): yield TextBlock(text.strip('\n'))
                            text = ''
                            key = self._cprop(DOCFile.PROP_CHAR_EMBED)
                            if key is None: raise SyntaxError(f"Failed to find source for figure")
                            self._advance()
                            self._consume()
                            if match := CAPTION_TEXT_RE.match(more.strip()):
                                index = int(match['index'])
                                caption = match['name']
                            else:
                                index = None
                                caption = caption_text
                            yield Figure(index, caption, key)
                            break
                        text += more
                        if self._next == MARK.PARA:
                            self._advance()
                            self._consume()
                        while self._next in [MARK.PARA, MARK.FF]:
                            self._advance()
                            self._consume()
                            text += '\n'
                    if text.strip('\n'): yield TextBlock(text.strip('\n'))
            else:
                raise SyntaxError(f"Unexpected control character: 0x{ord(self._next):02x}")
                
    def parse_table(self) -> Table:
        """ Parse a table, including a caption. """
        def _row() -> Generator[str, None, None]:
            while True:
                yield self.parse_text_content()
                if self._next not in [MARK.CELL, MARK.PARA]:
                    raise SyntaxError(f"Invalid table - unexpected character: 0x{ord(self._next):02x}")
                self._advance()
                self._consume()
                if bool(self._pprop(DOCFile.PROP_PARA_END_OF_ROW)):
                    self._advance()
                    self._consume()
                    return
        
        def _table() -> Generator[tuple[str, ...], None, None]:
            while self._pprop(DOCFile.PROP_PARA_IN_TABLE):
                yield tuple(_row())

        rows = list(_table())
        caption_text = self.parse_text_content().strip()
        headers = rows[0]
        data = tuple(rows[1:])
        
        if match := CAPTION_TEXT_RE.match(caption_text):
            index = int(match['index'])
            caption = match['name']
        else:
            index = None
            caption = caption_text

        return Table(index, caption, headers, data)
    
    def parse_text_content(self) -> str:
        """ Parse and join a stream of textual content, interpreting fields but stopping at any other non-text content. """
        in_table = bool(self._pprop(DOCFile.PROP_PARA_IN_TABLE))
        def _gen() -> Generator[str, None, None]:
            if self._next == MARK.PARA:
                self._advance()
                self._consume()
            while True:
                if self._next == MARK.FIELD_START:
                    yield from self.parse_text_field()
                elif self._next in [MARK.FIELD_ALT, MARK.FIELD_END, MARK.EOF, MARK.SEP1, MARK.SEP2, MARK.OBJ, MARK.CELL, MARK.PARA, MARK.FF]:
                    return
                elif ord(self._next) >= 0x20 or self._next == '\t':
                    if bool(self._pprop(DOCFile.PROP_PARA_IN_TABLE)) != in_table \
                        or (self._pprop(DOCFile.PROP_PARA_HEADING_LEVEL) is not None):
                        return
                    text = self.parse_plain_text()
                    if self._next == MARK.PARA:
                        text += '\n'
                    yield text
                else:
                    raise SyntaxError(f"Unexpected control character: 0x{ord(self._next):02x}")
        return ''.join(_gen())
    
    def skip_command(self) -> None:
        """ Advance past the command part of a field. """
        while True:
            if self._next == MARK.FIELD_START:
                for _ in self.parse_text_field(): pass
            elif self._next in [MARK.PARA, MARK.FF, MARK.OBJ, MARK.CELL, MARK.SEP1, MARK.SEP2]:
                self._advance()
                self._consume()
            elif self._next in [MARK.FIELD_ALT, MARK.FIELD_END, MARK.EOF]:
                return
            elif ord(self._next) >= 0x20 or self._next == '\t':
                self.parse_plain_text()
            else:
                raise SyntaxError(f"Unexpected control character: 0x{ord(self._next):02x}")

    def parse_plain_text(self) -> str:
        """ Consume a run of plain text characters. """
        while ord(self._next) >= 0x20 or self._next == '\t':
            self._advance()
        return self._consume()
    
    def parse_field(self) -> Generator[Content, None, None]:
        """ Interpret a field and yield complex content from its static value. """
        self._expect(MARK.FIELD_START)
        self.skip_command()
        self._expect(MARK.FIELD_ALT)
        yield from self.parse_content()
        self._expect(MARK.FIELD_END)
        
    def parse_text_field(self) -> Generator[str, None, None]:
        """ Interpret a field and yield textual content from its static value. """
        self._expect(MARK.FIELD_START)
        self.skip_command()
        self._expect(MARK.FIELD_ALT)
        yield from self.parse_text_content()
        self._expect(MARK.FIELD_END)
