# Copyright (C) 2005-2010 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# 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, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

# TODO: For things like --diff-prefix, we want a way to customize the display
# of the option argument.

__docformat__ = "google"

import optparse
import re
from typing import Callable, Dict

from . import errors
from . import registry as _mod_registry
from . import revisionspec


class BadOptionValue(errors.BzrError):
    _fmt = """Bad value "%(value)s" for option "%(name)s"."""

    def __init__(self, name, value):
        errors.BzrError.__init__(self, name=name, value=value)


def _parse_revision_str(revstr):
    """This handles a revision string -> revno.

    This always returns a list.  The list will have one element for
    each revision specifier supplied.

    >>> _parse_revision_str('234')
    [<RevisionSpec_dwim 234>]
    >>> _parse_revision_str('234..567')
    [<RevisionSpec_dwim 234>, <RevisionSpec_dwim 567>]
    >>> _parse_revision_str('..')
    [<RevisionSpec None>, <RevisionSpec None>]
    >>> _parse_revision_str('..234')
    [<RevisionSpec None>, <RevisionSpec_dwim 234>]
    >>> _parse_revision_str('234..')
    [<RevisionSpec_dwim 234>, <RevisionSpec None>]
    >>> _parse_revision_str('234..456..789') # Maybe this should be an error
    [<RevisionSpec_dwim 234>, <RevisionSpec_dwim 456>, <RevisionSpec_dwim 789>]
    >>> _parse_revision_str('234....789') #Error ?
    [<RevisionSpec_dwim 234>, <RevisionSpec None>, <RevisionSpec_dwim 789>]
    >>> _parse_revision_str('revid:test@other.com-234234')
    [<RevisionSpec_revid revid:test@other.com-234234>]
    >>> _parse_revision_str('revid:test@other.com-234234..revid:test@other.com-234235')
    [<RevisionSpec_revid revid:test@other.com-234234>, <RevisionSpec_revid revid:test@other.com-234235>]
    >>> _parse_revision_str('revid:test@other.com-234234..23')
    [<RevisionSpec_revid revid:test@other.com-234234>, <RevisionSpec_dwim 23>]
    >>> _parse_revision_str('date:2005-04-12')
    [<RevisionSpec_date date:2005-04-12>]
    >>> _parse_revision_str('date:2005-04-12 12:24:33')
    [<RevisionSpec_date date:2005-04-12 12:24:33>]
    >>> _parse_revision_str('date:2005-04-12T12:24:33')
    [<RevisionSpec_date date:2005-04-12T12:24:33>]
    >>> _parse_revision_str('date:2005-04-12,12:24:33')
    [<RevisionSpec_date date:2005-04-12,12:24:33>]
    >>> _parse_revision_str('-5..23')
    [<RevisionSpec_dwim -5>, <RevisionSpec_dwim 23>]
    >>> _parse_revision_str('-5')
    [<RevisionSpec_dwim -5>]
    >>> _parse_revision_str('123a')
    [<RevisionSpec_dwim 123a>]
    >>> _parse_revision_str('abc')
    [<RevisionSpec_dwim abc>]
    >>> _parse_revision_str('branch:../branch2')
    [<RevisionSpec_branch branch:../branch2>]
    >>> _parse_revision_str('branch:../../branch2')
    [<RevisionSpec_branch branch:../../branch2>]
    >>> _parse_revision_str('branch:../../branch2..23')
    [<RevisionSpec_branch branch:../../branch2>, <RevisionSpec_dwim 23>]
    >>> _parse_revision_str('branch:..\\\\branch2')
    [<RevisionSpec_branch branch:..\\branch2>]
    >>> _parse_revision_str('branch:..\\\\..\\\\branch2..23')
    [<RevisionSpec_branch branch:..\\..\\branch2>, <RevisionSpec_dwim 23>]
    """
    # TODO: Maybe move this into revisionspec.py
    revs = []
    # split on .. that is not followed by a / or \
    sep = re.compile(r"\.\.(?![\\/])")
    for x in sep.split(revstr):
        revs.append(revisionspec.RevisionSpec.from_string(x or None))
    return revs


def _parse_change_str(revstr):
    """Parse the revision string and return a tuple with left-most
    parent of the revision.

    >>> _parse_change_str('123')
    (<RevisionSpec_before before:123>, <RevisionSpec_dwim 123>)
    >>> _parse_change_str('123..124')
    Traceback (most recent call last):
      ...
    breezy.errors.RangeInChangeOption: Option --change does not accept revision ranges
    """
    revs = _parse_revision_str(revstr)
    if len(revs) > 1:
        raise errors.RangeInChangeOption()
    return (revisionspec.RevisionSpec.from_string("before:" + revstr), revs[0])


def _parse_merge_type(typestring):
    return get_merge_type(typestring)


def get_merge_type(typestring):
    """Attempt to find the merge class/factory associated with a string."""
    from merge import merge_types

    try:
        return merge_types[typestring][0]
    except KeyError:
        templ = "%s%%7s: %%s" % (" " * 12)
        lines = [templ % (f[0], f[1][1]) for f in merge_types.items()]
        type_list = "\n".join(lines)
        msg = "No known merge type %s. Supported types are:\n%s" % (
            typestring,
            type_list,
        )
        raise errors.CommandError(msg)


class Option:
    """Description of a command line option

    Attributes:
      _short_name: If this option has a single-letter name, this is it.
         Otherwise None.
    """

    # The dictionary of standard options. These are always legal.
    STD_OPTIONS: Dict[str, "Option"] = {}

    # The dictionary of commonly used options. these are only legal
    # if a command explicitly references them by name in the list
    # of supported options.
    OPTIONS: Dict[str, "Option"] = {}

    def __init__(
        self,
        name,
        help="",
        type=None,
        argname=None,
        short_name=None,
        param_name=None,
        custom_callback=None,
        hidden=False,
    ):
        """Make a new command option.

        Args:
          name: regular name of the command, used in the double-dash
            form and also as the parameter to the command's run()
            method (unless param_name is specified).
          help: help message displayed in command help
          type: function called to parse the option argument, or
            None (default) if this option doesn't take an argument.
          argname: name of option argument, if any
          short_name: short option code for use with a single -, e.g.
            short_name="v" to enable parsing of -v.
          param_name: name of the parameter which will be passed to
            the command's run() method.
          custom_callback: a callback routine to be called after normal
            processing. The signature of the callback routine is
            (option, name, new_value, parser).
          hidden: If True, the option should be hidden in help and
            documentation.
        """
        self.name = name
        self.help = help
        self.type = type
        self._short_name = short_name
        if type is None:
            if argname:
                raise ValueError("argname not valid for booleans")
        elif argname is None:
            argname = "ARG"
        self.argname = argname
        if param_name is None:
            self._param_name = self.name.replace("-", "_")
        else:
            self._param_name = param_name
        self.custom_callback = custom_callback
        self.hidden = hidden

    def short_name(self):
        if self._short_name:
            return self._short_name

    def set_short_name(self, short_name):
        self._short_name = short_name

    def get_negation_name(self):
        if self.name.startswith("no-"):
            return self.name[3:]
        else:
            return "no-" + self.name

    def add_option(self, parser, short_name):
        """Add this option to an Optparse parser"""
        option_strings = ["--%s" % self.name]
        if short_name is not None:
            option_strings.append("-%s" % short_name)
        if self.hidden:
            help = optparse.SUPPRESS_HELP
        else:
            help = self.help
        optargfn = self.type
        if optargfn is None:
            parser.add_option(
                action="callback",
                callback=self._optparse_bool_callback,
                callback_args=(True,),
                help=help,
                *option_strings,
            )
            negation_strings = ["--%s" % self.get_negation_name()]
            parser.add_option(
                action="callback",
                callback=self._optparse_bool_callback,
                callback_args=(False,),
                help=optparse.SUPPRESS_HELP,
                *negation_strings,
            )
        else:
            parser.add_option(
                action="callback",
                callback=self._optparse_callback,
                type="string",
                metavar=self.argname.upper(),
                help=help,
                default=OptionParser.DEFAULT_VALUE,
                *option_strings,
            )

    def _optparse_bool_callback(self, option, opt_str, value, parser, bool_v):
        setattr(parser.values, self._param_name, bool_v)
        if self.custom_callback is not None:
            self.custom_callback(option, self._param_name, bool_v, parser)

    def _optparse_callback(self, option, opt, value, parser):
        try:
            v = self.type(value)
        except ValueError as e:
            raise optparse.OptionValueError(
                "invalid value for option {}: {}".format(option, value)
            )
        setattr(parser.values, self._param_name, v)
        if self.custom_callback is not None:
            self.custom_callback(option, self.name, v, parser)

    def iter_switches(self):
        """Iterate through the list of switches provided by the option

        :return: an iterator of (name, short_name, argname, help)
        """
        argname = self.argname
        if argname is not None:
            argname = argname.upper()
        yield self.name, self.short_name(), argname, self.help

    def is_hidden(self, name):
        return self.hidden


class ListOption(Option):
    """Option used to provide a list of values.

    On the command line, arguments are specified by a repeated use of the
    option. '-' is a special argument that resets the list. For example,
      --foo=a --foo=b
    sets the value of the 'foo' option to ['a', 'b'], and
      --foo=a --foo=b --foo=- --foo=c
    sets the value of the 'foo' option to ['c'].
    """

    def add_option(self, parser, short_name):
        """Add this option to an Optparse parser."""
        option_strings = ["--%s" % self.name]
        if short_name is not None:
            option_strings.append("-%s" % short_name)
        parser.add_option(
            action="callback",
            callback=self._optparse_callback,
            type="string",
            metavar=self.argname.upper(),
            help=self.help,
            dest=self._param_name,
            default=[],
            *option_strings,
        )

    def _optparse_callback(self, option, opt, value, parser):
        values = getattr(parser.values, self._param_name)
        if value == "-":
            del values[:]
        else:
            values.append(self.type(value))
        if self.custom_callback is not None:
            self.custom_callback(option, self._param_name, values, parser)


class RegistryOption(Option):
    """Option based on a registry

    The values for the options correspond to entries in the registry.  Input
    must be a registry key.  After validation, it is converted into an object
    using Registry.get or a caller-provided converter.
    """

    def validate_value(self, value):
        """Validate a value name"""
        if value not in self.registry:
            raise BadOptionValue(self.name, value)

    def convert(self, value):
        """Convert a value name into an output type"""
        self.validate_value(value)
        if self.converter is None:
            return self.registry.get(value)
        else:
            return self.converter(value)

    def __init__(
        self,
        name,
        help,
        registry=None,
        converter=None,
        value_switches=False,
        title=None,
        enum_switch=True,
        lazy_registry=None,
        short_name=None,
        short_value_switches=None,
    ):
        """
        Constructor.

        Args:
          name: The option name.
          help: Help for the option.
          registry: A Registry containing the values
          converter: Callable to invoke with the value name to produce
            the value.  If not supplied, self.registry.get is used.
          value_switches: If true, each possible value is assigned its
            own switch.  For example, instead of '--format knit',
            '--knit' can be used interchangeably.
          enum_switch: If true, a switch is provided with the option name,
            which takes a value.
          lazy_registry: A tuple of (module name, attribute name) for a
            registry to be lazily loaded.
          short_name: The short name for the enum switch, if any
          short_value_switches: A dict mapping values to short names
        """
        Option.__init__(self, name, help, type=self.convert, short_name=short_name)
        self._registry = registry
        if registry is None:
            if lazy_registry is None:
                raise AssertionError("One of registry or lazy_registry must be given.")
            self._lazy_registry = _mod_registry._LazyObjectGetter(*lazy_registry)
        if registry is not None and lazy_registry is not None:
            raise AssertionError("registry and lazy_registry are mutually exclusive")
        self.name = name
        self.converter = converter
        self.value_switches = value_switches
        self.enum_switch = enum_switch
        self.short_value_switches = short_value_switches
        self.title = title
        if self.title is None:
            self.title = name

    @property
    def registry(self):
        if self._registry is None:
            self._registry = self._lazy_registry.get_obj()
        return self._registry

    @staticmethod
    def from_kwargs(
        name_, help=None, title=None, value_switches=False, enum_switch=True, **kwargs
    ):
        """Convenience method to generate string-map registry options

        name, help, value_switches and enum_switch are passed to the
        RegistryOption constructor.  Any other keyword arguments are treated
        as values for the option, and their value is treated as the help.
        """
        reg = _mod_registry.Registry()
        for name, switch_help in sorted(kwargs.items()):
            name = name.replace("_", "-")
            reg.register(name, name, help=switch_help)
            if not value_switches:
                help = help + '  "' + name + '": ' + switch_help
                if not help.endswith("."):
                    help = help + "."
        return RegistryOption(
            name_,
            help,
            reg,
            title=title,
            value_switches=value_switches,
            enum_switch=enum_switch,
        )

    def add_option(self, parser, short_name):
        """Add this option to an Optparse parser"""
        if self.value_switches:
            parser = parser.add_option_group(self.title)
        if self.enum_switch:
            Option.add_option(self, parser, short_name)
        if self.value_switches:
            alias_map = self.registry.alias_map()
            for key in self.registry.keys():
                if key in self.registry.aliases():
                    continue
                option_strings = [
                    ("--%s" % name)
                    for name in [key]
                    + [
                        alias
                        for alias in alias_map.get(key, [])
                        if not self.is_hidden(alias)
                    ]
                ]
                if self.is_hidden(key):
                    help = optparse.SUPPRESS_HELP
                else:
                    help = self.registry.get_help(key)
                if self.short_value_switches and key in self.short_value_switches:
                    option_strings.append("-%s" % self.short_value_switches[key])
                parser.add_option(
                    action="callback",
                    callback=self._optparse_value_callback(key),
                    help=help,
                    *option_strings,
                )

    def _optparse_value_callback(self, cb_value):
        def cb(option, opt, value, parser):
            v = self.type(cb_value)
            setattr(parser.values, self._param_name, v)
            if self.custom_callback is not None:
                self.custom_callback(option, self._param_name, v, parser)

        return cb

    def iter_switches(self):
        """Iterate through the list of switches provided by the option

        :return: an iterator of (name, short_name, argname, help)
        """
        yield from Option.iter_switches(self)
        if self.value_switches:
            for key in sorted(self.registry.keys()):
                yield key, None, None, self.registry.get_help(key)

    def is_alias(self, name):
        """Check whether a particular format is an alias."""
        if name == self.name:
            return False
        return name in self.registry.aliases()

    def is_hidden(self, name):
        if name == self.name:
            return Option.is_hidden(self, name)
        return getattr(self.registry.get_info(name), "hidden", False)


class OptionParser(optparse.OptionParser):
    """OptionParser that raises exceptions instead of exiting"""

    DEFAULT_VALUE = object()

    def __init__(self):
        optparse.OptionParser.__init__(self)
        self.formatter = GettextIndentedHelpFormatter()

    def error(self, message):
        raise errors.CommandError(message)


class GettextIndentedHelpFormatter(optparse.IndentedHelpFormatter):
    """Adds gettext() call to format_option()"""

    def __init__(self):
        optparse.IndentedHelpFormatter.__init__(self)

    def format_option(self, option):
        """code taken from Python's optparse.py"""
        if option.help:
            from .i18n import gettext

            option.help = gettext(option.help)
        return optparse.IndentedHelpFormatter.format_option(self, option)


def get_optparser(options):
    """Generate an optparse parser for breezy-style options"""

    parser = OptionParser()
    parser.remove_option("--help")
    for option in options:
        option.add_option(parser, option.short_name())
    return parser


def custom_help(name, help):
    """Clone a common option overriding the help."""
    import copy

    o = copy.copy(Option.OPTIONS[name])
    o.help = help
    return o


def _standard_option(name, **kwargs):
    """Register a standard option."""
    # All standard options are implicitly 'global' ones
    Option.STD_OPTIONS[name] = Option(name, **kwargs)
    Option.OPTIONS[name] = Option.STD_OPTIONS[name]


def _standard_list_option(name, **kwargs):
    """Register a standard option."""
    # All standard options are implicitly 'global' ones
    Option.STD_OPTIONS[name] = ListOption(name, **kwargs)
    Option.OPTIONS[name] = Option.STD_OPTIONS[name]


def _global_option(name, **kwargs):
    """Register a global option."""
    Option.OPTIONS[name] = Option(name, **kwargs)


def _global_registry_option(name, help, registry=None, **kwargs):
    Option.OPTIONS[name] = RegistryOption(name, help, registry, **kwargs)


# This is the verbosity level detected during command line parsing.
# Note that the final value is dependent on the order in which the
# various flags (verbose, quiet, no-verbose, no-quiet) are given.
# The final value will be one of the following:
#
# * -ve for quiet
# * 0 for normal
# * +ve for verbose
_verbosity_level = 0


def _verbosity_level_callback(option, opt_str, value, parser):
    global _verbosity_level
    if not value:
        # Either --no-verbose or --no-quiet was specified
        _verbosity_level = 0
    elif opt_str == "verbose":
        if _verbosity_level > 0:
            _verbosity_level += 1
        else:
            _verbosity_level = 1
    else:
        if _verbosity_level < 0:
            _verbosity_level -= 1
        else:
            _verbosity_level = -1


# Declare the standard options
_standard_option("help", short_name="h", help="Show help message.")
_standard_option(
    "quiet",
    short_name="q",
    help="Only display errors and warnings.",
    custom_callback=_verbosity_level_callback,
)
_standard_option("usage", help="Show usage message and options.")
_standard_option(
    "verbose",
    short_name="v",
    help="Display more information.",
    custom_callback=_verbosity_level_callback,
)

# Declare commonly used options
_global_option(
    "change",
    type=_parse_change_str,
    short_name="c",
    param_name="revision",
    help='Select changes introduced by the specified revision. See also "help revisionspec".',
)
_global_option(
    "directory",
    short_name="d",
    type=str,
    help="Branch to operate on, instead of working directory.",
)
_global_option("file", type=str, short_name="F")
_global_registry_option(
    "log-format",
    "Use specified log format.",
    lazy_registry=("breezy.log", "log_formatter_registry"),
    value_switches=True,
    title="Log format",
    short_value_switches={"short": "S"},
)
_global_registry_option(
    "merge-type",
    "Select a particular merge algorithm.",
    lazy_registry=("breezy.merge", "merge_type_registry"),
    value_switches=True,
    title="Merge algorithm",
)
_global_option("message", type=str, short_name="m", help="Message string.")
_global_option(
    "null",
    short_name="0",
    help="Use an ASCII NUL (\\0) separator rather than " "a newline.",
)
_global_option(
    "overwrite",
    help="Ignore differences between branches and " "overwrite unconditionally.",
)
_global_option("remember", help="Remember the specified location as a" " default.")
_global_option("reprocess", help="Reprocess to reduce spurious conflicts.")
_global_option(
    "revision",
    type=_parse_revision_str,
    short_name="r",
    help='See "help revisionspec" for details.',
)
_global_option("show-ids", help="Show internal object ids.")
_global_option(
    "timezone", type=str, help="Display timezone as local, original, or utc."
)

diff_writer_registry = _mod_registry.Registry[str, Callable]()
diff_writer_registry.register("plain", lambda x: x, "Plaintext diff output.")
diff_writer_registry.default_key = "plain"
