#!/usr/bin/env python3
# -*- coding: utf-8 -*- vim60:fdm=marker
#
# 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.

''' DEP-5 copyright handling '''

import fnmatch
import logging
import os

try:
    import regex as re
except ImportError:
    import re

import debian.copyright as dc

# Local modules
from .tree import DirInfo, FileGroup, CopyrightGroup
from .matchers import get_copyright_holders
from .datatypes import License, UNKNOWN, UNKNOWN_COPYRIGHTED


class Copyright(dc.Copyright):
    '''This class extends the debian.copyright.Copyright class, adding some
    mangling of paragraphs and the capability of interacting with the filetree
    that is the basis of how decopy works.'''

    groups = None

    @property
    def paragraphs(self):
        return self.__paragraphs

    @staticmethod
    def build(filetree, options):
        '''Creates an instance of Copyright and then populates the filetree.'''

        filename = options.copyright_file
        if not os.path.isabs(filename):
            filename = os.path.join(filetree.root, filename)

        if not os.path.exists(filename):
            return Copyright()

        try:
            with open(filename) as c:
                copyright_ = Copyright(c, strict=False)
        except dc.NotMachineReadableError:
            return Copyright()

        return copyright_

    def process(self, filetree):
        '''Match files in the filetree to copyright information.'''
        files_paragraphs = []

        # Add the license paragraphs to the License instances
        for paragraph in self.all_license_paragraphs():
            license_ = License.get(paragraph.license.synopsis)
            license_.stored = paragraph

        # Scan the files paragraphs in reverse order, that way the files should
        # match only once.
        paragraphs = list(self.all_files_paragraphs())
        for paragraph in reversed(paragraphs):
            # apply fnmatch to each file_pattern
            matched_patterns, files = \
                self._get_matched_patterns(paragraph, filetree)

            # Nothing in the paragraph matched any files. The paragraph will not
            # be included in files_paragraphs
            # TODO: for diff mode
            if not matched_patterns:
                logging.info('No matching pattern in:\n%s', paragraph.dump())
                continue

            # Some patterns (but not all) failed to match.
            # Those specific patterns get removed from the paragraph.
            # TODO: for diff mode
            if len(matched_patterns) != len(paragraph.files):
                paragraph.files = matched_patterns

            files_paragraphs.append((paragraph, files))

        self._process_groups(filetree, files_paragraphs)

    @staticmethod
    def _get_matched_patterns(paragraph, filetree):
        '''Returns the files that match the patterns for a paragraph.'''
        matched_patterns = []
        files = set()

        for file_pattern in paragraph.files:
            found = False
            for filename in fnmatch.filter(filetree.names, file_pattern):
                fileinfo = filetree[filename]

                # If the file already has a matching pattern, it was already
                # matched by previous paragraphs.
                if fileinfo.matching_pattern:
                    continue

                # Set the matched pattern
                fileinfo.matching_pattern = file_pattern
                if not isinstance(fileinfo, DirInfo):
                    files.add(filename)

                if not found:
                    matched_patterns.append(file_pattern)
                    found = True

            if not found:
                # TODO: this might be interesting to print in the diff mode
                logging.info('No match found for %s', file_pattern)

        return matched_patterns, files

    def _process_groups(self, filetree, files_paragraphs):

        self.groups = []

        for paragraph, files in files_paragraphs:

            group = Group('')
            group.stored = paragraph

            # Get license keys from the synopsis
            license_keys = re.split(' or ', paragraph.license.synopsis,
                                    flags=re.I)
            for key in license_keys:
                group.licenses[key] = License.get(key)
            # Does this file paragraph includes the license text?
            # Can we reuse it?
            if len(license_keys) == 1 \
                    and paragraph.license.text:
                license_ = License.get(license_keys[0])
                if not license_.stored:
                    license_.stored = \
                        dc.LicenseParagraph.create(paragraph.license)

            holders = []
            if paragraph.copyright:
                for value in paragraph.copyright.split('\n'):
                    value = value.lstrip()
                    new_holders, _ = get_copyright_holders(value)
                    holders.extend(new_holders)
            group.add_copyrights(holders)

            for filename in files:
                fileinfo = filetree[filename]
                fileinfo.stored_group = group
                group.add_file(fileinfo)

            if paragraph.comment:
                group.comments.append(paragraph.comment)

            self.groups.append(group)

    def remove_misplaced_files(self, options):
        '''Remove files from groups where they no longer belong.

        This method should be called after files have been processed and their
        licensing and copyright information has been obtained. Files that do not
        match their placement in the copyright file get removed from those
        groups. No files are added to existing or new groups.
        '''
        for group in self.groups:
            group_key = group.get_key(options)
            group_key_no_copyright = group.get_key(options, ignore_copyright=True)
            logging.debug("Keys: %s %s", group_key, group_key_no_copyright)

            # Make an explicit copy to be able to remove items from the list.
            for fileinfo in list(group.files):
                # In partial mode, only included files are taken into account
                if options.mode == 'partial' and not fileinfo.included:
                    group.files.del_file(fileinfo)
                    continue

                # Check if the current key for the file matches the group key.
                file_key = fileinfo.get_group_key(options)
                logging.debug("File: %s %s", fileinfo, file_key)
                remove = file_key != group_key

                # options.group_by copyright would split the parts when the
                # copyright holder is not found
                if remove and options.group_by == 'copyright':
                    remove = file_key != group_key_no_copyright

                # Either delete from the group or set the group field in the
                # fileinfo, to show that the file has matched its group.
                if remove:
                    # TODO: for diff mode
                    group.files.del_file(fileinfo)
                else:
                    # Update parsed copyrights
                    group.copyrights.merge(fileinfo.copyrights)
                    fileinfo.group = group

    def get_group_dict(self, options):
        groups = {}
        for group in self.groups:
            key = group.get_key(options)
            if key in groups:
                for fileinfo in group.files:
                    groups[key].add_file(fileinfo)
            else:
                groups[key] = group
        return groups


class Group(object):

    def __init__(self, key=None):
        self.key = key
        self.files = FileGroup()
        self.copyrights = CopyrightGroup()
        self.licenses = {}
        self.license_filenames = set()
        self.comments = []
        self.stored = None

    @property
    def license(self):
        name = ' or '.join(sorted(self.licenses))
        if not name:
            if self.copyrights:
                return UNKNOWN_COPYRIGHTED
            return UNKNOWN
        return name

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

    def __repr__(self):
        return 'Group({})'.format(str(self))

    def add_file(self, fileinfo):
        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):
        self.copyrights.extend(copyrights)

    def get_inherited_comment(self):
        if not self.license_filenames:
            return
        # TODO: Is this enough?
        if self.stored:
            return
        filenames = sorted(self.license_filenames)
        return 'No explicit license found, using license(s) from:\n {}'.format(
            '\n '.join(filenames))

    def get_comments(self):
        comments = []
        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):
        return self.license

    def get_copyright_key(self):
        return self.copyrights.key()

    def get_path_key(self, options):
        return self.files.key(options)

    def get_key(self, options, ignore_copyright=False):
        if not self.key:
            license_key = self.get_license_key()

            if options.group_by == 'copyright':
                copyright_key = self.get_copyright_key()
            else:
                copyright_key = None

            path_key = self.get_path_key(options)

            self.key = (license_key, copyright_key, path_key)
        if ignore_copyright:
            return self.key[0], tuple(), self.key[2]
        return self.key

    def copyright_block_valid(self):
        if not self.files:
            return False
        if len(self.files) > 1:
            return True
        return bool(list(f.fullname for f in self.files)[0])

    def copyright_block(self, glob_):
        if not self.copyright_block_valid():
            return ''

        if glob_:
            files = '\n       '.join(self.files.get_patterns())
        else:
            # When called with --no-glob, only list the file names
            files = '\n       '.join(self.files.sorted_members())

        block = ['Files: {files}'.format(files=files)]
        if self.copyrights:
            holders = '\n           '.join(self.copyrights.sorted_members())
            block.append('Copyright: {holders}'.format(holders=holders))
        block.append('License: {license}'.format(license=self.license))
        comments = self.get_comments()
        if comments:
            block.append('Comment: {}'.format(comments))
        # And finally add all the "unknown" fields
        if self.stored:
            for field in self.stored:
                if field in {'Files', 'Copyright', 'License', 'Comment'}:
                    continue
                block.append('{}: {}'.format(field, self.stored[field]))
        return '\n'.join(block)

    def sort_key_root(self):
        if self.files:
            root = self.files.commonpath()
        else:
            root = ''
        return root, -1 * len(self.files), -1 * len(self.copyrights)

    def sort_key(self, options):
        debian_group = 0
        if options.split_debian:
            path_key = self.get_path_key(options)
            if path_key == 'debian':
                debian_group = 1

        return debian_group, -1 * len(self.files), -1 * len(self.copyrights)

    def sort_by_files_and_holders(self, other):
        cmp_res = len(other.files) - len(self.files)
        if cmp_res == 0:
            cmp_res = len(other.copyrights) - len(self.copyrights)
        return cmp_res
