# Copyright (C) 2009, 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

import re
import sys

import breezy

from ... import cmdline, commands, config, help_topics, option, plugin


class ZshCodeGen:
    """Generate a zsh script for given completion data."""

    def __init__(self, data, function_name="_brz", debug=False):
        self.data = data
        self.function_name = function_name
        self.debug = debug

    def script(self):
        return """\
#compdef brz bzr

%(function_name)s ()
{
    local ret=1
    local -a args
    args+=(
%(global-options)s
    )

    _arguments $args[@] && ret=0

    return ret
}

%(function_name)s
""" % {"global-options": self.global_options(), "function_name": self.function_name}

    def global_options(self):
        lines = []
        for long, short, help in self.data.global_options:
            lines.append(
                "      '({}{}){}[{}]'".format(
                    (short + " ") if short else "", long, long, help
                )
            )

        return "\n".join(lines)


class CompletionData:
    def __init__(self):
        self.plugins = {}
        self.global_options = []
        self.commands = []

    def all_command_aliases(self):
        for c in self.commands:
            yield from c.aliases


class CommandData:
    def __init__(self, name):
        self.name = name
        self.aliases = [name]
        self.plugin = None
        self.options = []
        self.fixed_words = None


class PluginData:
    def __init__(self, name, version=None):
        if version is None:
            try:
                version = breezy.plugin.plugins()[name].__version__
            except:
                version = "unknown"
        self.name = name
        self.version = version

    def __str__(self):
        if self.version == "unknown":
            return self.name
        return "{} {}".format(self.name, self.version)


class OptionData:
    def __init__(self, name):
        self.name = name
        self.registry_keys = None
        self.error_messages = []

    def __str__(self):
        return self.name

    def __lt__(self, other):
        return self.name < other.name


class DataCollector:
    def __init__(self, no_plugins=False, selected_plugins=None):
        self.data = CompletionData()
        self.user_aliases = {}
        if no_plugins:
            self.selected_plugins = set()
        elif selected_plugins is None:
            self.selected_plugins = None
        else:
            self.selected_plugins = {x.replace("-", "_") for x in selected_plugins}

    def collect(self):
        self.global_options()
        self.aliases()
        self.commands()
        return self.data

    def global_options(self):
        for name, item in option.Option.OPTIONS.items():
            self.data.global_options.append(
                (
                    "--" + item.name,
                    "-" + item.short_name() if item.short_name() else None,
                    item.help.rstrip(),
                )
            )

    def aliases(self):
        for alias, expansion in config.GlobalConfig().get_aliases().items():
            for token in cmdline.split(expansion):
                if not token.startswith("-"):
                    self.user_aliases.setdefault(token, set()).add(alias)
                    break

    def commands(self):
        for name in sorted(commands.all_command_names()):
            self.command(name)

    def command(self, name):
        cmd = commands.get_cmd_object(name)
        cmd_data = CommandData(name)

        plugin_name = cmd.plugin_name()
        if plugin_name is not None:
            if (
                self.selected_plugins is not None
                and plugin not in self.selected_plugins
            ):
                return None
            plugin_data = self.data.plugins.get(plugin_name)
            if plugin_data is None:
                plugin_data = PluginData(plugin_name)
                self.data.plugins[plugin_name] = plugin_data
            cmd_data.plugin = plugin_data
        self.data.commands.append(cmd_data)

        # Find all aliases to the command; both cmd-defined and user-defined.
        # We assume a user won't override one command with a different one,
        # but will choose completely new names or add options to existing
        # ones while maintaining the actual command name unchanged.
        cmd_data.aliases.extend(cmd.aliases)
        cmd_data.aliases.extend(
            sorted(
                [
                    useralias
                    for cmdalias in cmd_data.aliases
                    if cmdalias in self.user_aliases
                    for useralias in self.user_aliases[cmdalias]
                    if useralias not in cmd_data.aliases
                ]
            )
        )

        opts = cmd.options()
        for optname, opt in sorted(opts.items()):
            cmd_data.options.extend(self.option(opt))

        if "help" == name or "help" in cmd.aliases:
            cmd_data.fixed_words = "($cmds %s)" % " ".join(
                sorted(help_topics.topic_registry.keys())
            )

        return cmd_data

    def option(self, opt):
        optswitches = {}
        parser = option.get_optparser([opt])
        parser = self.wrap_parser(optswitches, parser)
        optswitches.clear()
        opt.add_option(parser, opt.short_name())
        if isinstance(opt, option.RegistryOption) and opt.enum_switch:
            enum_switch = "--%s" % opt.name
            enum_data = optswitches.get(enum_switch)
            if enum_data:
                try:
                    enum_data.registry_keys = opt.registry.keys()
                except ImportError as e:
                    enum_data.error_messages.append(
                        "ERROR getting registry keys for '--%s': %s"
                        % (opt.name, str(e).split("\n")[0])
                    )
        return sorted(optswitches.values())

    def wrap_container(self, optswitches, parser):
        def tweaked_add_option(*opts, **attrs):
            for name in opts:
                optswitches[name] = OptionData(name)

        parser.add_option = tweaked_add_option
        return parser

    def wrap_parser(self, optswitches, parser):
        orig_add_option_group = parser.add_option_group

        def tweaked_add_option_group(*opts, **attrs):
            return self.wrap_container(
                optswitches, orig_add_option_group(*opts, **attrs)
            )

        parser.add_option_group = tweaked_add_option_group
        return self.wrap_container(optswitches, parser)


def zsh_completion_function(
    out, function_name="_brz", debug=False, no_plugins=False, selected_plugins=None
):
    dc = DataCollector(no_plugins=no_plugins, selected_plugins=selected_plugins)
    data = dc.collect()
    cg = ZshCodeGen(data, function_name=function_name, debug=debug)
    res = cg.script()
    out.write(res)


class cmd_zsh_completion(commands.Command):
    __doc__ = """Generate a shell function for zsh command line completion.

    This command generates a shell function which can be used by zsh to
    automatically complete the currently typed command when the user presses
    the completion key (usually tab).

    Commonly used like this:
        eval "`brz zsh-completion`"
    """

    takes_options = [
        option.Option(
            "function-name",
            short_name="f",
            type=str,
            argname="name",
            help="Name of the generated function (default: _brz)",
        ),
        option.Option(
            "debug",
            type=None,
            hidden=True,
            help="Enable shell code useful for debugging",
        ),
        option.ListOption(
            "plugin",
            type=str,
            argname="name",
            # param_name="selected_plugins", # doesn't work, bug #387117
            help="Enable completions for the selected plugin"
            + " (default: all plugins)",
        ),
    ]

    def run(self, **kwargs):
        if "plugin" in kwargs:
            # work around bug #387117 which prevents us from using param_name
            if len(kwargs["plugin"]) > 0:
                kwargs["selected_plugins"] = kwargs["plugin"]
            del kwargs["plugin"]
        zsh_completion_function(sys.stdout, **kwargs)
