#!/usr/bin/env python3
#
# Copyright OpenEmbedded Contributors
#
# SPDX-License-Identifier: MIT
#
# This script is to be called by b4:
# - through the b4.prep-perpatch-check-cmd with "prep-perpatch-check-cmd" as
#   first argument,
# - through b4.send-auto-cc-cmd with "send-auto-cc-cmd" as first argument,
# - through b4.send-auto-to-cmd with "send-auto-to-cmd" as first argument,
#
# When prep-perpatch-check-cmd is passsed:
#
#  This checks that a patch makes changes to at most one project in the poky
#  combo repo (that is, out of yocto-docs, bitbake, openembedded-core combined
#  into poky and the poky-specific files).
#
#  Printing something to stdout in this file will result in b4 prep --check fail
#  for the currently parsed patch.
#
#  It checks that all patches in the series make changes to at most one project.
#
# When send-auto-cc-cmd is passed:
#
#  This returns the list of Cc recipients for a patch.
#
# When send-auto-to-cmd is passed:
#
#  This returns the list of To recipients for a patch.
#
# This script takes as stdin a patch.

import pathlib
import re
import shutil
import subprocess
import sys

cmd = sys.argv[1]

patch = sys.stdin.readlines()

# Subject field is used to identify the last patch as this script is called for
# each patch. We edit the same file in a series by using the References field
# unique identifier to check which projects are modified by earlier patches in
# the series. To avoid cluttering the disk, the last patch in the list removes
# that shared file.
re_subject = re.compile(r'^Subject:.*\[.*PATCH.*\s(\d+)/\1')
re_ref = re.compile(r'^References: <(.*)>$')

subject = None
ref = None

if not shutil.which("lsdiff"):
    print("lsdiff missing from host, please install patchutils", file=sys.stderr)
    sys.exit(-1)

try:
    one_patch_series = False
    for line in patch:
        subject = re_subject.match(line)
        if subject:
            # Handle [PATCH 1/1]
            if subject.group(1) == 1:
                one_patch_series = True
            break
        if re.match(r'^Subject: .*\[.*PATCH[^/]*\]', line):
            # Single patch is named [PATCH] but if there are prefix, it could be
            # [PATCH prefix], so handle everything that doesn't have a /
            # character which is used as separator between current patch number
            # and total patch number
            one_patch_series = True
            break

    if cmd == "prep-perpatch-check-cmd" and not one_patch_series:
        for line in patch:
            ref = re_ref.match(line)
            if ref:
                break

        if not ref:
            print("Failed to find ref to cover letter (References:)...", file=sys.stderr)
            sys.exit(-2)

        ref = ref.group(1)
        series_check = pathlib.Path(f".tmp-{ref}")

    patch = "".join(patch)

    if cmd == "send-auto-cc-cmd":
        # Patches to BitBake documentation should also go to yocto-docs mailing list
        project_paths = {
                "yocto-docs": ["bitbake/doc/*"],
        }
    else:
        project_paths = {
                "bitbake": ["bitbake/*"],
                "yocto-docs": ["documentation/*"],
                "poky": [
                    "meta-poky/*",
                    "meta-yocto-bsp/*",
                    "README.hardware.md",
                    "README.poky.md",
                    # scripts/b4-wrapper-poky.py is only run by b4 when in poky
                    # git repo. With that limitation, changes made to .b4-config
                    # can only be for poky's and not OE-Core's as only poky's is
                    # stored in poky git repo.
                    ".b4-config",
                    ],
        }

    # List of projects touched by this patch
    projs = []

    # Any file not matched by any path in project_paths means it is from
    # OE-Core.
    # When matching some path in project_paths, remove the matched files from
    # that list.
    files_left = subprocess.check_output(["lsdiff", "--strip-match=1", "--strip=1"],
                                         input=patch, text=True)
    files_left = set(files_left)

    for proj, proj_paths in project_paths.items():
        lsdiff_args = [f"--include={path}" for path in proj_paths]
        files = subprocess.check_output(["lsdiff", "--strip-match=1", "--strip=1"] + lsdiff_args,
                                        input=patch, text=True)
        if len(files):
            files_left = files_left - set(files)
            projs.append(proj)
            continue

        # Handle patches made with --no-prefix
        files = subprocess.check_output(["lsdiff"] + lsdiff_args,
                                        input=patch, text=True)
        if len(files):
            files_left = files_left - set(files)
            projs.append(proj)

    # Catch-all for everything not poky-specific or in bitbake/yocto-docs
    if len(files_left) and cmd != "send-auto-cc-cmd":
        projs.append("openembedded-core")

    if cmd == "prep-perpatch-check-cmd":
        if len(projs) > 1:
            print(f"Diff spans more than one project ({', '.join(sorted(projs))}), split into multiple commits...",
                  file=sys.stderr)
            sys.exit(-3)

        # No need to check other patches in the series as there aren't any
        if one_patch_series:
            sys.exit(0)

        # This should be replaced once b4 supports prep-perseries-check-cmd (or something similar)

        if series_check.exists():
            # NOT race-free if b4 decides to parallelize prep-perpatch-check-cmd
            series_projs = series_check.read_text().split('\n')
        else:
            series_projs = []

        series_projs += projs
        uniq_series_projs = set(series_projs)
        # NOT race-free, if b4 decides to parallelize prep-perpatch-check-cmd
        series_check.write_text('\n'.join(uniq_series_projs))

        if len(uniq_series_projs) > 1:
            print(f"Series spans more than one project ({', '.join(sorted(uniq_series_projs))}), split into multiple series...",
                  file=sys.stderr)
            sys.exit(-4)
    else:  # send-auto-cc-cmd / send-auto-to-cmd
        ml_projs = {
            "bitbake": "bitbake-devel@lists.openembedded.org",
            "yocto-docs": "docs@lists.yoctoproject.org",
            "poky": "poky@lists.yoctoproject.org",
            "openembedded-core": "openembedded-core@lists.openembedded.org",
        }

        print("\n".join([ml_projs[ml] for ml in projs]))

    sys.exit(0)
finally:
    # Last patch in the series, cleanup tmp file
    if subject and ref and series_check.exists():
        series_check.unlink()
