#!/usr/bin/env python

# A simple setup script for the packages.
# There should be a file named `locations.json` in this setup where it contains a top-level associative array with the packages as the key and their target path as the value.
# Feel free to modify it accordingly.

# This script is tailored to my specific needs.
# It also strives to only rely on the standard library so further no installation needed.
# Feel free to modify this script as well.

# For future references, the Python version when creating for this script is v3.8.2.
# If there's any reason why stuff is not working, it might be because it is different on the older versions or just my bad code lol.

# Anyway, I feel like this script should be only up to half the size.
# Hell, I think this should be simpler but no... I pushed for a more complex setup or something.
# What am I doing?
# Is this what ricing is all about?
# Why are you reading this?

import argparse
import json
import logging
import os
import os.path
from pathlib import Path
import subprocess
import sys

DEFAULT_PACKAGE_DATA_FILE="locations.json"


class PackageDir:
    """ A package directory should have a file named `locations.json` where it contains a top-level object of the stow packages with their usual target path. """

    def __init__(self, package_path = os.getcwd(), package_data_path = DEFAULT_PACKAGE_DATA_FILE):
        """
        Creates an instance of PackageDir

        :param: package_path - The directory where it should contain a file named `locations.json`.
        """
        self.path = Path(package_path)
        self.data_path = Path(package_data_path)

        # Loads the packages
        self.packages = {}
        try:
            self.load_packages()
        except:
            pass


    def add_package(self, package, target):
        """
        Add the package to the list.

        :param: package - the name of the package
        :param: target - the target path of the package
        """
        package_path = self.path / package
        assert package_path.is_dir(), f"The given package '{package}' does not exist in the package directory."
        self.packages[package] = target


    def remove_package(self, package):
        """
        Remove the package in the list.
        Although this function is quite simple, this is only meant as an official API.

        :param: package - the package to be removed
        """
        return self.packages.pop(package, None)


    def load_packages(self):
        """
        Loads the packages from the data file.
        """
        assert self.json_location.is_file(), "There is no 'package.json` in the given directory."

        with open(self.json_location) as f:
            package_map = json.load(f)
            for package, target in package_map.items():
                try:
                    self.add_package(package, target)
                except Exception as e:
                    logging.error(e)


    def execute_packages(self, commands):
        """
        Execute a set of commands with the packages.

        :param: commands - A list of strings that'll be used as a template.
                           The template string uses the `string.format` syntax.
                           (https://docs.python.org/3/library/string.html?highlight=template#format-string-syntax)
                           It should contain a binding to the keywords `package` and `location` (e.g., `stow --restow {package} --target {location}`).
        """
        for package, location in self.packages.items():
            # Making sure the location is expanded.
            location = os.path.expanduser(location)
            target_cwd = os.path.realpath(self.path)

            for command in commands:
                command = command.format(package=package, location=location)

                process_status = subprocess.run(command, cwd=target_cwd, capture_output=True, shell=True, encoding='utf-8')
                if process_status.returncode == 0:
                    logging.info(f"{command}: successfully ran")
                else:
                    logging.error(f"{command}: returned with following error\n{process_status.stderr.strip()}")


    @property
    def json_location(self):
        """ Simply appends the path with the required JSON file. """
        return self.path / self.data_path


def setup_logging():
    """
    Setup the logger instance.
    """
    logging.basicConfig(format="[%(levelname)s] %(message)s", level=logging.INFO, stream=sys.stdout)


def setup_args():
    """
    Setup the argument parser.

    :returns: An ArgumentParser object.
    """
    description = """A quick installation script for this setup. Take note this is tailored to my specific needs but I tried to make this script generic."""
    argparser = argparse.ArgumentParser(description=description)

    argparser.add_argument("-c", "--commands", metavar = "command", help = "Executing the specified commands. All of the commands are treated as they were entered in the shell.", nargs = "*", default = ["echo {package} is set at {location}"])
    argparser.add_argument("-d", "--directory", metavar = "path", help = "Set the directory of the package data file.", type = Path, nargs = "?", default = Path(os.getcwd()))
    argparser.add_argument("-m", "--manifest", metavar = "manifest", help = "Specify what metadata file to be used (e.g., locations.json).", type = Path, nargs = "?", default = DEFAULT_PACKAGE_DATA_FILE)
    argparser.add_argument("--exclude", metavar = "package", help = "Exclude the given packages.", type = str, nargs = "+", default = [])
    argparser.add_argument("--include", metavar = ("package", "location"), help = "Include with the following packages.", type = str, nargs = 2, action = "append", default = [])
    argparser.add_argument("--only", metavar = "package", help = "Only execute with the given packages.", type = str, nargs = "+", default = [])

    return argparser


def parse_args(parser, argv):
    """
    Parse the arguments.

    This is also the main function to pay attention to.

    :param: parser - An instance of the argument parser.
    :param: argv - A list of arguments to be parsed.
    """
    args = parser.parse_args(argv)

    try:
        package_dir = PackageDir(args.directory, args.manifest)

        # Include the following packages.
        for package, target in args.include:
            try:
                package_dir.add_package(package, target)
            except Exception as e:
                logging.error(e)

        # Exclude the following packages.
        # We don't need the value here so we'll let it pass.
        for package in args.exclude:
            package_dir.remove_package(package)

        if len(args.only) >= 1:
            items = {}
            for package in args.only:
                value = package_dir.remove_package(package)
                if value is None:
                    continue
                items[package] = value

            package_dir.packages.clear()
            package_dir.packages = items

        # Execute the commands with the packages.
        package_dir.execute_packages(args.commands)
    except Exception as e:
        logging.error(e)


if __name__ == "__main__":
    setup_logging()
    argparser = setup_args()

    parse_args(argparser, sys.argv[1:])