# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2015-2018 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import os
import logging
import subprocess
import sys
from typing import List, Sequence

import snapcraft
from snapcraft.internal import errors, repo

logger = logging.getLogger(__name__)


class WstoolError(errors.SnapcraftError):
    pass


class WorkspaceInitializationError(WstoolError):
    fmt = "Error initializing workspace: {message}"

    def __init__(self, message: str) -> None:
        super().__init__(message=message)


class RosinstallMergeError(WstoolError):
    fmt = "Error merging rosinstall file {path!r} into workspace: {message}"

    def __init__(self, path: str, message: str) -> None:
        super().__init__(path=path, message=message)


class WorkspaceUpdateError(WstoolError):
    fmt = "Error updating workspace: {message}"

    def __init__(self, message: str) -> None:
        super().__init__(message=message)


class Wstool:
    """This class serves as a Python wrapper for the CLI utility wstool."""

    def __init__(
        self,
        ros_package_path: str,
        wstool_path: str,
        ubuntu_sources: str,
        ubuntu_keyrings: Sequence[str],
        project: snapcraft.ProjectOptions,
    ) -> None:
        """Create new Wstool

        :param str ros_package_path: The path where the packages should be
                                     fetched.
        :param str wstool_path: Working directory for wstool (where it will be
                                installed).
        :param str ubuntu_sources: Ubuntu repositories from which wstool will
                                   be installed.
        :param project: Instance of ProjectOptions for project-wide settings.
        :type project: snapcraft.ProjectOptions
        """
        self._ros_package_path = ros_package_path
        self._ubuntu_sources = ubuntu_sources
        self._ubuntu_keyrings = ubuntu_keyrings
        self._wstool_path = wstool_path
        self._wstool_install_path = os.path.join(wstool_path, "install")
        self._project = project

    def setup(self) -> None:
        """Download/install wstool, and initialize workspace."""

        os.makedirs(self._wstool_install_path, exist_ok=True)

        # wstool isn't a dependency of the project, so we'll unpack it
        # somewhere else, and use it from there.
        logger.info("Preparing to fetch wstool...")
        ubuntu = repo.Ubuntu(
            self._wstool_path,
            sources=self._ubuntu_sources,
            keyrings=self._ubuntu_keyrings,
            project_options=self._project,
        )
        logger.info("Fetching wstool...")
        ubuntu.get(["python-wstool"])

        logger.info("Installing wstool...")
        ubuntu.unpack(self._wstool_install_path)

        logger.info("Initializing workspace (if necessary)...")
        try:
            self._run(
                [
                    "init",
                    self._ros_package_path,
                    "-j{}".format(self._project.parallel_build_count),
                ]
            )
        except subprocess.CalledProcessError as e:
            output = e.output.decode(sys.getfilesystemencoding()).strip()
            if "already is a workspace" not in output:
                stderr = e.stderr.decode(sys.getfilesystemencoding()).strip()
                raise WorkspaceInitializationError(stderr)

    def merge(self, rosinstall_file: str) -> str:
        """Merge rosinstall file with existing rosinstall in workspace.

        :param str rosinstall_file: Path to rosinstall file to merge in.
        """

        try:
            # Summary of arguments used:
            # --confirm-all: Don't ask for confirmation
            # -t: Path to workspace
            return self._run(
                [
                    "merge",
                    rosinstall_file,
                    "--confirm-all",
                    "-t{}".format(self._ros_package_path),
                ]
            ).strip()
        except subprocess.CalledProcessError as e:
            stderr = e.stderr.decode(sys.getfilesystemencoding()).strip()
            raise RosinstallMergeError(rosinstall_file, stderr)

    def update(self) -> str:
        """Update workspace from rosinstall file.

        This actually hits the network and downloads all repositories in
        the workspace's rosinstall file.
        """
        try:
            # Summary of arguments used:
            # -j: Use multiple jobs to fetch
            # -t: Path to workspace (into which packages will be fetched)
            return self._run(
                [
                    "update",
                    "-j{}".format(self._project.parallel_build_count),
                    "-t{}".format(self._ros_package_path),
                ]
            )
        except subprocess.CalledProcessError as e:
            stderr = e.stderr.decode(sys.getfilesystemencoding()).strip()
            raise WorkspaceUpdateError(stderr)

    def _run(self, arguments: List[str]) -> str:
        env = os.environ.copy()

        env["PATH"] += ":" + os.path.join(self._wstool_install_path, "usr", "bin")
        env["PYTHONPATH"] = os.path.join(
            self._wstool_install_path, "usr", "lib", "python2.7", "dist-packages"
        )

        return (
            subprocess.check_output(
                ["wstool"] + arguments, stderr=subprocess.PIPE, env=env
            )
            .decode(sys.getfilesystemencoding())
            .strip()
        )
