#!/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.

''' Generic functions used by the parsers. '''

try:
    import regex as re
except ImportError:
    import re


# Local modules
from .datatypes import CopyrightHolder, YearRange
from .res import (COMMENTS_SUBS,
                  COPYRIGHT_PRE_IGNORE_RES, COPYRIGHT_POST_IGNORE_RES,
                  COPYRIGHT_PRE_INDICATOR_RE, COPYRIGHT_INDICATOR_RE,
                  CRUFT_SUBS,
                  EMAIL_SUBS,
                  NAME_CRUFT_SUBS,
                  ARTISTIC_SUBS,
                  BSD_SUBS,
                  CC_BY_SUBS,
                  CECILL_SUBS,
                  CNRI_SUBS,
                  EFL_SUBS,
                  GNU_VERSION_SUBS,
                  GNU_EXCEPTION_SUBS,
                  GFDL_SUBS,
                  LPPL_SUBS,
                  MIT_SUBS,
                  ZPL_SUBS,
                  LICENSES_RES,
                  COMPILED_RES,
                  )


def clean_comments(text):
    # replace it's way cheaper than re.sub
    for comment in (
            # C
            '/*', '*/',
            # Lisp
            ';;',
            # (HT|X)ML
            '<!--', '-->'):
        text = text.replace(comment, ' ')
    for comment_sub in COMMENTS_SUBS:
        text = comment_sub.pattern.sub(comment_sub.repl, text)

    return text


# Copyright holders handling

# We are discarding parts that are shorter than MIN_PART_LEN
MIN_PART_LEN = 3

# Year parsing
YEAR_RE = re.compile(
    r'\s*(?:[\s:([]*)?(?P<lo>\d{2,4})[]:\s]*'
    r'(?:(?:[-~=–—]|to)[\s:[]*(?P<hi>\d{1,4})[]:\s]*)?[,/)]*',
    re.IGNORECASE)


def _get_year(text, ref=0):
    year = int(text)
    if year < 10 and ref:
        # Handle: 1990-2
        fix = 0 if (ref % 10) < year else 1
        year += ((ref // 10) + fix) * 10
    if year < 50:
        year += 2000
    if year < 100:
        year += 1900
    if year < 1000:
        return None
    return year


def _year_post_ignore(text, match):
    'Make sure we got a year'

    start = max(0, match.start('lo') - 1)
    end = min(len(text), match.end('lo') + 1)
    return bool(
        re.match(
            r'[a-zA-Z]{0}|.{0}[a-zA-Z@]'.format(match.group('lo')),
            text[start:end]))


def _split_by_years(text, years=None):
    'Split string by years found'
    years_list = [] if years is None else [years]
    parts = [] if years is None else ['']
    last_end = 0
    for year_match in YEAR_RE.finditer(text):
        if _year_post_ignore(text, year_match):
            continue

        match_dict = year_match.groupdict()

        low = _get_year(match_dict['lo'])
        if low is None:
            continue

        high = low
        if match_dict.get('hi', None):
            high = _get_year(match_dict['hi'], low)

        part_len = year_match.start(0) - last_end

        if years is None or part_len >= MIN_PART_LEN:
            parts.append(text[last_end:year_match.start(0)])
            years = YearRange(low=low, high=high)
            years_list.append(years)
        else:
            years.add(low)
            years.add(high)

        last_end = year_match.end(0)

    parts.append(text[last_end:])
    return years_list, parts


# Email parsing
EMAIL_RE = re.compile(
    r'(?:^|[,;:\s<>/\\([])\s*(?P<email>[^]),;:\s<>/\\([]+?@[^]),;:\s<>@/\\([]+?)\s*(?:[]),;:\s/\\<>]|$)')


def _get_holders_emails(text):

    holders_emails = []

    # De-cruft email
    for sub in EMAIL_SUBS:
        text = sub.pattern.sub(sub.repl, text)

    last_end = 0
    for email_match in EMAIL_RE.finditer(text):
        email = email_match.group('email')
        name = text[last_end:email_match.start()]
        last_end = email_match.end()
        holders_emails.append((name, email))
    if len(text) - last_end >= MIN_PART_LEN:
        holders_emails.append((text[last_end:], ''))
    return holders_emails


def _process_name_email_pairs(name_email_pairs, holders, years):
    years_used = False
    for name, email in name_email_pairs:
        # Fixup name
        if name is None:
            name = ''
        name = name.strip(''',.;*'"@-–—[]{} \t''')
        for sub in NAME_CRUFT_SUBS:
            name = sub.pattern.sub(sub.repl, name)
        if len(name) < MIN_PART_LEN:
            name = ''
        if not name and not email:
            if years and len(holders) and not holders[-1].years:
                holders[-1].years.merge(years)
                years = None
            continue
        if (not name and not years) \
                and (email and len(holders) and not holders[-1].email):
            holders[-1].email = email
            continue
        if (not email and not years) \
                and (name and len(holders) and not holders[-1].name):
            holders[-1].name = name
            continue
        holders.append(CopyrightHolder(name, email, years))
        years_used = True
    return None if years_used else years


def _get_holders_years(text, years=None):
    'Get pairs [(years, text)]'
    years_list, parts = _split_by_years(text, years)
    if len(parts[0]) < MIN_PART_LEN:
        return zip(years_list, parts[1:])
    if len(parts[-1]) < MIN_PART_LEN:
        return zip(years_list, parts)
    years_list = [YearRange()] + years_list
    return zip(years_list, parts)


HOLDER_RE = re.compile(r'\s*(?:by\s*)?(?P<holder>\S.*?\S)[\s"\*,;/]*$', re.I)


# Copyright holders factory
def get_copyright_holders(copyright_, years=None):

    years_text_pairs = _get_holders_years(copyright_, years)

    holders = []
    # To be merged or leftover to be returned
    pending_years = None

    for years, text in years_text_pairs:
        if years and pending_years:
            pending_years.merge(years)
        elif pending_years is None or years:
            pending_years = years
        match = HOLDER_RE.match(text)
        if not match:
            # Discard current text, but keep what we found so far
            if pending_years and len(holders) and not holders[-1].years:
                holders[-1].years.merge(pending_years)
                pending_years = None
            continue

        stripped_text = match.group('holder')
        name_email_pairs = _get_holders_emails(stripped_text)

        pending_years = _process_name_email_pairs(
            name_email_pairs, holders, pending_years)

    return holders, pending_years


def _parse_copyright_continuation(text, continuation, years=None):

    if not continuation:
        return '', None
    prefix = continuation.string[:continuation.start(continuation.lastindex)]
    # Special case for C style multiline comments
    alt_prefix = prefix.replace('/*', ' *')
    # normalize spaces
    alt_prefix = alt_prefix.replace('\t', '  ')
    alt_text = text.replace('\t', '  ')
    if not text.startswith(prefix) and not alt_text.startswith(alt_prefix):
        return '', None
    cont_match = re.match(r'\s+', text[len(prefix):]) \
        or re.match(r'\s+', alt_text[len(alt_prefix):])
    if not cont_match and years:
        rest = text[len(prefix):]
    elif not cont_match:
        return '', None
    else:
        rest = cont_match.string[cont_match.end():]
    # If there is nothing else but empty text, drop the continuation
    if re.match(r'\s*$', rest):
        return '', None
    match = continuation
    return rest, match


def _re_split_iter(pattern, text):
    end = 0
    for indicator_match in COPYRIGHT_INDICATOR_RE.finditer(text):
        part = text[end:indicator_match.start()]
        end = indicator_match.end()
        yield part
    yield text[end:]


def parse_copyright(text, continuation=None, years=None):

    if len(text) < 3:
        return None, None

    match = COPYRIGHT_INDICATOR_RE.search(text)
    if match:
        rest = match.string[match.end():]
    else:
        # Process continuations
        rest, match = _parse_copyright_continuation(text, continuation, years)
        if not match:
            return None, None

    # if COPYRIGHT_PRE_IGNORE_RE.search(text) or
    #         COPYRIGHT_POST_IGNORE_RE.search(rest)
    if any(
        ignore_pattern.search(text)
        for ignore_pattern in COPYRIGHT_PRE_IGNORE_RES
    ) or any(
        ignore_pattern.search(rest)
        for ignore_pattern in COPYRIGHT_POST_IGNORE_RES
    ):
        return None, None

    copyrights = []

    for i, part in enumerate(_re_split_iter(COPYRIGHT_INDICATOR_RE, rest)):
        if not part:
            continue
        for sub in CRUFT_SUBS:
            part = sub.pattern.sub(sub.repl, part)
        if part and len(part) > 2:
            copyrights.append(part)

    return copyrights, match


def _add_new_holders(holders, copyrights, years):

    # copyrights is a list of strings, that were split by copyright
    # indicators.
    for copyright_ in copyrights:
        new_holders, years = get_copyright_holders(copyright_, years)
        if new_holders:
            holders.extend(new_holders)
        elif years:
            # If we found a year, but not a holder, maybe the previous
            # holder is missing the years
            # Example: Copyright Maximiliano Curia (c) 2018
            if len(holders) and not holders[-1].years:
                holders[-1].years.merge(years)
                years = None
    # The years might still be relevant for the next copyright holder.
    return years


def _iter_lines(text, start=0):
    ''' Line iterator '''

    end = start - 1
    while True:
        start = end + 1
        end = text.find('\n', start)
        if end <= 0:
            break
        yield text[start:end], start, end
    if text[start:]:
        yield text[start:], start, len(text)


def parse_holders(content):
    ''' Find the copyright holders specified in content '''

    # list containing the copyrightHolders found
    holders = []

    # last position checked
    last_seen = -1

    # Iterate only for lines that might have a copyright indicator.
    for copyright_match in COPYRIGHT_PRE_INDICATOR_RE.finditer(content):
        # Already seen
        if copyright_match.start() < last_seen:
            continue

        match_start = copyright_match.start()
        line_start = content.rfind('\n', 0, match_start) + 1

        # Initialize the parsing state (continuation and years)
        # continuation is set to the last copyright indicator found, that way we
        # can check the following lines, and correctly detect multiple copyright
        # holders after a single copyright indicator. Example:
        # Copyright:
        #  Maximiliano Curia
        #  Margarita Manterola
        continuation = None
        # Keep years, to parse things like:
        #  Copyright 2018
        #    Maximiliano Curia
        years = None

        for line, _, last_seen in _iter_lines(content, line_start):
            copyrights, continuation = parse_copyright(line, continuation, years)
            # An empty continuation means that the copyright block ended.
            if not continuation:
                # remain calm and carry on in the outside iteration
                break
            if not copyrights:
                # A line without indicators, drop the cached years
                years = None
                continue

            # parse copyrights (list of strings) and create CopyrightHolders
            # with that information. Keep the years for the next iteration.
            years = _add_new_holders(holders, copyrights, years)

    return holders


# License handling

def _parse_subs_first(text, fallback, subs):
    for sub in subs:
        match = sub.pattern.search(text)
        if not match:
            continue
        return match.expand(sub.repl).rstrip('.0')

    return fallback


def _parse_subs_all(text, subs):
    found = []
    for sub in subs:
        match = sub.pattern.search(text)
        if not match:
            continue
        found.append(match.expand(sub.repl).rstrip('.0'))

    return found


def parse_artistic(text, match, group):
    return _parse_subs_first(text, group, ARTISTIC_SUBS)


def parse_bsd(text, match, group):
    return _parse_subs_first(text, group, BSD_SUBS)


def parse_cc_by(text, match, group):
    details = _parse_subs_all(text, CC_BY_SUBS)
    return group + ''.join(details)


def parse_cecill(text, match, group):
    return _parse_subs_first(text, group, CECILL_SUBS)


def parse_cnri(text, match, group):
    return _parse_subs_first(text, group, CNRI_SUBS)


def parse_efl(text, match, group):
    return _parse_subs_first(text, group, EFL_SUBS)


def parse_gpl(text, match, group):
    version = _parse_subs_first(text, '', GNU_VERSION_SUBS)

    exceptions = _parse_subs_all(text, GNU_EXCEPTION_SUBS)

    exception = ' with {} exception'.format(
        '_'.join(sorted(exceptions))) if exceptions else ''

    return '{}{}{}'.format(group,
                           '-{}'.format(version.rstrip('0.')) if version else '',
                           exception)


def parse_gfdl(text, match, group):
    details = _parse_subs_all(text, GFDL_SUBS)
    return group + ''.join(details)


def parse_lppl(text, match, group):
    details = _parse_subs_all(text, LPPL_SUBS)
    return group + ''.join(details)


def parse_mit(text, match, group):
    return _parse_subs_first(text, group, MIT_SUBS)


def parse_zpl(text, match, group):
    details = _parse_subs_all(text, ZPL_SUBS)
    return group + ''.join(details)


PARSE_DETAILS = {
    'Artistic': parse_artistic,
    'BSD': parse_bsd,
    'CC-BY': parse_cc_by,
    'GPL': parse_gpl,
    'LGPL': parse_gpl,
    'GFDL': parse_gfdl,
    'LPPL': parse_lppl,
    'ZPL': parse_zpl,
    'AGPL': parse_gpl,
    'CeCILL': parse_cecill,
    'CNRI': parse_cnri,
    'EFL': parse_efl,
    'MIT/X11': parse_mit,
}


def find_licenses(text):
    '''Scan the text for presence of any of the supported licenses.

    Returns:
        A list of the license ids found in the text.
    '''
    licenses = {}

    for i, license_re in enumerate(LICENSES_RES):
        # Keep the license name in this frame for easier debugging
        license_ = license_re.license
        if license_re.license in licenses:
            continue
        match = COMPILED_RES[i].search(text)
        if not match:
            continue
        if license_re.get_detail:
            license_ = license_re.get_detail(text, match, license_)
        elif license_ in PARSE_DETAILS:
            license_ = PARSE_DETAILS[license_](text, match, license_)
        licenses[license_re.license] = license_

    return licenses.values()
