""" Configuration content parser for doc_file_processing """

from __future__ import annotations

import re
from dataclasses import dataclass, field
from datetime import datetime
from pathlib import Path
from typing import List, Literal, Optional

from document import *
from logutil import *


def _list(): return field(default_factory=list)

CONTROLLER_NAME_RE = re.compile(r'^(?P<name>\w+)( \(.*\))? .*')
HW_NAME_RE = re.compile(r'\s*(?P<index>\d+).*?\s{2,}(?P<description>.*)')
MODULE_NAME_RE = re.compile(r'^(?P<type>[^-]+?) - (?P<appl>\w+)\.(?P<name>[^\n]+)')
TABLE_CONTROLLER_NAME_RE = re.compile(r'^(?P<controllername>\w+)( \(.*\))?(?P<path>(\.\d+)*) .*')


@dataclass
class Configuration:
    """ Configuration content of a document """
    doc_title: str
    doc_source_path: Path
    doc_timestamp: datetime
    applications: List['Application'] = _list()
    controllers: List['Controller'] = _list()
    libraries: List['Library'] = _list()
    loose_diagrams: List['Module'] = _list()
    loose_programs: List['Module'] = _list()
    loose_scms: List['SCM'] = _list()
    loose_hardware_chapters: List['HardwareChapter'] = _list()
    tables: List[Table] = _list()
    figures: List[Figure] = _list()

@dataclass
class Application:
    name: str
    header: str
    diagrams: List['Module'] = _list()
    programs: List['Module'] = _list()
    types: List['TypeDefinition'] = _list()
    cmis: List['CMI'] = _list()
    scms: List['SCM'] = _list()
    tables: List[Table] = _list()
    figures: List[Figure] = _list()

@dataclass
class Module:
    name: str
    scope_name: str
    header: str
    cmis: List['CMI'] = _list()
    scms: List['SCM'] = _list()
    tables: List[Table] = _list()
    figures: List[Figure] = _list()
    code_blocks: List['CodeBlock'] = _list()

    def is_configured(self) -> bool:
        return any((self.figures, self.code_blocks, self.cmis, self.scms, self.tables))

@dataclass
class TypeDefinition:
    name: str
    scope_name: str
    params_table: Optional[Table]
    xparams_table: Optional[Table]
    module: Module

    def __init__(self, module: Module) -> None:
        self.name = module.name
        self.scope_name = module.scope_name
        self.params_table = None
        self.xparams_table = None
        self.module = module

        tables = module.tables
        module.tables = []
        for t in tables:
            if t.caption.endswith('(Parameters)') and not self.params_table:
                self.params_table = t
            elif t.caption.endswith('(Extensible parameters)') and not self.xparams_table:
                self.xparams_table = t
            else:
                module.tables.append(t)

    def is_description_only(self) -> bool:
        return not self.module.is_configured()

@dataclass
class CodeBlock:
    name: str
    tables: List[Table] = _list()
    figures: List[Figure] = _list()
    text_blocks: List[str] = _list()

@dataclass
class CMI:  # Control Module instance ("Control Module Type" chapter)
    name: str
    header: str
    type_name: str
    connections: Table
    tables: List[Table] = _list()  # doesn't include "Connections" table
    figures: List[Figure] = _list()

@dataclass
class SCM:  # Single Control Module
    name: str
    scope_name: str
    header: str
    scms: List['SCM'] = _list()
    cmis: List['CMI'] = _list()
    code_blocks: List['CodeBlock'] = _list()
    tables: List[Table] = _list()
    figures: List[Figure] = _list()

@dataclass
class Controller:
    name: str
    header: str
    hardware_chapters: List['HardwareChapter'] = _list()
    tables: List[Table] = _list()
    figures: List[Figure] = _list()

@dataclass
class HardwareChapter:
    name: str
    controller_name: str
    index_path: List[int]
    channel_tables: List[Table] = _list()
    other_tables: List[Table] = _list()
    sections: List['HardwareChapter'] = _list()

@dataclass
class Library:
    name: str
    types: List['TypeDefinition'] = _list()


def process_doc(doc: Document, title: str, source_path: Path, timestamp: datetime) -> Configuration:
    config = Configuration(
        doc_title=title,
        doc_source_path=source_path,
        doc_timestamp=timestamp)

    contents = doc.children
    while isinstance(contents[0], TextBlock):
        # Skip table of contents if present
        contents = contents[1:]

    for node in contents:
        if isinstance(node, Chapter):
            add_top_level_chapter(config, node)
        elif isinstance(node, Figure):
            config.figures.append(node)
        elif isinstance(node, Table):
            config.tables.append(node)
        else:
            wrnmsg(f'    Ignoring unexpected top-level element {node}')

    return config

def add_top_level_chapter(config: Configuration, chapter: Chapter) -> None:
    try:
        if chapter.title.startswith('Hardware ') or HW_NAME_RE.match(chapter.title):
            result = process_hardware_chapter(chapter)
            config.loose_hardware_chapters.append(result)
        elif chapter.title == 'Compiler switches':
            assert(isinstance(chapter.children[0], Table))
            config.tables.append(chapter.children[0])
        elif chapter.title == 'Programs' or chapter.title == 'Diagrams':
            for subchapter in chapter.children:
                if isinstance(subchapter, Chapter):
                    add_top_level_chapter(config, subchapter)
                else:
                    wrnmsg(f'    Ignoring unexpected element in top-level chapter: {subchapter}')
        elif chapter.title == 'Project constants':
            wrnmsg('    Document contains Project Constants chapter - this will not be exported')
        elif chapter.tag == 'Application':
            result = process_application_chapter(chapter)
            config.applications.append(result)
        elif chapter.tag == 'Diagram':
            result = process_module_chapter(chapter)
            config.loose_diagrams.append(result)
        elif chapter.tag == 'Program':
            result = process_module_chapter(chapter)
            config.loose_programs.append(result)
        elif chapter.tag == 'Single Control Module':
            result = process_scm_chapter(chapter)
            config.loose_scms.append(result)
        elif chapter.tag == 'Controller':
            result = process_controller_chapter(chapter)
            config.controllers.append(result)
        elif chapter.tag == 'Library':
            result = process_library_chapter(chapter)
            config.libraries.append(result)
        else:
            wrnmsg(f'    Skipping chapter {chapter.describe()} - unknown type')
    except Exception:
        errmsg(f'    Skipping chapter {chapter.describe()} - error in processing:')
        errexc()

def process_application_chapter(chapter: Chapter) -> Application:
    name = chapter.title.split('-', 1)[0].strip()

    assert(isinstance(chapter.children[0], TextBlock))
    header = chapter.title + '\n\n' + chapter.children[0].text

    result = Application(name, header)
    debugmsg(f"  - Application {result.name}")

    for node in chapter.children[1:]:
        if isinstance(node, Chapter):
            if node.title == 'Connected Libraries':
                result.header += process_list_chapter(node)
            elif node.title == 'Diagrams':
                add_modules_from_chapter(result, node, 'diagrams')
            elif node.title == 'Programs':
                add_modules_from_chapter(result, node, 'programs')
            elif node.title == 'Control Modules':
                add_modules_from_chapter(result, node, 'cms')
            elif node.title == 'Function Block Types':
                add_modules_from_chapter(result, node, 'types')
            elif node.title == 'Data Types':
                wrnmsg('    Application contains Data Types subchapter - this will not be exported')
            else:
                wrnmsg(f'    Skipping application subchapter {node.describe()} - unknown type')
        elif isinstance(node, Figure):
            result.figures.append(node)
        elif isinstance(node, Table):
            result.tables.append(node)
        else:
            wrnmsg(f'    Ignoring unexpected element in application: {node}')

    return result

def process_list_chapter(chapter: Chapter) -> str:
    s = '\n\n' + chapter.title

    for node in chapter.children:
        if isinstance(node, Chapter):
            s += '\n- ' + node.title
        else:
            wrnmsg(f'    Ignoring unexpected element in list chapter: {node}')

    return s

def add_modules_from_chapter(application: Application, chapter: Chapter, type: Literal['diagrams', 'programs', 'cms', 'types']) -> None:
    contents = chapter.children
    if (isinstance(contents[0], TextBlock)): contents = contents[1:]

    for node in contents:
        if isinstance(node, Table):
            application.tables.append(node)
        elif isinstance(node, Figure):
            application.figures.append(node)
        elif isinstance(node, Chapter):
            if type == 'diagrams':
                module = process_module_chapter(node)
                application.diagrams.append(module)
            elif type == 'programs':
                module = process_module_chapter(node)
                application.programs.append(module)
            elif type == 'cms':
                if node.tag == 'Single Control Module':
                    module = process_scm_chapter(node)
                    application.scms.append(module)
                elif node.tag == 'Control Module Type':
                    module = process_cmi_chapter(node)
                    application.cmis.append(module)
                else:
                    wrnmsg(f'    Skipping control module subchapter {node.describe()} - unknown type')
            elif type == 'types':
                module = process_module_chapter(node, header_skip_title=True)
                application.types.append(TypeDefinition(module))
            else:
                raise ValueError()
        else:
            wrnmsg(f'    Ignoring unexpected element in application subchapter: {node}')

def process_module_chapter(chapter: Chapter, *, header_skip_title=False) -> Module:
    assert(isinstance(chapter.children[0], TextBlock))
    if not header_skip_title:
        header = chapter.title + '\n\n' + chapter.children[0].text
    else:
        header = chapter.children[0].text

    if name_match := MODULE_NAME_RE.match(chapter.children[0].text):
        scope_name = name_match['appl']
        name = name_match['name']
        debugmsg(f"  - Module {scope_name}.{name}")
    else:
        wrnmsg(f'    Could not parse module name: {chapter.children[0].text}')
        name = chapter.children[0].text
        scope_name = ''

    result = Module(name, scope_name, header)

    for node in chapter.children[1:]:
        if isinstance(node, Table):
            result.tables.append(node)
        elif isinstance(node, Figure):
            result.figures.append(node)
        elif isinstance(node, Chapter):
            if node.tag == 'Single Control Module':
                module = process_scm_chapter(node)
                result.scms.append(module)
            elif node.tag == 'Control Module Type':
                module = process_cmi_chapter(node)
                result.cmis.append(module)
            else:
                cb = process_code_block_chapter(node)
                result.code_blocks.append(cb)
        else:
            wrnmsg(f'    Ignoring unexpected element in module: {node}')

    return result

def process_code_block_chapter(chapter: Chapter) -> CodeBlock:
    result = CodeBlock(chapter.title)
    debugmsg(f"  - Code block {result.name}")

    for node in chapter.children:
        if isinstance(node, Table):
            result.tables.append(node)
        elif isinstance(node, Figure):
            result.figures.append(node)
        elif isinstance(node, TextBlock):
            result.text_blocks.append(node.text)
        else:
            wrnmsg(f'    Ignoring unexpected element in code block: {node}')

    return result

def process_scm_chapter(chapter: Chapter) -> SCM:
    assert(isinstance(chapter.children[0], TextBlock))
    header = chapter.title + '\n\n' + chapter.children[0].text

    if name_match := MODULE_NAME_RE.match(chapter.children[0].text):
        scope_name = name_match['appl']
        name = name_match['name']
        debugmsg(f"  - Single control module {name}")
    else:
        wrnmsg(f'    Could not parse SCM name: {chapter.children[0].text}')
        name = chapter.children[0].text
        scope_name = ''

    result = SCM(name, scope_name, header)

    for node in chapter.children[1:]:
        if isinstance(node, Table):
            result.tables.append(node)
        elif isinstance(node, Figure):
            result.figures.append(node)
        elif isinstance(node, Chapter):
            submodule: object
            if node.children and isinstance(node.children[0], TextBlock) and node.children[0].text.startswith('Single Control Module -'):
                submodule = process_scm_chapter(node)
                result.scms.append(submodule)
            elif node.children and isinstance(node.children[0], TextBlock) and node.children[0].text.startswith('Control Module Type -'):
                submodule = process_cmi_chapter(node)
                result.cmis.append(submodule)
            else:
                submodule = process_code_block_chapter(node)
                result.code_blocks.append(submodule)
        else:
            wrnmsg(f'    Ignoring unexpected element in single control module: {node}')

    return result

def process_cmi_chapter(chapter: Chapter) -> CMI:
    name = chapter.title.split()[0]
    debugmsg(f"  - Control module instance {name}")

    assert(isinstance(chapter.children[0], TextBlock))
    header = chapter.title + '\n\n' + chapter.children[0].text
    type_name = chapter.children[0].text.split('\n')[0].split('-')[1].strip()

    connections_table: Optional[Table] = None
    tables: List[Table] = []
    for node in chapter.children[1:]:
        if isinstance(node, Table):
            if node.caption.startswith('Connections'):
                assert(not connections_table)
                connections_table = node
            else:
                tables.append(node)
        else:
            wrnmsg(f'    Ignoring unexpected element in control module instance: {node}')

    if not connections_table:
        wrnmsg(f'    Incomplete control module instance {name}?')
        assert(connections_table)
    return CMI(name, header, type_name, connections_table, tables)

def process_controller_chapter(chapter: Chapter) -> Controller:
    if name_match := CONTROLLER_NAME_RE.match(chapter.title):
        name = name_match['name']
        debugmsg(f"  - Controller {name}")
    else:
        wrnmsg(f'    Could not parse controller name: {chapter.title}')
        name = chapter.title

    assert(isinstance(chapter.children[0], TextBlock))
    header = chapter.title + '\n\n' + chapter.children[0].text

    result = Controller(name, header)

    for node in chapter.children[1:]:
        if isinstance(node, Chapter):
            if node.title == 'Connected Libraries' or node.title == 'Connected Applications':
                result.header += process_list_chapter(node)
            elif node.title in ['Tasks', 'Access Variables']:
                wrnmsg(f'    Hardware chapter contains {node.title} subchapter - this will not be exported')
            elif node.title == 'Controller Settings':
                for subnode in node.children:
                    if isinstance(subnode, TextBlock):
                        result.header += '\n' + subnode.text
                    elif isinstance(subnode, Table):
                        result.tables.append(subnode)
                    else:
                        wrnmsg(f'    Ignoring unexpected element in Controller Settings chapter: {subnode}')
            elif node.title.startswith('Hardware '):
                hwc = process_hardware_chapter(node)
                result.hardware_chapters.append(hwc)
            else:
                wrnmsg(f'    Skipping subchapter {node.describe()} - unknown type')
        elif isinstance(node, Figure):
            result.figures.append(node)
        elif isinstance(node, Table):
            result.tables.append(node)
        else:
            wrnmsg(f'    Ignoring unexpected element in controller: {node}')

    return result

def process_hardware_chapter(chapter: Chapter, parent_index_path: Optional[List[int]] = None) -> HardwareChapter:
    if chapter.title.startswith('Hardware '):
        title = chapter.title[len('Hardware '):]
    else:
        title = chapter.title

    match = HW_NAME_RE.match(title)
    if match:
        name = match['description'].strip()
        index_path = [*(parent_index_path or []), int(match['index'])]
    else:
        name = title.strip()
        index_path = []
    debugmsg(f"  - Hardware chapter {name}")

    # don't know controller name yet, so leave blank
    result = HardwareChapter(name, '', index_path)

    for node in chapter.children:
        if isinstance(node, Table):
            if (node.caption.endswith('(Connections)')
                or node.caption.endswith('(Properties)')
                or node.caption.endswith('(Status)')
            ):
                result.channel_tables.append(node)
            else:
                result.other_tables.append(node)

            if (not result.controller_name) or (not parent_index_path):
                match = TABLE_CONTROLLER_NAME_RE.match(node.caption)

                if not result.controller_name:
                    # no header, so try to get the controller name from a table caption
                    if match: result.controller_name = match['controllername']

                if not parent_index_path:
                    if match: result.index_path = [int(i) for i in match['path'][1:].split('.')] if match['path'] else []

        elif isinstance(node, Chapter):
            subchapter = process_hardware_chapter(node, result.index_path)
            result.sections.append(subchapter)
        else:
            wrnmsg(f'    Ignoring unexpected element in hardware chapter: {node}')

    return result

def process_library_chapter(chapter: Chapter) -> Library:
    result = Library(chapter.title)
    debugmsg(f"  - Library {result.name}")

    assert(isinstance(chapter.children[0], TextBlock))

    for node in chapter.children[1:]:
        if isinstance(node, Chapter):
            if node.title == 'Function Block Types':
                for subnode in node.children:
                    if isinstance(subnode, Chapter):
                        subrst = process_module_chapter(subnode, header_skip_title=True)
                        result.types.append(TypeDefinition(subrst))
                    else:
                        wrnmsg(f'    Ignoring unexpected element in Function Block Types chapter: {node}')
            else:
                wrnmsg(f'    Skipping subchapter {node.describe()} - unknown type')
        else:
            wrnmsg(f'    Ignoring unexpected element in library chapter: {node}')

    return result
