## @file
#  Retrieves the people to request review from on submission of a commit.
#
#  Copyright (c) 2019, Linaro Ltd. All rights reserved.<BR>
#
#  SPDX-License-Identifier: BSD-2-Clause-Patent
#

from __future__ import print_function
from collections import defaultdict
from collections import OrderedDict
import argparse
import os
import re
import SetupGit

EXPRESSIONS = {
    'exclude':    re.compile(r'^X:\s*(?P<exclude>.*?)\r*$'),
    'file':       re.compile(r'^F:\s*(?P<file>.*?)\r*$'),
    'list':       re.compile(r'^L:\s*(?P<list>.*?)\r*$'),
    'maintainer': re.compile(r'^M:\s*(?P<maintainer>.*?)\r*$'),
    'reviewer':   re.compile(r'^R:\s*(?P<reviewer>.*?)\r*$'),
    'status':     re.compile(r'^S:\s*(?P<status>.*?)\r*$'),
    'tree':       re.compile(r'^T:\s*(?P<tree>.*?)\r*$'),
    'webpage':    re.compile(r'^W:\s*(?P<webpage>.*?)\r*$')
}

def printsection(section):
    """Prints out the dictionary describing a Maintainers.txt section."""
    print('===')
    for key in section.keys():
        print("Key: %s" % key)
        for item in section[key]:
            print('  %s' % item)

def pattern_to_regex(pattern):
    """Takes a string containing regular UNIX path wildcards
       and returns a string suitable for matching with regex."""

    pattern = pattern.replace('.', r'\.')
    pattern = pattern.replace('?', r'.')
    pattern = pattern.replace('*', r'.*')

    if pattern.endswith('/'):
        pattern += r'.*'
    elif pattern.endswith('.*'):
        pattern = pattern[:-2]
        pattern += r'(?!.*?/.*?)'

    return pattern

def path_in_section(path, section):
    """Returns True of False indicating whether the path is covered by
       the current section."""
    if not 'file' in section:
        return False

    for pattern in section['file']:
        regex = pattern_to_regex(pattern)

        match = re.match(regex, path)
        if match:
            # Check if there is an exclude pattern that applies
            for pattern in section['exclude']:
                regex = pattern_to_regex(pattern)

                match = re.match(regex, path)
                if match:
                    return False

            return True

    return False

def get_section_maintainers(path, section):
    """Returns a list with email addresses to any M: and R: entries
       matching the provided path in the provided section."""
    maintainers = []
    reviewers = []
    lists = []
    nowarn_status = ['Supported', 'Maintained']

    if path_in_section(path, section):
        for status in section['status']:
            if status not in nowarn_status:
                print('WARNING: Maintained status for "%s" is \'%s\'!' % (path, status))
        for address in section['maintainer']:
            # Convert to list if necessary
            if isinstance(address, list):
                maintainers += address
            else:
                maintainers += [address]
        for address in section['reviewer']:
            # Convert to list if necessary
            if isinstance(address, list):
                reviewers += address
            else:
                reviewers += [address]
        for address in section['list']:
            # Convert to list if necessary
            if isinstance(address, list):
                lists += address
            else:
                lists += [address]

    return {'maintainers': maintainers, 'reviewers': reviewers, 'lists': lists}

def get_maintainers(path, sections, level=0):
    """For 'path', iterates over all sections, returning maintainers
       for matching ones."""
    maintainers = []
    reviewers = []
    lists = []
    for section in sections:
        recipients = get_section_maintainers(path, section)
        maintainers += recipients['maintainers']
        reviewers += recipients['reviewers']
        lists += recipients['lists']

    if not maintainers:
        # If no match found, look for match for (nonexistent) file
        # REPO.working_dir/<default>
        print('"%s": no maintainers found, looking for default' % path)
        if level == 0:
            recipients = get_maintainers('<default>', sections, level=level + 1)
            maintainers += recipients['maintainers']
            reviewers += recipients['reviewers']
            lists += recipients['lists']
        else:
            print("No <default> maintainers set for project.")
        if not maintainers:
            return None

    return {'maintainers': maintainers, 'reviewers': reviewers, 'lists': lists}

def parse_maintainers_line(line):
    """Parse one line of Maintainers.txt, returning any match group and its key."""
    for key, expression in EXPRESSIONS.items():
        match = expression.match(line)
        if match:
            return key, match.group(key)
    return None, None

def parse_maintainers_file(filename):
    """Parse the Maintainers.txt from top-level of repo and
       return a list containing dictionaries of all sections."""
    with open(filename, 'r') as text:
        line = text.readline()
        sectionlist = []
        section = defaultdict(list)
        while line:
            key, value = parse_maintainers_line(line)
            if key and value:
                section[key].append(value)

            line = text.readline()
            # If end of section (end of file, or non-tag line encountered)...
            if not key or not value or not line:
                # ...if non-empty, append section to list.
                if section:
                    sectionlist.append(section.copy())
                    section.clear()

        return sectionlist

def get_modified_files(repo, args):
    """Returns a list of the files modified by the commit specified in 'args'."""
    commit = repo.commit(args.commit)
    return commit.stats.files

if __name__ == '__main__':
    PARSER = argparse.ArgumentParser(
        description='Retrieves information on who to cc for review on a given commit')
    PARSER.add_argument('commit',
                        action="store",
                        help='git revision to examine (default: HEAD)',
                        nargs='?',
                        default='HEAD')
    PARSER.add_argument('-l', '--lookup',
                        help='Find section matches for path LOOKUP',
                        required=False)
    ARGS = PARSER.parse_args()

    REPO = SetupGit.locate_repo()

    CONFIG_FILE = os.path.join(REPO.working_dir, 'Maintainers.txt')

    SECTIONS = parse_maintainers_file(CONFIG_FILE)

    if ARGS.lookup:
        FILES = [ARGS.lookup.replace('\\','/')]
    else:
        FILES = get_modified_files(REPO, ARGS)

    # Accumulate a sorted list of addresses
    ADDRESSES = set([])
    for file in FILES:
        print(file)
        recipients = get_maintainers(file, SECTIONS)
        ADDRESSES |= set(recipients['maintainers'] + recipients['reviewers'] + recipients['lists'])
    ADDRESSES = list(ADDRESSES)
    ADDRESSES.sort()

    for address in ADDRESSES:
        if '<' in address and '>' in address:
            address = address.split('>', 1)[0] + '>'
        print('  %s' % address)
