#!/usr/bin/python3
"""
Do you have a lot of git repositories, distributed over your whole system?
Would you like to manage them using a simple command, without the need to constantly
switch directories?

Use multi-git to manage these repos.

First use
multi-git add /path/to/git/repo -t tag1
to add git repos. Then, use the standard git commands status, commit, pull and
push to execute those commands over all managed git repos. To limit the set of
repos, you can use the -t (--tags) argument, that selects all repos that have
at least one of these tags set. For example,
multi-git pull
will pull the latest changes for all git repos that are managed by multi-git.
As another exampe,
multi-git status
or
multi-git st
will display all changes in all managed repos. As already mentioned, -t or --tags
will limit the set of repos:
multi-git st -t work
"""

import argparse
import os
import subprocess

from config_path import ConfigPath
import toml

config_path = ConfigPath('multi-git', 'danielkullmann.de', '.toml')
base_path = config_path.saveFolderPath(mkdir = True)
CONFIG_FILE_NAME = base_path / 'multi-git.toml'
CONFIG = {}

def load_config():
    config = {}
    if CONFIG_FILE_NAME.exists():
        with open(CONFIG_FILE_NAME, 'r') as file_handle:
            config = toml.load(file_handle)
    else:
        print("WARN", "config file does not exist", CONFIG_FILE_NAME)

    make_config_path(config, ["config"])
    make_config_path(config, ["repos"], [])
    return config

def save_config(config):
    with open(CONFIG_FILE_NAME, 'w') as file_handle:
        toml.dump(config, file_handle)

def parse_tags(value):
    return value.split(",")

def absolute_path(value):
    return os.path.abspath(value)

def make_config_path(config, path_parts, value=None):
    current = config
    for index, part in enumerate(path_parts):
        if part not in current:
            if index == len(path_parts)-1 and value is not None:
                current[part] = value
            else:
                current[part] = {}
        current = current[part]

def tags_match(wanted_tags, given_tags):
    for tag in wanted_tags:
        if tag in given_tags:
            return True
    return False

def path_match(wanted_paths, repo_path):
    for path in wanted_paths:
        if path in repo_path:
            return True
    return False


def filter_repos(arguments):
    repos = CONFIG["repos"]

    if arguments.tags is not None and len(arguments.tags) > 0:
        repos = [r for r in repos if tags_match(arguments.tags, r["tags"])]
    if arguments.path is not None and len(arguments.path) > 0:
        repos = [r for r in repos if path_match(arguments.path, r["path"])]
    return repos

def command_list(arguments):
    repos = filter_repos(arguments)
    for repo in repos:
        print(repo["path"], ",".join(repo["tags"]))

def command_config(arguments):
    if arguments.edit:
        editor = os.environ.get("EDITOR", None)
        if editor:
            subprocess.run([editor, CONFIG_FILE_NAME], check=False)
        else:
            print("ERROR", "$EDITOR variable is not set")
    else:
        print("config file:", CONFIG_FILE_NAME)

def command_add(arguments):
    path = arguments.path
    exists = len(list(r for r in CONFIG["repos"] if r["path"] == path)) > 0
    if exists:
        print("ERROR", "path", path, "is already tracked")
    else:
        entry = {
            "path": path,
            "tags": arguments.tags or [],
            "auto-commit": arguments.auto_commit or False,
            "auto-commit-message": arguments.auto_commit_message or None
        }
        repos = CONFIG["repos"]
        repos.append(entry)

def command_edit(arguments):
    path = arguments.path
    existing = list(r for r in CONFIG["repos"] if r["path"] == path)
    if len(existing) == 0:
        print("ERROR", "path", path, "is not tracked")
    else:
        entry = existing[0]
        if arguments.tags is not None:
            entry["tags"] = arguments.tags
        if arguments.auto_commit is not None:
            entry["auto-commit"] = arguments.auto_commit
        if arguments.auto_commit_message is not None:
            entry["auto-commit-message"] = arguments.auto_commit_message

def command_status(arguments):
    repos = filter_repos(arguments)
    for repo in repos:
        print("#", repo["path"])
        subprocess.run(["git", "status", "-s"], cwd=repo["path"], check=False)
        print("")

def command_auto_commit(arguments):
    repos = filter_repos(arguments)
    for repo in repos:
        print("#", repo["path"])
        commit_message = repo.auto_commit_message
        if not commit_message:
            print("WARN", "no auto-commit message set!")
        else:
            command = ["git", "commit", "--message", commit_message, "."]
            subprocess.run(command, cwd=repo["path"], check=False)
            print("INFO", "Changes committed")
        print("")

def command_commit(arguments):
    repos = filter_repos(arguments)
    for repo in repos:
        print("#", repo["path"])
        commit_message = arguments.message
        extra_arguments = []
        if arguments.patch:
            extra_arguments.append("--patch")
        if not commit_message:
            print("WARN", "no commit message given!")
        else:
            subprocess.run(
                ["git", "commit", "--message", commit_message, "."] + extra_arguments,
                cwd=repo["path"],
                check=False
            )
        print("")

def command_pull(arguments):
    repos = filter_repos(arguments)
    for repo in repos:
        print("#", repo["path"])
        subprocess.run(["git", "pull"], cwd=repo["path"], check=False)
        print("")

def command_push(arguments):
    repos = filter_repos(arguments)
    for repo in repos:
        print("#", repo["path"])
        subprocess.run(["git", "push"], cwd=repo["path"], check=False)
        print("")

def add_filter_arguments(argument_parser):
    argument_parser.add_argument('-t', '--tags', type=parse_tags)
    argument_parser.add_argument('-p', '--path', type=parse_tags)

def parse_arguments():
    parser = argparse.ArgumentParser(description=__doc__)
    subparsers = parser.add_subparsers(dest='command')
    subparsers.required = True

    list_parser = subparsers.add_parser('list')
    add_filter_arguments(list_parser)
    list_parser.set_defaults(func=command_list)

    config_parser = subparsers.add_parser('config')
    config_parser.add_argument("-e", "--edit", action="store_true")
    config_parser.set_defaults(func=command_config)

    add_parser = subparsers.add_parser('add')
    add_parser.add_argument('path', type=absolute_path)
    add_parser.add_argument('-a', '--auto-commit', action="store_true")
    add_parser.add_argument('-m', '--auto-commit-message', type=str)
    add_parser.add_argument('-t', '--tags', type=parse_tags)
    add_parser.set_defaults(func=command_add)

    edit_parser = subparsers.add_parser('edit')
    edit_parser.add_argument('path', type=absolute_path)
    edit_parser.add_argument('-a', '--auto-commit', action="store_true")
    edit_parser.add_argument('-m', '--auto-commit-message', type=str)
    edit_parser.add_argument('-t', '--tags', type=parse_tags)
    edit_parser.set_defaults(func=command_edit)

    status_parser = subparsers.add_parser('status', aliases=["st"])
    add_filter_arguments(status_parser)
    status_parser.set_defaults(func=command_status)

    commit_parser = subparsers.add_parser('commit', aliases=["ci"])
    add_filter_arguments(commit_parser)
    commit_parser.add_argument('-m', '--message', type=str)
    commit_parser.add_argument('-P', '--patch', action="store_true")
    commit_parser.set_defaults(func=command_commit)

    ac_parser = subparsers.add_parser('auto-commit', aliases=["ac"])
    add_filter_arguments(ac_parser)
    ac_parser.set_defaults(func=command_auto_commit)

    pull_parser = subparsers.add_parser('pull')
    add_filter_arguments(pull_parser)
    pull_parser.set_defaults(func=command_pull)

    push_parser = subparsers.add_parser('push')
    add_filter_arguments(push_parser)
    push_parser.set_defaults(func=command_push)

    arguments = parser.parse_args()
    return arguments

if __name__ == "__main__":
    CONFIG = load_config()
    all_arguments = parse_arguments()
    all_arguments.func(all_arguments)
    save_config(CONFIG)
