# 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


class EntityPos(int):
    pass


mochibake = re.compile("\ufffd")


class Checker:
    """Abstract class to implement checks per file type."""

    pattern = None
    # if a check uses all reference entities, set this to True
    needs_reference = False

    @classmethod
    def use(cls, file):
        return cls.pattern.match(file.file)

    def __init__(self, extra_tests, locale=None):
        self.extra_tests = extra_tests
        self.locale = locale
        self.reference = None

    def check(self, refEnt, l10nEnt):
        """Given the reference and localized Entities, performs checks.

        This is a generator yielding tuples of
        - "warning" or "error", depending on what should be reported,
        - tuple of line, column info for the error within the string
        - description string to be shown in the report

        By default, check for possible encoding errors.
        """
        for m in mochibake.finditer(l10nEnt.all):
            yield (
                "warning",
                EntityPos(m.start()),
                f"\ufffd in: {l10nEnt.key}",
                "encodings",
            )

    def set_reference(self, reference):
        """Set the reference entities.
        Only do this if self.needs_reference is True.
        """
        self.reference = reference


class CSSCheckMixin:
    def maybe_style(self, ref_value, l10n_value):
        ref_map, _ = self.parse_css_spec(ref_value)
        if not ref_map:
            return
        l10n_map, errors = self.parse_css_spec(l10n_value)
        yield from self.check_style(ref_map, l10n_map, errors)

    def check_style(self, ref_map, l10n_map, errors):
        if not l10n_map:
            yield ("error", 0, "reference is a CSS spec", "css")
            return
        if errors:
            yield ("error", 0, "reference is a CSS spec", "css")
            return
        msgs = []
        for prop, unit in l10n_map.items():
            if prop not in ref_map:
                msgs.insert(0, "%s only in l10n" % prop)
                continue
            else:
                ref_unit = ref_map.pop(prop)
                if unit != ref_unit:
                    msgs.append(
                        "units for %s don't match "
                        "(%s != %s)" % (prop, unit, ref_unit)
                    )
        for prop in ref_map.keys():
            msgs.insert(0, "%s only in reference" % prop)
        if msgs:
            yield ("warning", 0, ", ".join(msgs), "css")

    def parse_css_spec(self, val):
        if not hasattr(self, "_css_spec"):
            self._css_spec = re.compile(
                r"(?:"
                r"(?P<prop>(?:min\-|max\-)?(?:width|height))"
                r"[ \t\r\n]*:[ \t\r\n]*"
                r"(?P<length>[0-9]+|[0-9]*\.[0-9]+)"
                r"(?P<unit>ch|em|ex|rem|px|cm|mm|in|pc|pt)"
                r")"
                r"|\Z"
            )
            self._css_sep = re.compile(r"[ \t\r\n]*(?P<semi>;)?[ \t\r\n]*$")
        refMap = errors = None
        end = 0
        for m in self._css_spec.finditer(val):
            if end == 0 and m.start() == m.end():
                # no CSS spec found, just immediately end of string
                return None, None
            if m.start() > end:
                split = self._css_sep.match(val, end, m.start())
                if split is None:
                    errors = errors or []
                    errors.append(
                        {
                            "pos": end,
                            "code": "css-bad-content",
                        }
                    )
                elif end > 0 and split.group("semi") is None:
                    errors = errors or []
                    errors.append(
                        {
                            "pos": end,
                            "code": "css-missing-semicolon",
                        }
                    )
            if m.group("prop"):
                refMap = refMap or {}
                refMap[m.group("prop")] = m.group("unit")
            end = m.end()
        return refMap, errors
