#!/usr/bin/env python3
# Copyright 2017 gRPC authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import collections
import operator
import os
import re
import subprocess

#
# Find the root of the git tree
#

git_root = (
    subprocess.check_output(["git", "rev-parse", "--show-toplevel"])
    .decode("utf-8")
    .strip()
)

#
# Parse command line arguments
#

default_out = os.path.join(git_root, ".github", "CODEOWNERS")

argp = argparse.ArgumentParser("Generate .github/CODEOWNERS file")
argp.add_argument(
    "--out",
    "-o",
    type=str,
    default=default_out,
    help="Output file (default %s)" % default_out,
)
args = argp.parse_args()

#
# Walk git tree to locate all OWNERS files
#

owners_files = [
    os.path.join(root, "OWNERS")
    for root, dirs, files in os.walk(git_root)
    if "OWNERS" in files
]

#
# Parse owners files
#

Owners = collections.namedtuple("Owners", "parent directives dir")
Directive = collections.namedtuple("Directive", "who globs")


def parse_owners(filename):
    with open(filename) as f:
        src = f.read().splitlines()
    parent = True
    directives = []
    for line in src:
        line = line.strip()
        # line := directive | comment
        if not line:
            continue
        if line[0] == "#":
            continue
        # it's a directive
        directive = None
        if line == "set noparent":
            parent = False
        elif line == "*":
            directive = Directive(who="*", globs=[])
        elif " " in line:
            (who, globs) = line.split(" ", 1)
            globs_list = [glob for glob in globs.split(" ") if glob]
            directive = Directive(who=who, globs=globs_list)
        else:
            directive = Directive(who=line, globs=[])
        if directive:
            directives.append(directive)
    return Owners(
        parent=parent,
        directives=directives,
        dir=os.path.relpath(os.path.dirname(filename), git_root),
    )


owners_data = sorted(
    [parse_owners(filename) for filename in owners_files],
    key=operator.attrgetter("dir"),
)

#
# Modify owners so that parented OWNERS files point to the actual
# Owners tuple with their parent field
#

new_owners_data = []
for owners in owners_data:
    if owners.parent == True:
        best_parent = None
        best_parent_score = None
        for possible_parent in owners_data:
            if possible_parent is owners:
                continue
            rel = os.path.relpath(owners.dir, possible_parent.dir)
            # '..' ==> we had to walk up from possible_parent to get to owners
            #      ==> not a parent
            if ".." in rel:
                continue
            depth = len(rel.split(os.sep))
            if not best_parent or depth < best_parent_score:
                best_parent = possible_parent
                best_parent_score = depth
        if best_parent:
            owners = owners._replace(parent=best_parent.dir)
        else:
            owners = owners._replace(parent=None)
    new_owners_data.append(owners)
owners_data = new_owners_data

#
# In bottom to top order, process owners data structures to build up
# a CODEOWNERS file for GitHub
#


def full_dir(rules_dir, sub_path):
    return os.path.join(rules_dir, sub_path) if rules_dir != "." else sub_path


# glob using git
gg_cache = {}


def git_glob(glob):
    global gg_cache
    if glob in gg_cache:
        return gg_cache[glob]
    r = set(
        subprocess.check_output(
            ["git", "ls-files", os.path.join(git_root, glob)]
        )
        .decode("utf-8")
        .strip()
        .splitlines()
    )
    gg_cache[glob] = r
    return r


def expand_directives(root, directives):
    globs = collections.OrderedDict()
    # build a table of glob --> owners
    for directive in directives:
        for glob in directive.globs or ["**"]:
            if glob not in globs:
                globs[glob] = []
            if directive.who not in globs[glob]:
                globs[glob].append(directive.who)
    # expand owners for intersecting globs
    sorted_globs = sorted(
        list(globs.keys()),
        key=lambda g: len(git_glob(full_dir(root, g))),
        reverse=True,
    )
    out_globs = collections.OrderedDict()
    for glob_add in sorted_globs:
        who_add = globs[glob_add]
        pre_items = [i for i in list(out_globs.items())]
        out_globs[glob_add] = who_add.copy()
        for glob_have, who_have in pre_items:
            files_add = git_glob(full_dir(root, glob_add))
            files_have = git_glob(full_dir(root, glob_have))
            intersect = files_have.intersection(files_add)
            if intersect:
                for f in sorted(files_add):  # sorted to ensure merge stability
                    if f not in intersect:
                        out_globs[os.path.relpath(f, start=root)] = who_add
                for who in who_have:
                    if who not in out_globs[glob_add]:
                        out_globs[glob_add].append(who)
    return out_globs


def add_parent_to_globs(parent, globs, globs_dir):
    if not parent:
        return
    for owners in owners_data:
        if owners.dir == parent:
            owners_globs = expand_directives(owners.dir, owners.directives)
            for oglob, oglob_who in list(owners_globs.items()):
                for gglob, gglob_who in list(globs.items()):
                    files_parent = git_glob(full_dir(owners.dir, oglob))
                    files_child = git_glob(full_dir(globs_dir, gglob))
                    intersect = files_parent.intersection(files_child)
                    gglob_who_orig = gglob_who.copy()
                    if intersect:
                        for f in sorted(
                            files_child
                        ):  # sorted to ensure merge stability
                            if f not in intersect:
                                who = gglob_who_orig.copy()
                                globs[os.path.relpath(f, start=globs_dir)] = who
                        for who in oglob_who:
                            if who not in gglob_who:
                                gglob_who.append(who)
            add_parent_to_globs(owners.parent, globs, globs_dir)
            return
    assert False


todo = owners_data.copy()
done = set()
with open(args.out, "w") as out:
    out.write("# Auto-generated by the tools/mkowners/mkowners.py tool\n")
    out.write("# Uses OWNERS files in different modules throughout the\n")
    out.write("# repository as the source of truth for module ownership.\n")
    written_globs = []
    while todo:
        head, *todo = todo
        if head.parent and not head.parent in done:
            todo.append(head)
            continue
        globs = expand_directives(head.dir, head.directives)
        add_parent_to_globs(head.parent, globs, head.dir)
        for glob, owners in list(globs.items()):
            skip = False
            for glob1, owners1, dir1 in reversed(written_globs):
                files = git_glob(full_dir(head.dir, glob))
                files1 = git_glob(full_dir(dir1, glob1))
                intersect = files.intersection(files1)
                if files == intersect:
                    if sorted(owners) == sorted(owners1):
                        skip = True  # nothing new in this rule
                        break
                elif intersect:
                    # continuing would cause a semantic change since some files are
                    # affected differently by this rule and CODEOWNERS is order dependent
                    break
            if not skip:
                out.write(
                    "/%s %s\n" % (full_dir(head.dir, glob), " ".join(owners))
                )
                written_globs.append((glob, owners, head.dir))
        done.add(head.dir)
