# coding: utf-8
"""
some project general utils functions
"""

from __future__ import annotations

import argparse
import functools
import importlib
import logging
import sys

from silx.utils.launcher import Launcher as _Launcher
from silx.utils.launcher import LauncherCommand as _LauncherCommand

_logger = logging.getLogger(__file__)


def _docstring(dest, origin):
    """Implementation of docstring decorator.

    It patches dest.__doc__.
    """
    if not isinstance(dest, type) and isinstance(origin, type):
        # func is not a class, but origin is, get the method with the same name
        try:
            origin = getattr(origin, dest.__name__)
        except AttributeError:
            raise ValueError("origin class has no %s method" % dest.__name__)

    dest.__doc__ = origin.__doc__
    return dest


def docstring(origin):
    """Decorator to initialize the docstring from another source.

    This is useful to duplicate a docstring for inheritance and composition.

    If origin is a method or a function, it copies its docstring.
    If origin is a class, the docstring is copied from the method
    of that class which has the same name as the method/function
    being decorated.

    :param origin:
        The method, function or class from which to get the docstring
    :raises ValueError:
        If the origin class has not method n case the
    """
    return functools.partial(_docstring, origin=origin)


class LauncherCommand(_LauncherCommand):
    """Description of a command"""

    def get_module(self):
        """Returns the python module to execute. If any."""
        try:
            module = importlib.import_module(self.module_name)
            return module
        except ImportError:
            msg = "Error while reaching module '%s'"
            _logger.error(msg, self.module_name, exc_info=True)
            raise


class Launcher(_Launcher):
    """
    Manage launch of module.

    Provides an API to describe available commands and feature to display help
    and execute the commands.
    """

    def __init__(
        self, prog=None, usage=None, description=None, epilog=None, version=None
    ):
        """
        :param prog: Name of the program. If it is not defined it uses the
            first argument of `sys.argv`
        :param usage: Custom the string explaining the usage. Else it is
            autogenerated.
        :param description: Description of the application displayed after the
            usage.
        :param epilog: Custom the string displayed at the end of the help.
        :param version: Define the version of the application.
        """
        if prog is None:
            prog = sys.argv[0]
        self.prog = prog
        self.usage = usage
        self.description = description
        self.epilog = epilog
        self.version = version
        self._commands = {}

        help_command = LauncherCommand(
            "help",
            description="Show help of the following command",
            function=self.execute_help,
        )
        self.add_command(command=help_command)

    def add_command(self, name=None, module_name=None, description=None, command=None):
        """
        Add a command to the launcher.

        See also `LauncherCommand`.

        :param name: Name of the command
        :param module_name: Module to execute
        :param description: Description of the command
        :param LauncherCommand command: A `LauncherCommand`
        """
        if command is not None:
            assert name is None and module_name is None and description is None
        else:
            command = LauncherCommand(
                name=name, description=description, module_name=module_name
            )
        self._commands[command.name] = command

    def print_help(self):
        """Print the help to stdout."""
        usage = self.usage
        if usage is None:
            usage = "usage: {0.prog} [--version|--help] <command> [<args>]"
        description = self.description
        epilog = self.epilog
        if epilog is None:
            epilog = "See '{0.prog} help <command>' to read about a specific subcommand"

        print(usage.format(self))
        print("")
        if description is not None:
            print(description)
            print("")
        print("The {0.prog} commands are:".format(self))
        commands = sorted(self._commands.keys())
        for command in commands:
            command = self._commands[command]
            print("   {:18s} {:s}".format(command.name, command.description))
        print("")
        print(epilog.format(self))

    def execute_help(self, argv):
        """Execute the help command.

        :param argv: The list of arguments (the first one is the
            name of the application with the help command)
        :returns: The execution status
        """
        description = "Display help information about %s" % self.prog
        parser = argparse.ArgumentParser(description=description)
        parser.add_argument(
            "command",
            default=None,
            nargs=argparse.OPTIONAL,
            help="Command in which aving help",
        )

        try:
            options = parser.parse_args(argv[1:])
        except SystemExit as e:
            # ArgumentParser likes to finish with a sys.exit
            return e.args[0]

        command_name = options.command
        if command_name is None:
            self.print_help()
            return 0

        if command_name not in self._commands:
            print(f"Unknown command: {command_name}")
            self.print_help()
            return -1

        command = self._commands[command_name]
        prog = f"{self.prog} {command.name}"
        return command.execute([prog, "--help"])

    def execute(self, argv=None):
        """Execute the launcher.

        :param argv: The list of arguments (the first one is the
            name of the application)
        :returns: The execution status
        """
        if argv is None:
            argv = sys.argv

        if len(argv) <= 1:
            self.print_help()
            return 0

        command_name = argv[1]

        if command_name == "--version":
            print(f"{self.prog} version {str(self.version)}")
            return 0

        if command_name in ["--help", "-h"]:
            # Special help command
            if len(argv) == 2:
                self.print_help()
                return 0
            else:
                command_name = argv[2]
                command_argv = argv[2:] + ["--help"]
                command_argv[0] = f"{self.prog} {command_argv[0]}"
        else:
            command_argv = argv[1:]
            command_argv[0] = f"{self.prog} {command_argv[0]}"

        if command_name not in self._commands:
            print("Unknown command: %s" % command_name)
            self.print_help()
            return -1

        command = self._commands[command_name]
        return command.execute(command_argv)
