#!/usr/bin/python3
"""
This script performs a few checkouts on branches on robotpkg, and then tries to compile and test everything
"""

import argparse
import logging
import os
import socket
import subprocess
from pathlib import Path
from shutil import rmtree

# Shell colors
BOLD = "\033[1m"
RED = "\033[1;31m"
GREEN = "\033[1;32m"
PURPLE = "\033[1;35m"
NC = "\033[0m"

# Robotpkg configuration
ACCEPTABLE_LICENSES = [
    "openhrp-grx-license",
    "cnrs-hpp-closed-source",
    "gnu-gpl",
    "motion-analysis-license",
    "pal-license",
]
PREFER_SYSTEM = [
    "gnupg",
    "urdfdom",
    "urdfdom-headers",
    "ros-catkin",
    "ros-comm",
    "ros-genlisp",
    "ros-message-generation",
    "ros-std-msgs",
    "ros-rospack",
    "ros-message-runtime",
    "ros-roscpp-core",
    "ros-xacro",
    "ros-common-msgs",
    "ros-lint",
    "ros-com-msgs",
    "ros-com-msgs",
    "bullet",
    "ros-ros",
    "ros-cmake-modules",
    "ros-dynamic-reconfigure",
    "ros-realtime-tools",
    "ros-control-toolbox",
    "ros-bond-core",
    "ros-class-loader",
    "ros-pluginlib",
    "ros-rqt",
    "ros-humanoid-msgs",
    "ros-genmsg",
    "ros-actionlib",
    "ros-geometry",
    "collada-dom",
    "orocos-kdl",
    "ros-angles ",
    "ros-console-bridge",
    "ros-eigen-stl-containers",
    "ros-random-numbers",
    "ros-resource-retriever",
    "ros-shape-tools",
    "ros-geometric-shapes",
    "ros-srdfdom",
    "ros-robot-model",
    "ros-orocos-kdl",
    "assimp",
]

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
    "robotpkg_root",
    nargs="?",
    type=Path,
    default=Path.home() / "devel-src/robotpkg-test-rc",
)
parser.add_argument("-v", "--verbose", action="count", default=0)
parser.add_argument("-d", "--delete", action="store_true")
parser.add_argument("-c", "--clean", action="store_true")
parser.add_argument(
    "--robotpkg_git", default="https://git.openrobots.org/robots/robotpkg.git"
)
parser.add_argument(
    "--robotpkg_wip_git",
    default="ssh://git@git.openrobots.org/robots/robotpkg/robotpkg-wip",
)
parser.add_argument("--conf", type=Path)


def prepend_paths(base, dirs, old=None):
    """
    format an environment variable with <dirs> in <base>, and eventually keep <old> value at the end
    """
    paths = [str(Path(base) / path) for path in dirs]
    if old is not None:
        paths += old.split(":")
    return ":".join(paths)


class RobotpkgTestRC:
    def __init__(
        self,
        robotpkg_root,
        verbose,
        delete,
        clean,
        robotpkg_git,
        robotpkg_wip_git,
        conf,
    ):
        """Init environment variables"""
        self.robotpkg_root = robotpkg_root
        self.robotpkg_base = self.robotpkg_root / "install"
        self.delete = delete
        self.clean = clean
        self.robotpkg_git = robotpkg_git
        self.robotpkg_wip_git = robotpkg_wip_git
        self.conf = conf

        logging.basicConfig(format="%(message)s", level=40 - verbose * 10)
        logging.critical("enabled logging of CRITICALs")
        logging.error(RED + "enabled logging of ERRORs" + NC)
        logging.warning(PURPLE + "enabled logging of WARNINGs" + NC)
        logging.info(GREEN + "enabled logging of INFOs" + NC)
        logging.debug(BOLD + "enabled logging of DEBUGs\n" + NC)

        self.init_environment_variables()
        self.init_robotpkg_conf_add()

    def init_environment_variables(self):
        """Prepare the environment variables.

        Specifies the environment when starting commands
        """
        self.env = os.environ.copy()
        self.env["ROBOTPKG_BASE"] = str(self.robotpkg_base)
        # For binaries
        self.env["PATH"] = prepend_paths(
            self.robotpkg_base, ["sbin", "bin"], self.env["PATH"]
        )

        # For libraries
        self.env["LD_LIBRARY_PATH"] = prepend_paths(
            self.robotpkg_base,
            ["lib", "lib/plugin", "lib64"],
            self.env.get("LD_LIBRARY_PATH"),
        )

        # For python
        self.env["PYTHON_PATH"] = prepend_paths(
            self.robotpkg_base,
            ["lib/python2.7/site-packages", "lib/python2.7/dist-packages"],
            self.env.get("PYTHON_PATH", ""),
        )

        # For pkgconfig
        self.env["PKG_CONFIG_PATH"] = prepend_paths(
            self.robotpkg_base, ["lib/pkgconfig"], self.env.get("PKG_CONFIG_PATH", "")
        )

        # For ros packages
        self.env["ROS_PACKAGE_PATH"] = prepend_paths(
            self.robotpkg_base,
            ["share", "stacks"],
            self.env.get("ROS_PACKAGE_PATH", ""),
        )

        # For cmake
        self.env["CMAKE_PREFIX_PATH"] = (
            str(self.robotpkg_base) + ":" + self.env.get("CMAKE_PREFIX_PATH", "")
        )

    def init_robotpkg_conf_add(self):
        self.robotpkg_conf_lines = [
            "ROS_PACKAGE_PATH=${ROBOTPKG_BASE}/share:$ROS_PACKAGE_PATH"
        ]
        self.robotpkg_conf_lines += [
            "ACCEPTABLE_LICENSES += %s" % license for license in ACCEPTABLE_LICENSES
        ]
        self.robotpkg_conf_lines += [
            "PREFER.%s = system" % pkg for pkg in PREFER_SYSTEM
        ]

    def execute(self, command, cwd=None):
        """Execute command

        Keyword arguments:
        command -- the command to be run
        cwd -- the Current Working Directory in which execute the command

        It returns a list of binary string that can be iterate
        and decode.

        """
        logging.debug(BOLD + "execute command: %s" + NC, command)
        outputdata = subprocess.check_output(
            command.split(), env=self.env, cwd=str(cwd), universal_newlines=True
        )
        for stdout_line in outputdata.splitlines():
            logging.debug(BOLD + stdout_line + NC)
        return outputdata

    def prepare_robotpkg(self):
        """
        Prepare the robotpkg environment

        Make robotpkg directoriers, clone it with wip, bootstrap and add
        information in the file ${ROBOTPKG_BASE}/etc/robotpkg.conf
        """
        self.make_robotpkg_dirs()
        self.cloning_robotpkg_main()
        self.cloning_robotpkg_wip()
        self.bootstrap_robotpkg()
        self.complete_robotpkg_conffile()

    def make_robotpkg_dirs(self):
        """
        Create directories for robotpkg
        and eventually delete or clean them, depending on the argparser's options
        """
        if self.delete and self.robotpkg_root.is_dir():
            logging.warning(PURPLE + "rm -rf %s" % self.robotpkg_root + NC + "\n")
            rmtree(str(self.robotpkg_root))
        if self.clean:
            if self.robotpkg_base.is_dir():
                logging.warning(PURPLE + "rm -rf %s" % self.robotpkg_base + NC + "\n")
                rmtree(str(self.robotpkg_base))
        logging.info(GREEN + "Creating the repositories" + NC)
        self.robotpkg_base.mkdir(parents=True, exist_ok=True)

    def cloning_robotpkg_main(self):
        """Clones the main robotpkg repository"""
        logging.info(GREEN + "Cloning robotpkg\n" + NC)
        if (self.robotpkg_root / "robotpkg").exists():
            self.execute("git pull", cwd=self.robotpkg_root / "robotpkg")
        else:
            self.execute("git clone %s" % self.robotpkg_git, cwd=self.robotpkg_root)

    def cloning_robotpkg_wip(self):
        """Clones the wip robotpkg repository"""
        logging.info(GREEN + "Cloning robotpkg/wip\n" + NC)
        if (self.robotpkg_root / "robotpkg/wip").exists():
            self.execute("git pull", cwd=self.robotpkg_root / "robotpkg/wip")
        else:
            self.execute(
                "git clone %s wip" % self.robotpkg_wip_git,
                cwd=self.robotpkg_root / "robotpkg",
            )

    def bootstrap_robotpkg(self):
        """bootstrap robotpkg

        This method calls:
        bootstrap --prefix=${robotpkg_base}
        only if there is no
        ${robotpkg_base}/etc/robotpkg.conf
        already present.
        """
        # Test if a file in robotpkg_base/etc/robotpkg.conf already exists
        rpkg_conf_file = self.robotpkg_base / "etc/robotpkg.conf"
        if rpkg_conf_file.is_file():
            logging.warning(PURPLE + str(rpkg_conf_file) + NC + " already exists\n")
            return
        logging.info(GREEN + "Boostrap robotpkg\n" + NC)
        self.execute(
            "./bootstrap --prefix=%s" % self.robotpkg_base,
            cwd=self.robotpkg_root / "robotpkg/bootstrap",
        )

    def complete_robotpkg_conffile(self):
        """Add the contents of robotpkg_conf_lines in robotpkg.conf file

        Avoid to add two times the same information.
        """
        logging.info(
            GREEN
            + "Adding information to "
            + str(self.robotpkg_base)
            + "/etc/robotpkg.conf\n"
            + NC
        )

        # Open the file, read it and stores it in file_robotpkg_contents
        with (self.robotpkg_base / "etc/robotpkg.conf").open() as file_robotpkgconf:
            file_robotpkgconf_contents = file_robotpkgconf.read()

        # Append the optional conf file given as parameter
        if self.conf is not None and self.conf.exists():
            logging.info(
                GREEN + "Adding %s to %s/etc/robotpkg.conf\n" + NC,
                self.conf,
                self.robotpkg_base,
            )
            with self.conf.open() as f:
                self.robotpkg_conf_lines += [line.strip() for line in f.readlines()]

        # Add new lines at the end of robotpkg.conf file.
        with (self.robotpkg_base / "etc/robotpkg.conf").open("a") as file_robotpkgconf:
            for stdout_line in self.robotpkg_conf_lines:
                if file_robotpkgconf_contents.find(stdout_line) == -1:
                    file_robotpkgconf.write(stdout_line + "\n")

    def build_rpkg_checkout_package(self, packagename, category):
        """Execute cmd in the working directory of packagename"""
        # Going into the repository directory
        pathname = (
            self.robotpkg_root
            / "robotpkg"
            / category
            / packagename
            / ("work." + socket.gethostname())
        )
        return pathname

    def apply_rpkg_checkout_package(self, packagename, branchname, category):
        """Performs a make checkout in packagename directory

        packagename: The name of package in which the git clone will be perfomed.
        branchname: The name of the branch used in the repository.
        category: the category of the package

        The location of the repository is specified in the robotpkg Makefile.
        """
        logging.info(
            GREEN + "Checkout " + packagename + " in robotpkg/" + category + NC + "\n"
        )
        # Checking if we need to clean or not the package

        # First check if the working directory exists
        directory_to_clean = True
        checkoutdir_packagename = self.build_rpkg_checkout_package(
            packagename, category
        )

        if checkoutdir_packagename.exists():
            logging.debug(BOLD + "Going into :\n" + str(checkoutdir_packagename) + NC)

            # If it does then maybe this is not a git directory
            for folder in checkoutdir_packagename.iterdir():
                if not folder.is_dir():
                    continue
                logging.debug(BOLD + "Going into: %s" % folder + NC)
                # Check if there is a git folder
                if (folder / ".git").is_dir():
                    logging.debug(BOLD + "Git folder found" + NC)
                    # Now that we detected a git folder
                    # Check the branch
                    outputdata = self.execute(
                        "git symbolic-ref --short -q HEAD", cwd=folder
                    )
                    for stdout_line in outputdata.splitlines():
                        if stdout_line != branchname:
                            logging.error(
                                RED
                                + " Wrong branch name: "
                                + stdout_line
                                + " instead of "
                                + branchname
                                + NC
                            )
                        else:
                            finaldirectory = folder
                            directory_to_clean = False
                    break

        logging.debug(BOLD + "Directory to clean: " + str(directory_to_clean) + NC)
        if directory_to_clean:
            # Going into the directory of the package
            cwd = self.robotpkg_root / "robotpkg" / category / packagename
            self.execute("make clean confirm", cwd=cwd)
            self.execute("make checkout", cwd=cwd)
        else:
            # Remove all the files which may have been modified.
            self.execute("git reset --hard", cwd=finaldirectory)
            # Pull all the modification push upstream.
            self.execute(
                "git pull origin " + branchname + ":" + branchname, cwd=finaldirectory
            )
            self.execute("git submodule update", cwd=finaldirectory)

    def apply_git_checkout_branch(self, packagename, branchname, category):
        """
        Changes the branch of a git repository in robotpkg.

        The method first detects that the package working directory is
        really a git repository. Then it performs the branch switch.
        """
        checkoutdir_packagename = self.build_rpkg_checkout_package(
            packagename, category
        )
        for folder in checkoutdir_packagename.iterdir():
            if not folder.is_dir():
                continue
            logging.debug(BOLD + "Going into: %s" % folder + NC)
            if (folder / ".git").is_dir():
                self.execute("git checkout " + branchname, cwd=folder)
                self.execute("git submodule update", cwd=folder)

    def compile_package(self, packagename, category):
        """Performs make replace confirm in package working directory"""
        # Going into the directory of the package
        logging.info(
            GREEN + "Compile " + packagename + " in robotpkg/" + category + NC + "\n"
        )
        # Compiling the repository
        self.execute(
            "make replace confirm",
            cwd=self.robotpkg_root / "robotpkg" / category / packagename,
        )

    def handle_package(self, packagename, branchname, category):
        """Compile and install packagename with branch branchname

        First performs the proper make checkout and git operation to get the branch
        Then compile the package with make replace.
        Do not use make update confirm, this install the release version (the tar file).
        """
        self.apply_rpkg_checkout_package(packagename, branchname, category)
        self.apply_git_checkout_branch(packagename, branchname, category)
        self.compile_package(packagename, category)

    def perform_test(self, arch_release_candidates):
        """Install packages specifued by release_candidates

        arch_release_candidates: tuple of triples [ ('category', 'package_name','branch_name'), ... ]
        """
        self.prepare_robotpkg()
        for category, package_name, branch_name in arch_release_candidates:
            self.handle_package(package_name, branch_name, category)


arch_release_candidates = [
    ("math", "pinocchio", "devel"),
    ("math", "py-pinocchio", "devel"),
    ("wip", "sot-core-v3", "topic/pinocchio_v2"),
    ("wip", "py-sot-core-v3", "topic/pinocchio_v2"),
    ("wip", "sot-dynamic-pinocchio-v3", "topic/pinocchio_v2"),
    ("wip", "py-sot-dynamic-pinocchio-v3", "topic/pinocchio_v2"),
    ("wip", "sot-talos", "master"),
    ("wip", "sot-hrp2-v3", "master"),
    ("wip", "py-sot-application-v3", "master"),
    ("wip", "py-dynamic-graph-bridge-v3", "master"),
]

if __name__ == "__main__":
    RobotpkgTestRC(**vars(parser.parse_args())).perform_test(arch_release_candidates)