#!/usr/bin/python
# Copyright (C) 2019 Jelmer Vernooij
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

"""Helper functions for fixers."""

import os
import sys
from collections.abc import MutableMapping
from typing import List, Optional, Tuple, Union

from debmutate.control import ControlEditor
from debmutate.deb822 import Deb822Paragraph
from debmutate.vendor import get_vendor_name

from debian.changelog import Version

from . import (
    DEFAULT_MINIMUM_CERTAINTY,
    certainty_sufficient,
)
from .lintian_overrides import (
    LintianOverride,
    get_overrides,
    load_renamed_tags,
)


class LintianIssue:
    """Represents a lintian issue."""

    target: Tuple[str, Optional[str]]
    info: Optional[str]
    tag: str

    def __init__(
        self,
        target: Union[Deb822Paragraph, MutableMapping, Tuple[str, str], str],
        tag: str,
        info: Optional[Union[str, Tuple[str, ...]]] = None,
    ):
        if isinstance(target, (Deb822Paragraph, MutableMapping)):
            if "Source" in target:
                self.target = ("source", target["Source"])
            elif "Package" in target:
                self.target = ("binary", target["Package"])
            else:
                raise ValueError(
                    "unable to determine source/binary package from target"
                )
        elif target == "source":
            self.target = ("source", None)
        else:
            self.target = target  # type: ignore
        if isinstance(info, tuple):
            self.info = " ".join(info)
        else:
            self.info = info
        self.tag = tag

    def json(self):
        return {
            "tag": self.tag,
            "info": self.info,
            "target": self.target,
        }

    def override_exists(self):
        return _override_exists(
            tag=self.tag,
            info=self.info,
            type=self.target[0],
            package=self.target[1],
        )

    def should_fix(self):
        if self.override_exists():
            _overriden_issues.append(self)
            return False
        return True

    def report_fixed(self):
        _fixed_lintian_issues.append(self)

    def __str__(self):
        ret = []
        if self.target[1] is not None:
            ret.append(self.target[1] + " ")
        ret.append(self.target[0])
        ret.append(": " + self.tag + (" " + self.info) if self.info else "")
        return "".join(ret)

    def __repr__(self):
        return f"{type(self).__name__}(target={self.target!r}, tag={self.tag!r}, info={self.info!r})"


_fixed_lintian_issues: List[LintianIssue] = []
_present_overrides: Optional[List[LintianOverride]] = None
_overriden_issues: List[LintianIssue] = []
_tag_renames = None


def _override_exists(
    tag: str,
    info: Optional[str] = None,
    package: Optional[str] = None,
    type: Optional[str] = None,
    arch: Optional[str] = None,
) -> bool:
    global _present_overrides, _tag_renames
    if _present_overrides is None:
        _present_overrides = list(get_overrides())
    if not _present_overrides:
        return False
    if _tag_renames is None:
        _tag_renames = load_renamed_tags()
    for override in _present_overrides:
        if _tag_renames.get(override.tag) == tag:
            tag = override.tag
        if override.matches(
            package=package, info=info, tag=tag, arch=arch, type=type
        ):
            return True
    return False


def fixed_lintian_tags():
    return {issue.tag for issue in _fixed_lintian_issues}


def fixed_lintian_tag(
    target: Union[MutableMapping, Tuple[str, str], str],
    tag: str,
    info: Optional[Union[str, Tuple[str, ...]]] = None,
):
    """Register a lintian tag as being fixed."""
    LintianIssue(target, tag, info).report_fixed()


def reset() -> None:
    """Reset any global state that may exist."""
    global _fixed_lintian_issues, _present_overrides, _overriden_issues
    _fixed_lintian_issues = []
    _present_overrides = None
    _overriden_issues = []


def report_result(
    description=None, certainty=None, patch_name=None, details=None
):
    """Report the result of a fixer.

    Args:
      description: Description of the fix
      certainty: Certainty of the fix
      patch_name: Suggested patch name, if there are upstream changes
      details: List of details, as lines
    """
    if description:
        print(description)
    if details:
        for detail in details:
            print(f"+ {detail}")
    if certainty:
        print(f"Certainty: {certainty}")
    fixed_lintian_tags = {issue.tag for issue in _fixed_lintian_issues}
    if fixed_lintian_tags:
        print(
            "Fixed-Lintian-Tags: {}".format(
                ", ".join(sorted(fixed_lintian_tags))
            )
        )
    if _overriden_issues:
        print("Overridden-Lintian-Issues:")
        for issue in _overriden_issues:
            print(" " + str(issue))
    if patch_name:
        print(f"Patch-Name: {patch_name}")
    reset()


def net_access_allowed():
    """Check whether network access is allowed."""
    return os.environ.get("NET_ACCESS", "disallow") == "allow"


def compat_release():
    """Codename of oldest release to stay compatible with."""
    return os.environ.get("COMPAT_RELEASE", "sid")


def upgrade_release():
    """Codename of oldest release to allow upgrading from."""
    return os.environ.get("UPGRADE_RELEASE", "oldstable")


def current_package_version():
    return Version(os.environ["CURRENT_VERSION"])


def package_is_native():
    return not current_package_version().debian_revision


def meets_minimum_certainty(certainty):
    return certainty_sufficient(
        certainty,
        os.environ.get("MINIMUM_CERTAINTY", DEFAULT_MINIMUM_CERTAINTY),
    )


def trust_package():
    return os.environ.get("TRUST_PACKAGE") == "true"


def opinionated():
    return os.environ.get("OPINIONATED", "no") == "yes"


def warn(msg):
    sys.stderr.write(f"{msg}\n")


def diligence():
    return int(os.environ.get("DILIGENCE", "0"))


def source_package_name():
    return os.environ.get("DEB_SOURCE")


def is_debcargo_package():
    return os.path.exists("debian/debcargo.toml")


if is_debcargo_package():
    from debmutate.debcargo import DebcargoControlShimEditor

    control = DebcargoControlShimEditor.from_debian_dir("debian")
else:
    control = ControlEditor()


def vendor() -> str:
    return get_vendor_name()


def linenos_to_ranges(linenos):
    ret = []
    cur_range: List[int] = []

    def finalize_range(r):
        if len(r) == 1:
            ret.append(f"{r[0]}")
        else:
            ret.append(f"{r[0]}-{r[-1]}")

    for lineno in linenos:
        if not cur_range or cur_range[-1] == lineno - 1:
            cur_range.append(lineno)
        else:
            finalize_range(cur_range)
            cur_range = [lineno]
    if cur_range:
        finalize_range(cur_range)
    return ret


def shorten_path(path):
    ps = path.split("/")
    if ps[0] == "debian":
        ps[0] = "d"
    return "/".join(ps)
