from __future__ import annotations
from typing import List, Set, Tuple, cast

import logging

import fluent.syntax.ast as FTL
from fluent.migrate.util import fold

from .transforms import Source
from .util import get_message, skeleton
from .errors import (
    EmptyLocalizationError,
    UnreadableReferenceError,
)
from ._context import InternalContext


__all__ = [
    "EmptyLocalizationError",
    "UnreadableReferenceError",
    "MigrationContext",
]


class MigrationContext(InternalContext):
    """Stateful context for merging translation resources.

    `MigrationContext` must be configured with the target locale and the
    directory locations of the input data.

    The transformation takes four types of input data:

        - The en-US FTL reference files which will be used as templates for
          message order, comments and sections. If the reference_dir is None,
          the migration will create Messages and Terms in the order given by
          the transforms.

        - The current FTL files for the given locale.

        - A list of `FTL.Message` or `FTL.Term` objects some of whose nodes
          are special helper or transform nodes:

              helpers: VARIABLE_REFERENCE, MESSAGE_REFERENCE, TERM_REFERENCE
              transforms: COPY, REPLACE_IN_TEXT, REPLACE, PLURALS, CONCAT
              fluent value helper: COPY_PATTERN

    The legacy (DTD, properties) translation files are deduced by the
    dependencies in the transforms. The translations from these files will be
    read from the localization_dir and transformed into FTL and merged
    into the existing FTL files for the given language.
    """

    def __init__(
        self,
        locale: str,
        reference_dir: str,
        localization_dir: str,
        enforce_translated=False,
    ):
        super().__init__(
            locale,
            enforce_translated=enforce_translated,
        )
        self.locale = locale
        # Paths to directories with input data, relative to CWD.
        self.reference_dir = reference_dir
        self.localization_dir = localization_dir

        self.dependencies = {}
        """
        A dict whose keys are `(path, key)` tuples corresponding to target
        FTL translations, and values are sets of `(path, key)` tuples
        corresponding to localized entities which will be migrated.
        """

    def add_transforms(
        self, target: str, reference: str, transforms: List[FTL.Message | FTL.Term]
    ):
        """Define transforms for target using reference as template.

        `target` is a path of the destination FTL file relative to the
        localization directory. `reference` is a path to the template FTL
        file relative to the reference directory.

        Each transform is an extended FTL node with `Transform` nodes as some
        values.  Transforms are stored in their lazy AST form until
        `merge_changeset` is called, at which point they are evaluated to real
        FTL nodes with migrated translations.

        Each transform is scanned for `Source` nodes which will be used to
        build the list of dependencies for the transformed message.

        For transforms that merely copy legacy messages or Fluent patterns,
        using `fluent.migrate.helpers.transforms_from` is recommended.
        """

        def get_sources(acc, cur):
            if isinstance(cur, Source):
                acc.add((cur.path, cur.key))
            return acc

        if self.reference_dir is None:
            # Add skeletons to resource body for each transform
            # if there's no reference.
            reference_ast = self.reference_resources.get(target)
            if reference_ast is None:
                reference_ast = FTL.Resource()
            reference_ast.body.extend(skeleton(transform) for transform in transforms)
        else:
            reference_ast = self.read_reference_ftl(reference)
        self.reference_resources[target] = reference_ast

        for node in transforms:
            ident = cast(str, node.id.name)
            # Scan `node` for `Source` nodes and collect the information they
            # store into a set of dependencies.
            dependencies = cast(Set[Tuple[str, Source]], fold(get_sources, node, set()))
            # Set these sources as dependencies for the current transform.
            self.dependencies[(target, ident)] = dependencies

            # The target Fluent message should exist in the reference file. If
            # it doesn't, it's probably a typo.
            # Of course, only if we're having a reference.
            if self.reference_dir is None:
                continue
            if get_message(reference_ast.body, ident) is None:
                logger = logging.getLogger("migrate")
                logger.warning(
                    '{} "{}" was not found in {}'.format(
                        type(node).__name__, ident, reference
                    )
                )

        # Keep track of localization resource paths which were defined as
        # sources in the transforms.
        expected_paths = set()

        # Read all legacy translation files defined in Source transforms. This
        # may fail but a single missing legacy resource doesn't mean that the
        # migration can't succeed.
        for dependencies in self.dependencies.values():
            for path in {path for path, _ in dependencies}:
                expected_paths.add(path)
                self.maybe_add_localization(path)

        # However, if all legacy resources are missing, bail out early. There
        # are no translations to migrate. We'd also get errors in hg annotate.
        if len(expected_paths) > 0 and len(self.localization_resources) == 0:
            error_message = "No localization files were found"
            logging.getLogger("migrate").error(error_message)
            raise EmptyLocalizationError(error_message)

        # Add the current transforms to any other transforms added earlier for
        # this path.
        path_transforms = self.transforms.setdefault(target, [])
        path_transforms += transforms

        if target not in self.target_resources:
            target_ast = self.read_localization_ftl(target)
            self.target_resources[target] = target_ast
