#!/usr/bin/env python3
# Copyright: 2018, Maximiliano Curia <maxy@debian.org>
#
# License: ISC
#  Permission to use, copy, modify, and/or distribute this software for any
#  purpose with or without fee is hereby granted, provided that the above
#  copyright notice and this permission notice appear in all copies.
#  .
#  THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
#  REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
#  AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
#  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
#  LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
#  OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
#  PERFORMANCE OF THIS SOFTWARE.


"""License detail parsers and the SUBS evaluation helpers.

The get_detail callbacks and parse_* family parsers that refine a matched
license id, plus the shared SUBS helpers. Consumes the data in license_subs;
wired onto the rules in res via get_detail / _FAMILY_DETAIL.
"""

from __future__ import annotations

import collections.abc as abc
import itertools
import re as reo

from decopy.common._regex import rex
from decopy.common.re_types import ReSub, ReSubGroup
from decopy.licensing.license_subs import (
    APACHE_EXCEPTION_SUBS,
    ARTISTIC_SUBS,
    ASWF_SUBS,
    BOEHM_SUBS,
    BSD_SUBS,
    CC_BY_SUBS,
    CECILL_SUBS,
    CMU_MACH_SUBS,
    CNRI_SUBS,
    CTAN_SUBS,
    DOC_SUBS,
    EFL_SUBS,
    ESA_PL_SUBS,
    FONT_SUBS,
    GFDL_SUBS,
    GNU_EXCEPTION_SUBS,
    GNU_VERSION_SUBS,
    HW_SUBS,
    LPPL_SUBS,
    MANPAGE_SUBS,
    MIT_SUBS,
    MITRE_SUBS,
    MULAN_SUBS,
    OSL_DERIVED_SUBS,
    PD_MISC_SUBS,
    QPL_EXCEPTION_SUBS,
    SRCAVAIL_SUBS,
    SSH_SUBS,
    STD_SUBS,
    TU_BERLIN_SUBS,
    UDA_SUBS,
    UNIVERSAL_EXCEPTION_SUBS,
    USGOV_PD_SUBS,
    ZPL_SUBS,
)

# Match type accepted by the family detail callbacks. They ignore the match
# argument, so it also admits None (callers may pass it explicitly).
type DetailMatch = rex.Match[str] | reo.Match[str] | None


# LICENSES_RES common get_details
def name_version(
    text: str,
    version_match: rex.Match[str] | reo.Match[str],
    group: str,
    name: str | None = None,
) -> str:
    _ = text
    if not name:
        name = group
    version = version_match.group(1)
    return "{}{}".format(name, "-{}".format(version.rstrip(".0") if version else ""))


def group_version(
    text: str,
    version_match: rex.Match[str] | reo.Match[str],
    group: str,
) -> str:
    return name_version(text, version_match, group)


def python_detail(
    text: str,
    _match: DetailMatch,
    _group: str,
) -> str:
    # The Python license text comes in three shapes: the standalone PSF license
    # (PSF-2.0) and the 2.0 / 2.0.1 collections that wrap PSF + BeOpen + CNRI +
    # CWI. The collection is marked by the BeOpen heading; 2.0.1 is the variant
    # whose CNRI agreement is the GPL-compatible "for Python" form.
    if "CNRI LICENSE AGREEMENT FOR PYTHON" in text:
        return "Python-2.0.1"
    if "BEOPEN PYTHON OPEN SOURCE LICENSE" in text:
        return "Python-2"
    return "PSF-2"


def cc_at_detail(
    text: str,
    _match: DetailMatch,
    _group: str,
) -> str:
    # The two Austrian CC 3.0 legal codes have no deed title for the generic
    # CC_BY_SUBS path to read (CC-BY-3.0-AT has no version string in its text at
    # all), so emit the full id directly. They are matched on "Österreich", which
    # is unique to these two files; the ShareAlike clause tells them apart.
    if "Weitergabe unter gleichen Bedingungen" in text:
        return "CC-BY-SA-3-AT"
    return "CC-BY-3-AT"


def caldera_detail(
    text: str,
    _match: DetailMatch,
    _group: str,
) -> str:
    # Caldera and Caldera-no-preamble share the same BSD-4-clause body; the full
    # Caldera licence additionally carries the ancient-UNIX "fee free license"
    # preamble. One family rule (anchored on the shared body) tells them apart.
    if "hereby grants a fee free license" in text:
        return "Caldera"
    return "Caldera-no-preamble"


def ijg_detail(
    text: str,
    _match: DetailMatch,
    _group: str,
) -> str:
    # IJG and IJG-short share the README / no-warranty permission clause; the full
    # IJG licence additionally carries the "derived from or based on the IJG code"
    # condition that the short form drops.
    if "derived from or based on the IJG code" in text:
        return "IJG"
    return "IJG-short"


def libpng_detail(
    text: str,
    version_match: rex.Match[str] | reo.Match[str],
    _group: str,
) -> str:
    # The PNG Reference Library License title carries the major version: v1 is the
    # historic libpng (0.5 through 1.6.35), v2 the current form.
    _ = text
    return "libpng-1.6.35" if version_match.group(1) == "1" else "libpng-2"


def mackerras_detail(
    text: str,
    _match: DetailMatch,
    _group: str,
) -> str:
    # Both Mackerras BSD-3-clause variants share the disclaimer; the acknowledgment
    # form adds a "developed by Paul Mackerras" attribution clause.
    if "developed by Paul Mackerras" in text:
        return "Mackerras-3-Clause-acknowledgment"
    return "Mackerras-3-Clause"


# subs are way more efficient when using python's re (reo here)
# SUBS evaluation helpers, shared by the get_detail callbacks below and by
# find_licenses (which applies a matched rule's exception_subs).


def _strip_zero_version(value: str) -> str:
    # Drop a trailing decimal ".0" (e.g. "GFDL-1.0" -> "GFDL-1") without eating a
    # bare trailing zero that belongs to the name itself (e.g. "MIT-0").
    return rex.sub(r"\.0*$", "", value)


def _parse_subs_first(
    text: str,
    fallback: str,
    subs: abc.Iterable[ReSub],
) -> str:
    for sub in subs:
        match = sub.pattern.search(text)
        if not match:
            continue
        return _strip_zero_version(match.expand(sub.repl))

    return fallback


def _parse_subs_all(
    text: str,
    subs: abc.Iterable[ReSub | ReSubGroup],
) -> list[str]:
    found: dict[str, None] = {}

    def _try(sub: ReSub) -> bool:
        match = sub.pattern.search(text)
        if match:
            found[_strip_zero_version(match.expand(sub.repl))] = None
        return bool(match)

    for item in subs:
        if isinstance(item, ReSubGroup):
            any(_try(sub) for sub in item.subs)
        else:
            _try(item)

    return list(found)


def parse_exceptions(text: str, group: str, exception_subs: abc.Iterable) -> str:
    exceptions = _parse_subs_all(
        text, itertools.chain(exception_subs, UNIVERSAL_EXCEPTION_SUBS)
    )
    exception = (
        " with {} exception".format("_".join(sorted(exceptions))) if exceptions else ""
    )
    return group + exception


# Per-family get_detail callbacks: refine a matched family id using its SUBS.
# Wired onto the matching rules below via _FAMILY_DETAIL.


def parse_artistic(text: str, _match: DetailMatch, group: str) -> str:
    return _parse_subs_first(text, group, ARTISTIC_SUBS)


def parse_bsd(text: str, _match: DetailMatch, group: str) -> str:
    # A bare BSD warranty disclaimer is not enough to assert BSD: only report it
    # when a specific clause count or variant is recognised. Otherwise return ""
    # so a disclaimer borrowed by an ISC/MIT-style licence is not mislabelled BSD
    # (find_licenses drops empty results).
    return _parse_subs_first(text, "", BSD_SUBS)


def parse_cc_by(text: str, _match: DetailMatch, group: str) -> str:
    details = _parse_subs_all(text, CC_BY_SUBS)
    return group + "".join(details)


def parse_doc(text: str, _match: DetailMatch, group: str) -> str:
    # The DocLicense gate fires on a title alone; require a body clause here so a
    # mere reference to one of these licences is not reported as the licence.
    return _parse_subs_first(text, "", DOC_SUBS)


def parse_font(text: str, _match: DetailMatch, group: str) -> str:
    # The FontLicense gate groups the font licences; the per-licence rule must
    # match a substantive grant clause, not just a foundry or font name.
    return _parse_subs_first(text, "", FONT_SUBS)


def parse_hw(text: str, _match: DetailMatch, group: str) -> str:
    # The HardwareLicense gate groups the open-hardware licences; each rule pairs
    # the versioned title with a preamble clause so references do not match.
    return _parse_subs_first(text, "", HW_SUBS)


def parse_usgov_pd(text: str, _match: DetailMatch, group: str) -> str:
    # The USGovPD gate groups the US-Government / agency public-domain notices;
    # each rule pins the public-domain clause to the agency naming the id.
    return _parse_subs_first(text, "", USGOV_PD_SUBS)


def parse_pd_misc(text: str, _match: DetailMatch, group: str) -> str:
    # The PdMisc gate groups unrelated public-domain / "do as you wish" notices;
    # each rule anchors on the notice's distinctive dedication clause.
    return _parse_subs_first(text, "", PD_MISC_SUBS)


def parse_manpage(text: str, _match: DetailMatch, group: str) -> str:
    # The ManPage gate groups the Linux man-pages licences; specific variant
    # tails come first, the bare two-paragraph form is the fallback.
    return _parse_subs_first(text, "", MANPAGE_SUBS)


def parse_ctan(text: str, _match: DetailMatch, group: str) -> str:
    # The CtanTex gate groups the CTAN/TeX licences; each rule anchors on its
    # distinctive permission/rename clause.
    return _parse_subs_first(text, "", CTAN_SUBS)


def parse_std(text: str, _match: DetailMatch, group: str) -> str:
    # The StandardsBody gate groups Unicode + the W3C-derived licences; the ICU
    # Unicode rule precedes the bare-title 2015 fallback.
    return _parse_subs_first(text, "", STD_SUBS)


def parse_uda(text: str, _match: DetailMatch, group: str) -> str:
    # The UdaData gate groups the Microsoft Use of Data Agreements; each rule
    # anchors on the Computational/Open title with its version.
    return _parse_subs_first(text, "", UDA_SUBS)


def parse_aswf(text: str, _match: DetailMatch, group: str) -> str:
    # The AswfAssets gate groups the ASWF Digital Assets versions; each rule
    # anchors on the title version line.
    return _parse_subs_first(text, "", ASWF_SUBS)


def parse_mitre(text: str, _match: DetailMatch, group: str) -> str:
    # The MitreTou gate groups the MITRE terms-of-use grants; each rule names the
    # dataset (CAPEC, CVE).
    return _parse_subs_first(text, "", MITRE_SUBS)


def parse_cmu_mach(text: str, _match: DetailMatch, group: str) -> str:
    # The CmuMach gate groups the CMU/Mach permission notices; the documentation
    # variant precedes the bare no-doc fallback.
    return _parse_subs_first(text, "", CMU_MACH_SUBS)


def parse_ssh(text: str, _match: DetailMatch, group: str) -> str:
    # The Ssh gate groups Tatu Ylonen's SSH notices; the full OpenSSH form
    # precedes the short grant-only fallback.
    return _parse_subs_first(text, "", SSH_SUBS)


def parse_boehm(text: str, _match: DetailMatch, group: str) -> str:
    # The BoehmGc gate groups the Boehm GC permission notices; the without-fee
    # variant precedes the base form.
    return _parse_subs_first(text, "", BOEHM_SUBS)


def parse_mulan(text: str, _match: DetailMatch, group: str) -> str:
    # The MulanPsl gate groups the two Mulan PSL versions; each rule anchors on
    # the "第1版"/"第2版" version marker inside the grant clause.
    return _parse_subs_first(text, "", MULAN_SUBS)


def parse_osl_derived(text: str, _match: DetailMatch, group: str) -> str:
    # The OslDerived gate groups the OSL-template derivatives (TGPPL, UCL); each
    # rule pairs the licence's own title with the shared OSL body clause.
    return _parse_subs_first(text, "", OSL_DERIVED_SUBS)


def parse_esa_pl(text: str, _match: DetailMatch, group: str) -> str:
    # The EsaPl gate groups the three ESA-PL copyleft strengths; each rule
    # anchors on its variant title plus the start of the shared definitions.
    return _parse_subs_first(text, "", ESA_PL_SUBS)


def parse_tu_berlin(text: str, _match: DetailMatch, group: str) -> str:
    # The TuBerlin gate groups the two TU-Berlin notice versions; the v2.0 rule
    # (appended grant) precedes the v1.0 base-notice fallback.
    return _parse_subs_first(text, "", TU_BERLIN_SUBS)


def parse_srcavail(text: str, _match: DetailMatch, group: str) -> str:
    # The SrcAvailable gate groups the source-available / business copyleft
    # licences; each rule pairs the title-and-version line with a substantive
    # body clause. The two Parity rules precede nothing else they could shadow.
    return _parse_subs_first(text, "", SRCAVAIL_SUBS)


def parse_cecill(text: str, _match: DetailMatch, group: str) -> str:
    return _parse_subs_first(text, group, CECILL_SUBS)


def parse_cnri(text: str, _match: DetailMatch, group: str) -> str:
    return _parse_subs_first(text, group, CNRI_SUBS)


def parse_efl(text: str, _match: DetailMatch, group: str) -> str:
    return _parse_subs_first(text, group, EFL_SUBS)


def parse_gpl(text: str, _match: DetailMatch, group: str) -> str:
    version = _parse_subs_first(text, "", GNU_VERSION_SUBS)
    base = "{}{}".format(
        group,
        "-{}".format(version.rstrip("0.")) if version else "",
    )
    return parse_exceptions(text, base, GNU_EXCEPTION_SUBS)


def parse_gfdl(text: str, _match: DetailMatch, group: str) -> str:
    details = _parse_subs_all(text, GFDL_SUBS)
    return group + "".join(details)


def parse_lppl(text: str, _match: DetailMatch, group: str) -> str:
    details = _parse_subs_all(text, LPPL_SUBS)
    return group + "".join(details)


def parse_mit(text: str, _match: DetailMatch, group: str) -> str:
    return _parse_subs_first(text, group, MIT_SUBS)


def parse_apache(text: str, _match: DetailMatch, group: str) -> str:
    return parse_exceptions(text, group, APACHE_EXCEPTION_SUBS)


def parse_qpl(text: str, _match: DetailMatch, group: str) -> str:
    return parse_exceptions(text, group, QPL_EXCEPTION_SUBS)


def parse_zpl(text: str, _match: DetailMatch, group: str) -> str:
    details = _parse_subs_all(text, ZPL_SUBS)
    return group + "".join(details)
