'''
Module dedicated to the export of diagrams in the PlanUML syntax.

Bibliography:
- https://plantuml.com/fr/sequence-diagram: syntax of sequence diagrams
- https://plantuml.com/en/guide, https://plantuml.com/fr/guide: the PlanUML syntax books. Pages or sections of interest:
  - 24.6 Colors: color names
- https://plantuml-documentation.readthedocs.io/en/latest/formatting/all-skin-params.html: "All skin parameters available in PlantUML in alphabetical order."
'''

from contextlib import contextmanager
from re import Pattern
from re import compile as re_compile
from string import Formatter
from typing import Any, Iterator

from pydoctrace.domain.sequence import Call, Error
from pydoctrace.exporters import Exporter


class PlantUMLEscapeFormatter(Formatter):
    '''
    Formatter handling edge cases of the PlantUML syntax:
    - escape values having dunder ('__') substrings in them to prevent the PlantUML
      from interpreting them as underlining symbols of the Creole syntax.
      See https://plantuml-documentation.readthedocs.io/en/latest/formatting/creole.html#formatting-text
    - replace '@' by its unicode entity to prevent from breaking the PlantUML syntax
    '''

    DUNDER_REPLACE_PATTERN: Pattern = re_compile('__')
    AROBASE_REPLACE_PATTERN: Pattern = re_compile('@')
    UNICODE_AROBASE: str = '<U+0040>'

    def format_field(self, value: Any, format_spec: str) -> Any:
        if value is not None:
            # replaces '@' by '<U+0040>'
            value = self.AROBASE_REPLACE_PATTERN.sub(self.UNICODE_AROBASE, str(value))

        if format_spec == 'dunder':
            # escapes each text part of the given str value (splitted on '.') with '~'
            # when format_spec is 'dunder' to avoid PlantUML interpreting dunder symbols
            if isinstance(value, str):
                value = '.'.join((self.escape_dunder(part) for part in value.split('.')))

            # dunder formatting is done, cancel it for values that are not string
            format_spec = ''

        return super().format_field(value, format_spec)

    def escape_dunder(self, text: str):
        return self.DUNDER_REPLACE_PATTERN.sub('~__', text)


HEADER_TPL = r'''@startuml {diagram_name}
skinparam BoxPadding 10
skinparam ParticipantPadding 5
skinparam NoteBackgroundColor Cornsilk
skinparam NoteBorderColor Sienna
hide footbox
'''

TRACING_START_TPL = r'''
[o-> "{called.fq_module_text:dunder}\n{called.function_name:dunder}"
note right: line {called.line_index}
'''

CALL_START_TPL = r'''
"{caller.fq_module_text:dunder}\n{caller.function_name:dunder}" -> "{called.fq_module_text:dunder}\n{called.function_name:dunder}" ++
note left: line {caller.line_index}
note right: line {called.line_index}
'''

CALL_END_TPL = r'''
return {arg:dunder}
note right: line {called.line_index}
|||
'''

ERROR_PROPAGATION_TPL = r'''
"{error_caller.fq_module_text:dunder}\n{error_caller.function_name:dunder}" o<--x "{error_called.fq_module_text:dunder}\n{error_called.function_name:dunder}": ""{error.class_name}""\\n{error.message}
deactivate "{error_called.fq_module_text:dunder}\n{error_called.function_name:dunder}"
note right: line {error_called.line_index}
note left: line {error_caller.line_index}
'''

TRACING_END_TPL = r'''
[<-- "{called.fq_module_text:dunder}\n{called.function_name:dunder}": {arg:dunder}
note right: line {called.line_index}
'''

UNHANDLED_ERROR_END_TPL = r'''
[<-->x "{called.fq_module_text:dunder}\n{called.function_name:dunder}": ""{error.class_name}""\\n{error.message}
note right: line {called.line_index}
'''

FOOTER_TPL = r'''
footer Generated by //pydoctrace//
@enduml
'''


class PlantUMLExporter(Exporter):
    '''
    Exports the sequence diagram in the PlantUML format.
    '''

    fmt: PlantUMLEscapeFormatter = PlantUMLEscapeFormatter()

    def write_header(self, start_module: str, start_func_name: str):
        diagram_name = f'{start_module}.{start_func_name}'
        self.io_sink.write(self.fmt.format(HEADER_TPL, diagram_name=diagram_name))

    def write_tracing_start(self, called: Call):
        self.io_sink.write(self.fmt.format(TRACING_START_TPL, called=called))

    def write_start_call(self, caller: Call, called: Call):
        self.io_sink.write(self.fmt.format(CALL_START_TPL, caller=caller, called=called))

    def format_arg_value(self, arg: Any) -> str:
        if arg is None:
            return ''
        return arg

    def write_error_propagation(self, error_called: Call, error_caller: Call, error: Error):
        self.io_sink.write(
            self.fmt.format(ERROR_PROPAGATION_TPL, error_called=error_called, error_caller=error_caller, error=error)
        )

    def write_return(self, called: Call, arg: Any, **kwargs):
        self.io_sink.write(self.fmt.format(CALL_END_TPL, called=called, arg=self.format_arg_value(arg)))

    def write_tracing_end(self, called: Call, arg: Any):
        self.io_sink.write(self.fmt.format(TRACING_END_TPL, called=called, arg=self.format_arg_value(arg)))

    def write_unhandled_error_end(self, called: Call, error: Error):
        self.io_sink.write(self.fmt.format(UNHANDLED_ERROR_END_TPL, called=called, error=error))

    def write_footer(self):
        self.io_sink.write(FOOTER_TPL)

    @staticmethod
    @contextmanager
    def export_manager_manager(export_file_path: str) -> Iterator[Exporter]:
        with open(export_file_path, 'w', encoding='utf8') as diagram_file:
            exporter = PlantUMLExporter(diagram_file)
            yield exporter
