#!/usr/bin/env python3
# Copyright: 2016, 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.

"""Group management and processing"""

from __future__ import annotations

import collections.abc as abc
import os
import typing as t

import debian.copyright as dc

from .core_types import UNKNOWN, UNKNOWN_COPYRIGHTED, CopyrightHolder, License, Options
from .file_policy import splitting_path
from .group_contracts import GroupProtocol
from .grouping import GroupKey, make_group_key
from .grouping_strategy import grouping_strategy_for
from .holders import CopyrightHolders
from .nodes import FileInfo


class TallyTracker:
    """Tracks nodes already emitted under a wildcard.

    When file paragraphs are wildcarded, a later, more specific group must not
    re-wildcard an ancestor an earlier group already covered. One tracker is
    shared across all groups in a single output run; it holds the bookkeeping
    that used to live on the nodes themselves.
    """

    def __init__(self) -> None:
        self._tallied: set[FileInfo] = set()

    def first_untallied(self, node: FileInfo) -> FileInfo:
        """The topmost ancestor of `node` (including itself) not yet tallied."""
        if node.parent is not None and node.parent not in self._tallied:
            return self.first_untallied(node.parent)
        return node

    def tally(self, node: FileInfo) -> None:
        """Mark `node` and all its ancestors as tallied."""
        current: FileInfo | None = node
        while current is not None and current not in self._tallied:
            self._tallied.add(current)
            current = current.parent


class FileGroup:
    def __init__(self) -> None:
        self.files: dict[str, FileInfo] = {}

    def __len__(self) -> int:
        return len(self.files)

    def __bool__(self) -> bool:
        return bool(self.files)

    def __iter__(self) -> abc.Iterator[FileInfo]:
        return iter(self.files.values())

    def __repr__(self) -> str:
        return f"FileGroup({self.sorted_members()!s})"

    def add_file(self, fileinfo: FileInfo) -> None:
        self.files[fileinfo.fullname] = fileinfo

    def del_file(self, fileinfo: FileInfo) -> FileInfo:
        return self.files.pop(fileinfo.fullname)

    def get_patterns(self, tracker: TallyTracker) -> list[str]:
        """Returns the wildcarded patters for the contained files.

        The order in which this method is called is important.  It should be
        called with the group that will get the more general wildcard first, and
        with the groups that will get more specific values later. The shared
        tracker carries that ordering state across groups.
        """
        patterns: set[str] = set()
        to_tally: set[FileInfo] = set()

        for fileinfo in self:
            untallied = tracker.first_untallied(fileinfo)
            to_tally.add(untallied)

        for fileinfo in to_tally:
            patterns.add(fileinfo.wildcard_pattern())
        for fileinfo in self:
            tracker.tally(fileinfo)

        return sorted(patterns)

    def path_key(self, options: Options) -> str:
        if not self.files:
            return ""
        return os.path.commonpath(
            tuple(t.cast(str, splitting_path(v, options)) for v in self.files.values()),
        )

    def commonpath(self) -> str:
        if not self.files:
            return ""
        return os.path.commonpath(tuple(f.fullname for f in self.files.values()))

    def sorted_members(self) -> list[str]:
        return sorted(self.files.keys())


class Group(GroupProtocol):
    """Group class for managing files and their associated copyright/license
    information
    """

    def __init__(
        self,
        key: GroupKey | None = None,
        position: int = 0,
    ) -> None:
        self.key: GroupKey | None = key
        self.files: FileGroup = FileGroup()
        self.copyrights: CopyrightHolders = CopyrightHolders()
        self.licenses: dict[str, License] = {}
        self.license_filenames: set[str] = set()
        self.comments: list[str] = []
        self.stored: dc.FilesParagraph | None = None
        self.position: int = position

    @property
    def license(self) -> str:
        """Get the license name for this group."""
        name = " or ".join(sorted(self.licenses))
        if not name:
            if self.copyrights:
                return UNKNOWN_COPYRIGHTED
            return UNKNOWN
        return name

    def __str__(self) -> str:
        return str(self.key) if self.key else self.license

    def __repr__(self) -> str:
        return f"Group({self!s})"

    def add_file(self, fileinfo: FileInfo) -> None:
        """Add a file to this group and update associated metadata."""
        self.files.add_file(fileinfo)
        self.copyrights.merge(fileinfo.copyrights)
        for name in fileinfo.get_licenses():
            self.licenses[name] = License.get(name)
        self.license_filenames.update(fileinfo.get_license_filenames())

    def add_copyrights(self, copyrights: abc.Iterable[CopyrightHolder]) -> None:
        """Add copyright information to this group."""
        self.copyrights.extend(copyrights)

    def get_inherited_comment(self) -> str | None:
        """Get inherited comment for license files."""
        if not self.license_filenames:
            return None
        # TODO(maxy): Is this enough?
        if self.stored:
            return None
        filenames = sorted(self.license_filenames)
        return "No explicit license found, using license(s) from:\n {}".format(
            "\n ".join(filenames),
        )

    def get_comments(self) -> str:
        """Get all comments for this group."""
        comments: list[str] = []
        inherited_comment = self.get_inherited_comment()
        if inherited_comment:
            comments.append(inherited_comment)
        comments.extend(self.comments)
        return "\n .\n ".join(comments)

    def get_license_key(self) -> str:
        """Get the license key for this group."""
        return self.license

    def get_copyright_key(self) -> tuple[str, ...]:
        """Get the copyright key for this group."""
        return self.copyrights.key()

    def get_path_key(self, options: Options) -> str:
        """Get the path key for this group."""
        return self.files.path_key(options)

    def get_key(
        self,
        options: Options,
        *,
        ignore_copyright: bool = False,
    ) -> GroupKey:
        """Generate the complete key for this group based on grouping options."""
        if not self.key:
            self.key = make_group_key(
                self.get_license_key(),
                self.get_copyright_key(),
                self.get_path_key(options),
                options,
            )
        if ignore_copyright:
            return self.key[0], (), self.key[2]
        return self.key

    def copyright_block_valid(self) -> bool:
        """Check if this group can generate a valid copyright block."""
        if not self.files:
            return False
        if len(self.files) > 1:
            return True
        return bool(next(f.fullname for f in self.files))

    def sort_key(
        self,
        options: Options,
    ) -> tuple[int, int, int, int, t.Any]:
        strategy = grouping_strategy_for(options)
        debian_group = strategy.sort_band(
            self.license, self.get_path_key(options), options
        )

        return (
            debian_group,
            -1 * self.position,
            -1 * len(self.files),
            -1 * len(self.copyrights),
            self.get_key(options, ignore_copyright=True),
        )
