import difflib
from typing import Optional

from ._tokenizer import Tokenizer
from ._template_script import TemplateScript
from ._files import read_file, write_file, FileName
from ._global_evaluation_context import GlobalEvaluationContext
from ._evaluator import evaluate_template_script


class PreProcessor:
    """Text pre-processor that separates macro code sections and
    text sections, generates a template script for them, executes
    this script, and uses the output as template expansion result.

    Results are cached / reused within the PreProcessor.

    :param tokenizer: Optionally, you can provide a non-standard tokenizer,
        e.g., one with a customized syntax.
    """

    def __init__(self, tokenizer: Optional[Tokenizer] = None) -> None:
        self._tokenizer = Tokenizer() if tokenizer is None else tokenizer
        # Tokenizer to be used by the PreProcessor. Currently, the same is
        # used for all files and for recursive evaluations the might start.
        # But this might change in the future.
        self._global_evaluation_context = GlobalEvaluationContext()
        # Context information for all template expansions happening under the
        # current PreProcessor.

    @staticmethod
    def diff(str1: str, str2: str, fromfile_txt: str, tofile_txt: str) -> str:
        """Compare two multi-line strings. If they are equal, return the empty string.
        Otherwise, return the result of the comparison in human-readable form.
        Currently, function *difflib.context_diff* is used for the comparison."""
        if str1 == str2:
            return ""
        return "".join(
            difflib.context_diff(
                str1.splitlines(keepends=True),
                str2.splitlines(keepends=True),
                fromfile_txt,
                tofile_txt,
            )
        )

    def template_script(
        self, template_file: FileName, trace_parsing: bool = False
    ) -> str:
        """
        Generate and return a template script for the template file.
        The method is typically only used for debugging purposes.

        :param template_file: Template to expand.
        :param trace_parsing: Print parsing log to stderr.
        """
        template = read_file(template_file)
        template_script = TemplateScript(
            str(template_file), template, self._tokenizer, trace_parsing
        )
        return str(template_script)

    def expand_file(
        self,
        template_file: FileName,
        diffs_to_template: bool = False,
        trace_parsing: bool = False,
        trace_evaluation: bool = False,
    ) -> str:
        """
        Load a template, expand it (recursive expansions may happen), and return the
        result.

        :param template_file: Template to expand.
        :param diffs_to_template: If True, the differences between the original
           template and the output generated by the macro expansion
           are returned, and the empty string if there are no differences.
        :param trace_parsing: Print parsing log to stderr.
        :param trace_evaluation: Print evaluation log to stderr.
        """
        template = read_file(template_file)
        template_script = TemplateScript(
            str(template_file),
            template,
            self._tokenizer,
            trace_parsing,
            trace_evaluation,
        )

        try:
            result = evaluate_template_script(
                template_script=template_script,
                tokenizer=self._tokenizer,
                global_evaluation_context=self._global_evaluation_context,
                already_imported_files=set[str](),
                globals_dict=None,
            )
        except Exception as exc:
            note = (
                f"Error occurred when expanding the following template: "
                f"  {str(template_file)}"
            )
            # Depending on the used Python version, one of the following will happen.
            if hasattr(exc, "add_note"):  # pragma: no cover
                exc.add_note(note)
                raise
            raise RuntimeError(note) from exc  # pragma: no cover

        return (
            self.diff(template, result, "template", "expansion result")
            if diffs_to_template
            else result
        )

    def expand_file_to_file(
        self,
        template_file: FileName,
        result_file: FileName,
        trace_parsing: bool = False,
        trace_evaluation: bool = False,
        diffs_to_result_file: bool = False,
    ) -> str:
        """Load a template file, expand it, and save the result to a file.

        :param template_file: Template to expand.
        :param result_file: File to store the results in.
        :param diffs_to_result_file: If True, nothing is written to the result_file.
           Instead, the differences between the content of the result_file
           and the output generated by the macro expansion are returned,
           or the empty string, if there are no differences.
        :param trace_parsing: Print parsing log to stderr.
        :param trace_evaluation: Print evaluation log to stderr.
        """
        result = self.expand_file(
            template_file,
            trace_parsing=trace_parsing,
            trace_evaluation=trace_evaluation,
        )
        if diffs_to_result_file:
            content = read_file(result_file)
            return self.diff(content, result, "current content", "expansion result")
        else:
            write_file(result_file, result)
            return ""
