# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

"Mozilla l10n compare locales tool"

import codecs
import os
import shutil
import re

from compare_locales import parser
from compare_locales import mozpath
from compare_locales.checks import getChecker, EntityPos
from compare_locales.keyedtuple import KeyedTuple

from .observer import ObserverList
from .utils import AddRemove


class ContentComparer:
    keyRE = re.compile("[kK]ey")
    nl = re.compile("\n", re.M)

    def __init__(self, quiet=0):
        """Create a ContentComparer.
        observer is usually a instance of Observer. The return values
        of the notify method are used to control the handling of missing
        entities.
        """
        self.observers = ObserverList(quiet=quiet)

    def create_merge_dir(self, merge_file):
        outdir = mozpath.dirname(merge_file)
        os.makedirs(outdir, exist_ok=True)

    def merge(
        self,
        ref_entities,
        ref_file,
        l10n_file,
        merge_file,
        missing,
        skips,
        ctx,
        capabilities,
        encoding,
    ):
        """Create localized file in merge dir

        `ref_entities` and `ref_map` are the parser result of the
        reference file
        `ref_file` and `l10n_file` are the File objects for the reference and
        the l10n file, resp.
        `merge_file` is the output path for the generated content. This is None
        if we're just comparing or validating.
        `missing` are the missing messages in l10n - potentially copied from
        reference
        `skips` are entries to be dropped from the localized file
        `ctx` is the parsing context
        `capabilities` are the capabilities for the merge algorithm
        `encoding` is the encoding to be used when serializing, usually utf-8
        """

        if not merge_file:
            return

        if capabilities == parser.CAN_NONE:
            return

        self.create_merge_dir(merge_file)

        if capabilities & parser.CAN_COPY:
            # copy the l10n file if it's good, or the reference file if not
            if skips or missing:
                src = ref_file.fullpath
            else:
                src = l10n_file.fullpath
            shutil.copyfile(src, merge_file)
            print("copied reference to " + merge_file)
            return

        if not (capabilities & parser.CAN_SKIP):
            return

        # Start with None in case the merge file doesn't need to be created.
        f = None

        if skips:
            # skips come in ordered by key name, we need them in file order
            skips.sort(key=lambda s: s.span[0])

            # we need to skip a few erroneous blocks in the input, copy by hand
            f = codecs.open(merge_file, "wb", encoding)
            offset = 0
            for skip in skips:
                chunk = skip.span
                f.write(ctx.contents[offset : chunk[0]])
                offset = chunk[1]
            f.write(ctx.contents[offset:])

        if f is None:
            # l10n file is a good starting point
            shutil.copyfile(l10n_file.fullpath, merge_file)

        if not (capabilities & parser.CAN_MERGE):
            if f:
                f.close()
            return

        if skips or missing:
            if f is None:
                f = codecs.open(merge_file, "ab", encoding)
            trailing = (
                ["\n"]
                + [ref_entities[key].all for key in missing]
                + [
                    ref_entities[skip.key].all
                    for skip in skips
                    if not isinstance(skip, parser.Junk)
                ]
            )

            def ensureNewline(s):
                if not s.endswith("\n"):
                    return s + "\n"
                return s

            print("adding to " + merge_file)
            f.write("".join(map(ensureNewline, trailing)))

        if f is not None:
            f.close()

    def remove(self, ref_file, l10n, merge_file):
        """Obsolete l10n file.

        Copy to merge stage if we can.
        """
        self.observers.notify("obsoleteFile", l10n, None)
        self.merge(
            KeyedTuple([]),
            ref_file,
            l10n,
            merge_file,
            [],
            [],
            None,
            parser.CAN_COPY,
            None,
        )

    def compare(self, ref_file, l10n, merge_file, extra_tests=None):
        try:
            p = parser.getParser(ref_file.file)
        except UserWarning:
            # no comparison, XXX report?
            # At least, merge
            self.merge(
                KeyedTuple([]),
                ref_file,
                l10n,
                merge_file,
                [],
                [],
                None,
                parser.CAN_COPY,
                None,
            )
            return
        try:
            p.readFile(ref_file)
        except Exception as e:
            self.observers.notify("error", ref_file, str(e))
            return
        ref_entities = p.parse()
        try:
            p.readFile(l10n)
            l10n_entities = p.parse()
            l10n_ctx = p.ctx
        except Exception as e:
            self.observers.notify("error", l10n, str(e))
            return

        ar = AddRemove()
        ar.set_left(ref_entities.keys())
        ar.set_right(l10n_entities.keys())
        report = missing = obsolete = changed = unchanged = keys = 0
        missing_w = changed_w = unchanged_w = 0  # word stats
        missings = []
        skips = []
        checker = getChecker(l10n, extra_tests=extra_tests)
        if checker and checker.needs_reference:
            checker.set_reference(ref_entities)
        for msg in p.findDuplicates(ref_entities):
            self.observers.notify("warning", l10n, msg)
        for msg in p.findDuplicates(l10n_entities):
            self.observers.notify("error", l10n, msg)
        for action, entity_id in ar:
            if action == "delete":
                # missing entity
                if isinstance(ref_entities[entity_id], parser.Junk):
                    self.observers.notify("warning", l10n, "Parser error in en-US")
                    continue
                _rv = self.observers.notify("missingEntity", l10n, entity_id)
                if _rv == "ignore":
                    continue
                if _rv == "error":
                    # only add to missing entities for l10n-merge on error,
                    # not report
                    missings.append(entity_id)
                    missing += 1
                    refent = ref_entities[entity_id]
                    missing_w += refent.count_words()
                else:
                    # just report
                    report += 1
            elif action == "add":
                # obsolete entity or junk
                if isinstance(l10n_entities[entity_id], parser.Junk):
                    junk = l10n_entities[entity_id]
                    self.observers.notify("error", l10n, junk.error_message())
                    if merge_file is not None:
                        skips.append(junk)
                elif (
                    self.observers.notify("obsoleteEntity", l10n, entity_id) != "ignore"
                ):
                    obsolete += 1
            else:
                # entity found in both ref and l10n, check for changed
                refent = ref_entities[entity_id]
                l10nent = l10n_entities[entity_id]
                if self.keyRE.search(entity_id):
                    keys += 1
                else:
                    if refent.equals(l10nent):
                        self.doUnchanged(l10nent)
                        unchanged += 1
                        unchanged_w += refent.count_words()
                    else:
                        self.doChanged(ref_file, refent, l10nent)
                        changed += 1
                        changed_w += refent.count_words()
                        # run checks:
                if checker:
                    for tp, pos, msg, cat in checker.check(refent, l10nent):
                        if isinstance(pos, EntityPos):
                            line, col = l10nent.position(pos)
                        else:
                            line, col = l10nent.value_position(pos)
                        # skip error entities when merging
                        if tp == "error" and merge_file is not None:
                            skips.append(l10nent)
                        self.observers.notify(
                            tp,
                            l10n,
                            "%s at line %d, column %d for %s"
                            % (msg, line, col, refent.key),
                        )
                pass

        if merge_file is not None:
            self.merge(
                ref_entities,
                ref_file,
                l10n,
                merge_file,
                missings,
                skips,
                l10n_ctx,
                p.capabilities,
                p.encoding,
            )

        stats = {
            "missing": missing,
            "missing_w": missing_w,
            "report": report,
            "obsolete": obsolete,
            "changed": changed,
            "changed_w": changed_w,
            "unchanged": unchanged,
            "unchanged_w": unchanged_w,
            "keys": keys,
        }
        self.observers.updateStats(l10n, stats)
        pass

    def add(self, orig, missing, merge_file):
        """Add missing localized file."""
        f = orig
        try:
            p = parser.getParser(f.file)
        except UserWarning:
            p = None

        # if we don't support this file, assume CAN_COPY to mimick
        # l10n dir as closely as possible
        caps = p.capabilities if p else parser.CAN_COPY
        if caps & (parser.CAN_COPY | parser.CAN_MERGE):
            # even if we can merge, pretend we can only copy
            self.merge(
                KeyedTuple([]),
                orig,
                missing,
                merge_file,
                ["trigger copy"],
                [],
                None,
                parser.CAN_COPY,
                None,
            )

        if self.observers.notify("missingFile", missing, None) == "ignore":
            # filter said that we don't need this file, don't count it
            return

        if p is None:
            # We don't have a parser, cannot count missing strings
            return

        try:
            p.readFile(f)
            entities = p.parse()
        except Exception as ex:
            self.observers.notify("error", f, str(ex))
            return
        # strip parse errors
        entities = [e for e in entities if not isinstance(e, parser.Junk)]
        self.observers.updateStats(missing, {"missing": len(entities)})
        missing_w = 0
        for e in entities:
            missing_w += e.count_words()
        self.observers.updateStats(missing, {"missing_w": missing_w})

    def doUnchanged(self, entity):
        # overload this if needed
        pass

    def doChanged(self, file, ref_entity, l10n_entity):
        # overload this if needed
        pass
