#!/usr/bin/env python3

import os
import sys
import subprocess
import argparse
from typing import Iterable
from pathlib import Path
from lint.util import EXTENSIONS_TO_CHECK

CLANG_FORMAT_EXTS = set([".m", ".mm", ".h"])


def sort_forward_decl_statement_block(text):
    lines = text.split("\n")
    lines = [line.strip() for line in lines if line.strip()]
    lines = list(set(lines))
    lines.sort()
    return "\n" + "\n".join(lines) + "\n"


def find_matching_section(text, match_test):
    lines = text.split("\n")
    first_matching_line_index = None
    for index, line in enumerate(lines):
        if match_test(line):
            first_matching_line_index = index
            break

    if first_matching_line_index is None:
        return None

    # Absorb any leading empty lines.
    while first_matching_line_index > 0:
        prev_line = lines[first_matching_line_index - 1]
        if prev_line.strip() != "":
            break
        first_matching_line_index = first_matching_line_index - 1

    first_non_matching_line_index = None
    for index, line in enumerate(lines[first_matching_line_index:]):
        if line.strip() == "":
            # Absorb any trailing empty lines.
            continue
        if not match_test(line):
            first_non_matching_line_index = index + first_matching_line_index
            break

    text0 = "\n".join(lines[:first_matching_line_index])
    if first_non_matching_line_index is None:
        text1 = "\n".join(lines[first_matching_line_index:])
        text2 = None
    else:
        text1 = "\n".join(
            lines[first_matching_line_index:first_non_matching_line_index]
        )
        text2 = "\n".join(lines[first_non_matching_line_index:])

    return text0, text1, text2


def sort_matching_blocks(sort_name, filepath, text, match_func, sort_func):
    unprocessed = text
    processed = None
    while True:
        section = find_matching_section(unprocessed, match_func)
        if not section:
            if processed:
                processed = "\n".join((processed, unprocessed))
            else:
                processed = unprocessed
            break

        text0, text1, text2 = section

        if processed:
            processed = "\n".join((processed, text0))
        else:
            processed = text0

        text1 = sort_func(text1)
        processed = "\n".join((processed, text1))
        if text2:
            unprocessed = text2
        else:
            break

    if text != processed:
        print(sort_name, filepath)
    return processed


def find_forward_class_statement_section(text):
    def is_forward_class_statement(line):
        return line.strip().startswith("@class ")

    return find_matching_section(text, is_forward_class_statement)


def find_forward_protocol_statement_section(text):
    def is_forward_protocol_statement(line):
        return line.strip().startswith("@protocol ") and line.strip().endswith(";")

    return find_matching_section(text, is_forward_protocol_statement)


def sort_forward_class_statements(filepath, file_extension, text):
    if file_extension not in (".h", ".m", ".mm"):
        return text
    return sort_matching_blocks(
        "sort_class_statements",
        filepath,
        text,
        find_forward_class_statement_section,
        sort_forward_decl_statement_block,
    )


def sort_forward_protocol_statements(filepath, file_extension, text):
    if file_extension not in (".h", ".m", ".mm"):
        return text
    return sort_matching_blocks(
        "sort_forward_protocol_statements",
        filepath,
        text,
        find_forward_protocol_statement_section,
        sort_forward_decl_statement_block,
    )


def get_ext(file: str) -> str:
    return os.path.splitext(file)[1]


def process(filepath):
    file_ext = get_ext(filepath)

    with open(filepath, "rt") as f:
        text = f.read()

    original_text = text

    text = sort_forward_class_statements(filepath, file_ext, text)
    text = sort_forward_protocol_statements(filepath, file_ext, text)
    text = text.strip() + "\n"

    if original_text == text:
        return

    with open(filepath, "wt") as f:
        f.write(text)


def get_file_paths_for_commit(commit):
    return (
        subprocess.run(
            ["git", "diff", "--name-only", "--diff-filter=ACMR", commit],
            check=True,
            capture_output=True,
            encoding="utf8",
        )
        .stdout.rstrip()
        .split("\n")
    )


def should_process_file(file_path: str) -> bool:
    if get_ext(file_path) not in EXTENSIONS_TO_CHECK:
        return False

    for component in Path(file_path).parts:
        if component.startswith("."):
            return False
        if component in ("Pods", "ThirdParty"):
            return False
        if component.startswith("MobileCoinExternal."):
            return False

    return True


def swiftlint(file_paths):
    file_paths = list(filter(lambda f: get_ext(f) == ".swift", file_paths))
    if len(file_paths) == 0:
        return True

    subprocess.run(["swiftlint", "lint", "--quiet", "--fix", *file_paths])
    proc = subprocess.run(["swiftlint", "lint", "--quiet", "--strict", *file_paths])

    return proc.returncode == 0


def clang_format(file_paths):
    file_paths = list(filter(lambda f: get_ext(f) in CLANG_FORMAT_EXTS, file_paths))
    if len(file_paths) == 0:
        return True
    proc = subprocess.run(["clang-format", "-i", *file_paths])
    return proc.returncode == 0


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="lint & format files")
    parser.add_argument("path", nargs="*", help="a path to process")
    parser.add_argument(
        "--ref",
        metavar="commit-sha",
        help="process paths that have changed since this commit",
    )
    ns = parser.parse_args()

    if len(ns.path) > 0:
        file_paths = ns.path
    else:
        file_paths = get_file_paths_for_commit(ns.ref or "HEAD")
    file_paths = sorted(set(filter(should_process_file, file_paths)))

    result = True

    print("Checking license headers...", flush=True)
    proc = subprocess.run(["Scripts/lint/lint-license-headers", "--fix", *file_paths])
    if proc.returncode != 0:
        result = False
    print("")

    print("Running swiftlint...", flush=True)
    if not swiftlint(file_paths):
        result = False
    print("")

    print("Sorting forward declarations...", flush=True)
    for file_path in file_paths:
        process(file_path)
    print("")

    print("Sorting Xcode project...", flush=True)
    proc = subprocess.run(["Scripts/sort-Xcode-project-file", "Signal.xcodeproj"])
    if proc.returncode != 0:
        result = False
    print("")

    print("Running clang-format...", flush=True)
    if not clang_format(file_paths):
        result = False
    print("")

    if not result:
        print("Some errors couldn't be fixed automatically.")
        sys.exit(1)
