# 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 https://mozilla.org/MPL/2.0/.

import pathlib
import re

from mozlint import result
from mozlint.pathutils import expand_exclusions

validReasons = [
    "git-only",
    "hg-only",
    "syntax-difference",
]


class IgnorePattern:
    """A class that represents single pattern in .gitignore or .hgignore, and
    provides rough comparison, and also hashing for SequenceMatcher."""

    def __init__(self, pattern, lineno):
        self.pattern = pattern
        self.lineno = lineno

        self.simplePattern = self.removePunctuation(pattern)
        self.hash = hash(self.pattern)

    @staticmethod
    def removePunctuation(s):
        # Remove special characters.
        # '.' is also removed because .hgignore uses regular expression.
        s = re.sub(r"[^A-Za-z0-9_~/]", "", s)

        # '/' is kept, except for the following cases:
        #   * leading '/' in .gitignore to specify top-level file,
        #     which is represented as '^' in .hgignore
        #   * leading '(^|/)' in .hgignore to specify filename
        #     (characters except for '/' are removed above)
        #   * leading '[^/]*' in .hgignore
        #     (characters except for '/' are removed above)
        s = re.sub(r"^/", "", s)

        return s

    def __eq__(self, other):
        return self.simplePattern == other.simplePattern

    def __hash__(self):
        return self.hash


def parseFile(results, path, config):
    patterns = []
    ignoreNextLine = False

    lineno = 0
    with open(path) as f:
        for line in f:
            line = line.rstrip("\n")
            lineno += 1

            if ignoreNextLine:
                ignoreNextLine = False
                continue

            m = re.match(r"^# lint-ignore-next-line: (.+)", line)
            if m:
                reason = m.group(1)
                if reason not in validReasons:
                    res = {
                        "path": str(path),
                        "lineno": lineno,
                        "message": f'Unknown lint rule: "{reason}"',
                        "level": "error",
                    }
                    results.append(result.from_config(config, **res))
                    continue

                ignoreNextLine = True
                continue

            m = line.startswith("#")
            if m:
                continue

            if line == "":
                continue

            patterns.append(IgnorePattern(line, lineno))

    return patterns


def getLineno(patterns, index):
    if index >= len(patterns):
        return patterns[-1].lineno

    return patterns[index].lineno


def doLint(results, path1, config):
    if path1.name == ".gitignore":
        path2 = path1.parent / ".hgignore"
    elif path1.name == ".hgignore":
        path2 = path1.parent / ".gitignore"
    else:
        res = {
            "path": str(path1),
            "lineno": 0,
            "message": "Unsupported file",
            "level": "error",
        }
        results.append(result.from_config(config, **res))
        return

    patterns1 = parseFile(results, path1, config)
    patterns2 = parseFile(results, path2, config)

    # Comparison for each line is done via IgnorePattern.__eq__, which
    # ignores punctuation.
    if patterns1 == patterns2:
        return

    # Report minimal differences.
    from difflib import SequenceMatcher

    s = SequenceMatcher(None, patterns1, patterns2)
    for tag, index1, _, index2, _ in s.get_opcodes():
        if tag == "replace":
            res = {
                "path": str(path1),
                "lineno": getLineno(patterns1, index1),
                "message": f'Pattern mismatch: "{patterns1[index1].pattern}" in {path1.name} vs "{patterns2[index2].pattern}" in {path2.name}',
                "level": "error",
            }
            results.append(result.from_config(config, **res))
        elif tag == "delete":
            res = {
                "path": str(path1),
                "lineno": getLineno(patterns1, index1),
                "message": f'Pattern "{patterns1[index1].pattern}" not found in {path2.name}',
                "level": "error",
            }
            results.append(result.from_config(config, **res))
        elif tag == "insert":
            res = {
                "path": str(path1),
                "lineno": getLineno(patterns1, index1),
                "message": f'Pattern "{patterns2[index2].pattern}" not found in {path1.name}',
                "level": "error",
            }
            results.append(result.from_config(config, **res))


def lint(paths, config, fix=None, **lintargs):
    results = []

    for path in expand_exclusions(paths, config, lintargs["root"]):
        doLint(results, pathlib.Path(path), config)

    return {"results": results, "fixed": 0}
