# 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/.

import re
from difflib import SequenceMatcher

from compare_locales.parser import PropertiesEntity
from compare_locales import plurals
from .base import Checker


class PrintfException(Exception):
    def __init__(self, msg, pos):
        self.pos = pos
        self.msg = msg


class PropertiesChecker(Checker):
    """Tests to run on .properties files."""

    pattern = re.compile(r".*\.properties$")
    printf = re.compile(
        r"%(?P<good>%|"
        r"(?:(?P<number>[1-9][0-9]*)\$)?"
        r"(?P<width>\*|[0-9]+)?"
        r"(?P<prec>\.(?:\*|[0-9]+)?)?"
        r"(?P<spec>[duxXosScpfg]))?"
    )

    def check(self, refEnt, l10nEnt):
        """Test for the different variable formats."""
        yield from super().check(refEnt, l10nEnt)
        refValue, l10nValue = refEnt.val, l10nEnt.val
        refSpecs = None
        # check for PluralForm.jsm stuff, should have the docs in the
        # comment
        # That also includes intl.properties' pluralRule, so exclude
        # entities with that key and values with just numbers
        if (
            refEnt.pre_comment
            and "Localization_and_Plurals" in refEnt.pre_comment.all
            and refEnt.key != "pluralRule"
            and not re.match(r"\d+$", refValue)
        ):
            yield from self.check_plural(refValue, l10nValue)
            return
        # check for lost escapes
        raw_val = l10nEnt.raw_val
        for m in PropertiesEntity.escape.finditer(raw_val):
            if (
                m.group("single")
                and m.group("single") not in PropertiesEntity.known_escapes
            ):
                yield (
                    "warning",
                    m.start(),
                    "unknown escape sequence, \\" + m.group("single"),
                    "escape",
                )
        try:
            refSpecs = self.getPrintfSpecs(refValue)
        except PrintfException:
            refSpecs = []
        if refSpecs:
            yield from self.checkPrintf(refSpecs, l10nValue)
            return

    def check_plural(self, refValue, l10nValue):
        """Check for the stringbundle plurals logic.
        The common variable pattern is #1.
        """
        known_plurals = plurals.get_plural(self.locale)
        if known_plurals:
            expected_forms = len(known_plurals)
            found_forms = l10nValue.count(";") + 1
            msg = f"expecting {expected_forms} plurals, found {found_forms}"
            if expected_forms > found_forms:
                yield ("warning", 0, msg, "plural")
            if expected_forms < found_forms:
                yield ("warning", 0, msg, "plural")
        pats = {int(m.group(1)) for m in re.finditer("#([0-9]+)", refValue)}
        if len(pats) == 0:
            return
        lpats = {int(m.group(1)) for m in re.finditer("#([0-9]+)", l10nValue)}
        if pats - lpats:
            yield ("warning", 0, "not all variables used in l10n", "plural")
            return
        if lpats - pats:
            yield ("error", 0, "unreplaced variables in l10n", "plural")

    def checkPrintf(self, refSpecs, l10nValue):
        try:
            l10nSpecs = self.getPrintfSpecs(l10nValue)
        except PrintfException as e:
            yield ("error", e.pos, e.msg, "printf")
            return
        if refSpecs != l10nSpecs:
            sm = SequenceMatcher()
            sm.set_seqs(refSpecs, l10nSpecs)
            msgs = []
            warn = None
            for action, i1, i2, j1, j2 in sm.get_opcodes():
                if action == "equal":
                    continue
                if action == "delete":
                    # missing argument in l10n
                    if i2 == len(refSpecs):
                        # trailing specs missing, that's just a warning
                        warn = ", ".join(
                            "trailing argument %d `%s` missing" % (i + 1, refSpecs[i])
                            for i in range(i1, i2)
                        )
                    else:
                        for i in range(i1, i2):
                            msgs.append(
                                "argument %d `%s` missing" % (i + 1, refSpecs[i])
                            )
                    continue
                if action == "insert":
                    # obsolete argument in l10n
                    for i in range(j1, j2):
                        msgs.append("argument %d `%s` obsolete" % (i + 1, l10nSpecs[i]))
                    continue
                if action == "replace":
                    for i, j in zip(range(i1, i2), range(j1, j2)):
                        msgs.append(
                            "argument %d `%s` should be `%s`"
                            % (j + 1, l10nSpecs[j], refSpecs[i])
                        )
            if msgs:
                yield ("error", 0, ", ".join(msgs), "printf")
            if warn is not None:
                yield ("warning", 0, warn, "printf")

    def getPrintfSpecs(self, val):
        hasNumber = False
        specs = []
        for m in self.printf.finditer(val):
            if m.group("good") is None:
                # found just a '%', signal an error
                raise PrintfException("Found single %", m.start())
            if m.group("good") == "%":
                # escaped %
                continue
            if (hasNumber and m.group("number") is None) or (
                not hasNumber and specs and m.group("number") is not None
            ):
                # mixed style, numbered and not
                raise PrintfException("Mixed ordered and non-ordered args", m.start())
            hasNumber = m.group("number") is not None
            if hasNumber:
                pos = int(m.group("number")) - 1
                ls = len(specs)
                if pos >= ls:
                    # pad specs
                    nones = pos - ls
                    specs[ls:pos] = nones * [None]
                    specs.append(m.group("spec"))
                else:
                    specs[pos] = m.group("spec")
            else:
                specs.append(m.group("spec"))
        # check for missing args
        if hasNumber and not all(specs):
            raise PrintfException("Ordered argument missing", 0)
        return specs
