#!/usr/bin/env python3
# -*- coding:utf-8 -*-
"""
@file       gspylib/common/CommandLineParser.py
@version    0.0.2
@date       2019-04-17
@license    2012-2017, Huawei Tech. Co., Ltd.
@brief      The command line parser module.
@details    Parse command line parameters and store them as variables with the same name.
"""

# export module
__all__ = ["CommandLineParser", "Option"]

# system import.
import sys as _sys
from optparse import Option as _option, OptionParser as _optparse, NO_DEFAULT

# local import
from gspylib.common.ErrorCode import ErrorCode


class Option(_option, object):
    """
    The command line option class which use to the "OptionParser" instance. But this class does not accept the "dest"
    parameter.
    """

    def __init__(self, *args, **kwargs):
        """
        Initialize the command line option instance.

        :param args:            The command line command string, maximum length is 2.
        :param kwargs:          The command line command parameters.
        :param action:          The named parameter, specified command line parameter action allowed by option parser.
        :param type:            The named parameter, specified command line parameter type of current option.
        :param default:         The named parameter, specified command line parameter default value.
        :param nargs:           The named parameter, specified the number of the command line parameter value.
        :param const:           The named parameter, specified the const value of the command line parameter.
        :param choices:         The named parameter, specified the choice range of the command line parameter, the item
                                 in the choices list must be string type, and must not set the "type" parameter.
                                 Otherwise, it will lead to unexpected errors.
        :param callback:        The named parameter, specified the handler function for the command line parameter.
        :param callback_args:   The named parameter, specified the unnamed parameters of the handler function for the
                                 command line parameter.
        :param callback_kwargs: The named parameter, specified the named parameters of the handler function for the
                                 command line parameter.
        :param help:            The named parameter, the help string for the command line parameter.
        :param metavar:         The named parameter, the display string for the command line parameter value.

        :type args:             str
        :type kwargs:           *
        :type action:           str
        :type type:             str
        :type default:          *
        :type nargs:            int
        :type const:            *
        :type choices:          List[str]
        :type callback:         function
        :type callback_args:    tuple
        :type callback_kwargs:  dict
        :type help:             str
        :type metavar:          str
        """
        # Remove the "dest" parameter.
        if "dest" in kwargs:
            kwargs.pop("dest")
        # Initialize the command line option instance.
        _option.__init__(self, *args, **kwargs)


class OptionParser(_optparse, object):
    """
    The command line option parser.
    """

    def __init__(self, *args, **kwargs):
        """
        Initialize the internal command line parser.

        :param args:                The additional unnamed parameter of the command line parser.
        :param kwargs:              The additional named parameter of the command line parser.
        :param usage:               A usage string for your program.  Before it is displayed to the user, "%prog" will
                                     be expanded to the name of your program (prog or os.path.basename(sys.argv[0])).
        :param option_list:         A list of option instance for this parser.
        :param option_class:        The command line option class type, default is "optparse.Option".
        :param version:             The version string for this scripts.
        :param conflict_handler:    The solutions after command line options conflict, "resolve" will override before
                                     option, "errors" will raise error, default is "error".
        :param description:         A paragraph of text giving a brief overview of your program. optparse re-formats
                                     this paragraph to fit the current terminal width and prints it when the user
                                     requests help (after usage, but before the list of options).
        :param formatter:           The formatter instance for the description information.
        :param add_help_option:     Whether add the help option instance automatically.
        :param prog:                The name of the current program (to override os.path.basename(sys.argv[0])).
        :param epilog:              A paragraph of help text to print after option help.

        :type args:                 str | list | bool | type | _optparse.IndentedHelpFormatter
        :type kwargs:               str | list | bool | type | _optparse.IndentedHelpFormatter
        :type usage:                str
        :type option_list:          List[Option]
        :type option_class:         type
        :type version:              str
        :type conflict_handler:     str
        :type description:          str
        :type formatter:            _optparse.IndentedHelpFormatter
        :type add_help_option:      bool
        :type prog:                 str
        :type epilog:               str
        """
        # Call the parent init function.
        _optparse.__init__(self, *args, **kwargs)

    def print_help(self, _file=_sys.stderr):
        """
        print_help(file : file = stderr)

        Print an extended help message, listing all options and any help text provided with them, to 'file' (default
         stderr).

        :param _file:   The file descriptor instance.
        :type _file:    file

        :rtype: None
        """
        _optparse.print_help(self, _file)

    def print_usage(self, _file=_sys.stderr):
        """
        print_usage(file : file = stderr)

        Print the usage message for the current program (self.usage) to 'file' (default stderr).  Any occurrence of the
         string "%prog" in self.usage is replaced with the name of the current program (basename of sys.argv[0]).
         Does nothing if self.usage is empty or not defined.

        :param _file:   The file descriptor instance.
        :type _file:    file

        :rtype: None
        """
        _optparse.print_usage(self, _file)

    def print_version(self, _file=_sys.stderr):
        """
        print_version(file : file = stderr)

        Print the version message for this program (self.version) to 'file' (default stderr).  As with print_usage(),
         any occurrence of "%prog" in self.version is replaced by the current program's name.  Does nothing if
         self.version is empty or undefined.

        :param _file:   The file descriptor instance.
        :type _file:    file

        :rtype: None
        """
        _optparse.print_version(self, _file)

    def error(self, _msg):
        """
        error(msg : string)

        Print a usage message incorporating 'msg' to stderr and exit. If you override this in a subclass, it should not
         return -- it should either exit or raise an exception.

        :param _msg:    The error message.
        :type _msg:     str

        :rtype: None
        """
        raise Exception(ErrorCode.GAUSS_500["GAUSS_50015"] % _msg)


class CommandLineMetaClass(type):
    """
    The command line parser metaclass.

    Used to magically save command line parsing options instances.
    """

    def __new__(mcs, name, bases, attrs):
        """
        Create an new command line parser class.

        :param name:    The name of the current class.
        :param bases:   The parent instances of the current class.
        :param attrs:   The attribute dict of the current class.

        :type name:     str
        :type bases:    Tuple[type]
        :type attrs:    Dict[str, Any]
        :return:
        """
        # If it is the base command line parser class, we will do nothing.
        if name == "CommandLineOption":
            return type.__new__(mcs, name, bases, attrs)

        # Store the command line option instance mapping.
        mappings = {}
        # Store the attribute key-value pair list of the current class.
        items = list(attrs.items())

        # Store the command line option instance to the mapping, and remove it from current class attribute.
        for key, value in items:
            if isinstance(value, Option):
                mappings.setdefault(key, value)
                attrs.pop(key)

                # Add the additional function.
                if value.action in ["append", "append_const", "count"]:
                    def ensure_value(_self, _attr, _value):
                        """
                        Ensure the non-existence of object attributes and set the value of attributes.

                        :param _self:   The object instance.
                        :param _attr:   The object attribute name.
                        :param _value:  The object attribute value.

                        :type _self:    Option
                        :type _attr:    str
                        :type _value:   *

                        :return:    Return the object attribute value.
                        :rtype:     *
                        """
                        if not hasattr(_self, _attr) or getattr(_self, _attr) is None:
                            setattr(_self, _attr, _value)
                        return getattr(_self, _attr)

                    # Add function.
                    attrs["ensure_value"] = ensure_value

        # Store the mapping into a named parameter of current class.
        attrs["__mappings__"] = mappings

        return type.__new__(mcs, name, bases, attrs)


class CommandLineParser(object, metaclass=CommandLineMetaClass):
    """
    The base class of the command line parser.
    """

    # Set the metaclass type, this approach is not supported python 3.x.

    def __init__(self, _parameters=None, *args, **kwargs):
        """
        Initialize the command line parser.

        :param _parameters:         The command line parameters list, default is sys.argv.
        :param args:                The additional unnamed parameter of the command line parser.
        :param kwargs:              The additional named parameter of the command line parser.
        :param usage:               A usage string for your program.  Before it is displayed to the user, "%prog" will
                                     be expanded to the name of your program (prog or os.path.basename(sys.argv[0])).
        :param option_list:         A list of option instance for this parser.
        :param option_class:        The command line option class type, default is "optparse.Option".
        :param version:             The version string for this scripts.
        :param conflict_handler:    The solutions after command line options conflict, "resolve" will override before
                                     option, "errors" will raise error, default is "error".
        :param description:         A paragraph of text giving a brief overview of your program. optparse re-formats
                                     this paragraph to fit the current terminal width and prints it when the user
                                     requests help (after usage, but before the list of options).
        :param formatter:           The formatter instance for the description information.
        :param add_help_option:     Whether add the help option instance automatically.
        :param prog:                The name of the current program (to override os.path.basename(sys.argv[0])).
        :param epilog:              A paragraph of help text to print after option help.

        :type _parameters:          List[str] | None
        :type args:                 str | list | bool | type | _optparse.IndentedHelpFormatter
        :type kwargs:               str | list | bool | type | _optparse.IndentedHelpFormatter
        :type usage:                str
        :type option_list:          List[Option]
        :type option_class:         type
        :type version:              str
        :type conflict_handler:     str
        :type description:          str
        :type formatter:            _optparse.IndentedHelpFormatter
        :type add_help_option:      bool
        :type prog:                 str
        :type epilog:               str
        """
        # Create a new command line parser.
        opt = OptionParser(*args, **kwargs)

        # Add the "dest" attribute to the command line option instance, and add the option instance to the parser.
        # noinspection PyUnresolvedReferences
        for key, value in self.__mappings__.items():
            setattr(value, "dest", key)
            opt.add_option(value)

        # Parse the command line parameter.
        if not _parameters:
            _parameters = _sys.argv[1:]
        _, unknown_args = opt.parse_args(_parameters, self)

        # If some command line parameter does not supplied by user, we will set it to "None".
        # noinspection PyUnresolvedReferences
        for key in self.__mappings__.keys():
            if not hasattr(self, key):
                # noinspection PyUnresolvedReferences
                value = self.__mappings__.get(key)
                if hasattr(value, "default") and getattr(value, "default") != NO_DEFAULT:
                    setattr(self, key, getattr(value, "default"))
                elif hasattr(value, "const") and getattr(value, "const") != NO_DEFAULT:
                    setattr(self, key, getattr(value, "const"))
                else:
                    setattr(self, key, None)

        # If it contains configuration that cannot be resolved, save it.
        if unknown_args:
            setattr(self, "unknown_args", unknown_args)