# -*- coding: utf-8 -*-
# Authors : Alix Chagué <alix.chague@inria.fr>
#           Lucas Terriel <lucas.terriel@inria.fr>
# Licence : MIT
"""Client interface — calls several sub-systems of Cthulhu."""

import os
from typing import List, Union

from cthulhu.parser import parser_text, parser_xml
from cthulhu.preprocessing.transformation import (
    RemoveDigits,
    ToLowerCase,
    ToUpperCase,
    RemovePunctuation,
    RemoveDiacritics,
    _Composer,
    count_diacritics,
)
from cthulhu.metrics.evaluation import Scorer


def _parse_input(source: str) -> str:
    """Resolve a single input to a plain string.

    Accepts:
    - a path to a ``.xml`` file → parsed with :class:`_XMLParser`
    - a path to any other text file → read with :class:`_TextParser`
    - a plain string → returned as-is
    """
    if isinstance(source, str) and source.endswith(".xml") and os.path.isfile(source):
        return parser_xml._XMLParser(source).content
    if isinstance(source, str) and os.path.isfile(source):
        return parser_text._TextParser(source).text
    return source


class Cthulhu:
    """Facade class providing a simple interface to evaluate HTR/OCR transcriptions.

    Orchestrates optional text preprocessing and metric computation between a
    reference (ground truth) and a prediction.

    Parameters
    ----------
    data : list[str]
        A list of exactly two elements. Each element can be:

        - a plain string (the text itself),
        - a path to a plain-text file (``.txt``),
        - a path to an ALTO or PAGE-XML file (``.xml``).

    apply_transforms : str, optional
        A string of codes selecting which text normalisations to apply
        **before** computing metrics. Each normalisation is computed
        independently *and* all together.

        ====  ========================
        Code  Transformation
        ====  ========================
        D     Remove digits
        U     Convert to uppercase
        L     Convert to lowercase
        P     Remove punctuation
        X     Remove diacritics
        ====  ========================

        Defaults to ``""`` (no transformation).
    verbosity : bool, optional
        Print progress messages. Defaults to ``False``.
    insertion_cost : float, optional
        Weight for insertion errors. Defaults to ``1.0``.
    substitution_cost : float, optional
        Weight for substitution errors. Defaults to ``1.0``.
    deletion_cost : float, optional
        Weight for deletion errors. Defaults to ``1.0``.
    truncate : bool, optional
        Truncate floating-point scores. Defaults to ``False``.
    percent : bool, optional
        Express rate metrics as percentages. Defaults to ``False``.
    round_digits : str, optional
        Decimal precision for truncation (e.g. ``'.01'``). Defaults to ``'.01'``.

    Attributes
    ----------
    reference : str
        Ground-truth text (after input resolution).
    prediction : str
        Prediction text (after input resolution).
    scores : Scorer
        Cthulhu :class:`Scorer` object containing all computed metrics.
    reference_preprocess : str
        Ground-truth text after all transforms are applied (if any).
    prediction_preprocess : str
        Prediction text after all transforms are applied (if any).
    """

    # Map user codes to (transform_instance, board_key_name)
    _CODES_TRANSFORMS = {
        "D": (RemoveDigits, "non_digits"),
        "U": (ToUpperCase, "uppercase"),
        "L": (ToLowerCase, "lowercase"),
        "P": (RemovePunctuation, "remove_punctuation"),
        "X": (RemoveDiacritics, "remove_diacritics"),
    }

    def __init__(
        self,
        data: List[str],
        apply_transforms: str = "",
        verbosity: bool = False,
        insertion_cost: float = 1.0,
        substitution_cost: float = 1.0,
        deletion_cost: float = 1.0,
        truncate: bool = False,
        percent: bool = False,
        round_digits: str = ".01",
    ) -> None:

        if not (isinstance(data, list) and len(data) >= 2):
            raise ValueError(
                "data must be a list of at least two elements "
                "(strings, text files, or XML files)."
            )

        # Resolve inputs to plain strings
        self.reference: str = _parse_input(data[0])
        self.prediction: str = _parse_input(data[1])

        # Preprocessing options
        self.apply_transforms: List[str] = list(apply_transforms.replace(" ", ""))

        # Debug
        self.verbosity = verbosity

        # Score weights
        self._insertion_cost = insertion_cost
        self._substitution_cost = substitution_cost
        self._deletion_cost = deletion_cost

        # Display options
        self.truncate = truncate
        self.percent = percent
        self.round_digits = round_digits

        # Preprocessed sequences (populated below if transforms requested)
        self.reference_preprocess: str = ""
        self.prediction_preprocess: str = ""

        # Base scores (on raw sequences)
        self.scores = self._build_scorer(self.reference, self.prediction)

        # Apply transformations if requested
        if apply_transforms:
            self.scores.board = self._apply_transforms(apply_transforms)

    # ------------------------------------------------------------------
    # Private helpers
    # ------------------------------------------------------------------

    def _build_scorer(self, ref: str, pred: str) -> Scorer:
        return Scorer(
            ref,
            pred,
            insertion_cost=self._insertion_cost,
            deletion_cost=self._deletion_cost,
            substitution_cost=self._substitution_cost,
            truncate_score=self.truncate,
            show_percent=self.percent,
            round_digits=self.round_digits,
        )

    def _compute_state_transformations(self, transform_instance) -> Scorer:
        """Return a Scorer computed after applying a single transformation."""
        transformed = transform_instance([self.reference, self.prediction])
        return self._build_scorer(transformed[0], transformed[1])

    def _compute_all_transformations(self, instances: list):
        """Return (Scorer, [ref, pred]) after applying all transformations at once."""
        composed = _Composer(instances)([self.reference, self.prediction])
        return self._build_scorer(composed[0], composed[1]), composed

    def _apply_transforms(self, apply_transforms: str) -> dict:
        """Build the extended board dict with per-transform and combined scores."""
        new_score: dict = {"default": self.scores.board}
        to_compose = []

        for code, (transform_cls, label) in self._CODES_TRANSFORMS.items():
            if code in apply_transforms:
                instance = transform_cls()
                to_compose.append(instance)
                new_score[label] = self._compute_state_transformations(instance).board

        all_scorer, composed_seqs = self._compute_all_transformations(to_compose)
        new_score["all_transforms"] = all_scorer.board

        self.reference_preprocess = composed_seqs[0]
        self.prediction_preprocess = composed_seqs[1]

        # Length statistics
        new_score["Length_reference"] = len(self.reference)
        new_score["Length_prediction"] = len(self.prediction)
        new_score["Length_reference_transformed"] = len(self.reference_preprocess)
        new_score["Length_prediction_transformed"] = len(self.prediction_preprocess)

        # Characters removed (relevant for D and P transforms)
        if "D" in apply_transforms or "P" in apply_transforms:
            new_score["Total_char_removed_from_reference"] = (
                len(self.reference) - len(self.reference_preprocess)
            )
            new_score["Total_char_removed_from_prediction"] = (
                len(self.prediction) - len(self.prediction_preprocess)
            )

        # Diacritics removed
        if "X" in apply_transforms:
            new_score["Total_diacritics_removed_from_reference"] = (
                count_diacritics(self.reference)
                - count_diacritics(self.reference_preprocess)
            )
            new_score["Total_diacritics_removed_from_prediction"] = (
                count_diacritics(self.prediction)
                - count_diacritics(self.prediction_preprocess)
            )

        # Case changes
        if "U" in apply_transforms:
            new_score["Total_char_pass_in_uppercase_in_reference"] = sum(
                1 for c in self.reference if c.islower()
            )
            new_score["Total_char_pass_in_uppercase_in_prediction"] = sum(
                1 for c in self.prediction if c.islower()
            )

        if "L" in apply_transforms:
            new_score["Total_char_pass_in_lowercase_in_reference"] = sum(
                1 for c in self.reference if c.isupper()
            )
            new_score["Total_char_pass_in_lowercase_in_prediction"] = sum(
                1 for c in self.prediction if c.isupper()
            )

        return new_score
