#!/usr/bin/env python

# 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

from __future__ import absolute_import

from brzlib import (
    cmdline,
    commands,
    config,
    help_topics,
    option,
    plugin,
)
import brzlib
import re
import sys


class BashCodeGen(object):
    """Generate a bash script for given completion data."""

    def __init__(self, data, function_name='_bzr', debug=False):
        self.data = data
        self.function_name = function_name
        self.debug = debug

    def script(self):
        return ("""\
# Programmable completion for the Bazaar-NG bzr command under bash.
# Known to work with bash 2.05a as well as bash 4.1.2, and probably
# all versions in between as well.

# Based originally on the svn bash completition script.
# Customized by Sven Wilhelm/Icecrash.com
# Adjusted for automatic generation by Martin von Gagern

# Generated using the bash_completion plugin.
# See https://launchpad.net/bzr-bash-completion for details.

# Commands and options of bzr %(bzr_version)s

shopt -s progcomp
%(function)s
complete -F %(function_name)s -o default bzr
"""     % {
            "function_name": self.function_name,
            "function": self.function(),
            "bzr_version": self.bzr_version(),
        })

    def function(self):
        return ("""\
%(function_name)s ()
{
	local cur cmds cmdIdx cmd cmdOpts fixedWords i globalOpts
	local curOpt optEnums
	local IFS=$' \\n'

	COMPREPLY=()
	cur=${COMP_WORDS[COMP_CWORD]}

	cmds='%(cmds)s'
	globalOpts=( %(global_options)s )

	# do ordinary expansion if we are anywhere after a -- argument
	for ((i = 1; i < COMP_CWORD; ++i)); do
		[[ ${COMP_WORDS[i]} == "--" ]] && return 0
	done

	# find the command; it's the first word not starting in -
	cmd=
	for ((cmdIdx = 1; cmdIdx < ${#COMP_WORDS[@]}; ++cmdIdx)); do
		if [[ ${COMP_WORDS[cmdIdx]} != -* ]]; then
			cmd=${COMP_WORDS[cmdIdx]}
			break
		fi
	done

	# complete command name if we are not already past the command
	if [[ $COMP_CWORD -le cmdIdx ]]; then
		COMPREPLY=( $( compgen -W "$cmds ${globalOpts[*]}" -- $cur ) )
		return 0
	fi

	# find the option for which we want to complete a value
	curOpt=
	if [[ $cur != -* ]] && [[ $COMP_CWORD -gt 1 ]]; then
		curOpt=${COMP_WORDS[COMP_CWORD - 1]}
		if [[ $curOpt == = ]]; then
			curOpt=${COMP_WORDS[COMP_CWORD - 2]}
		elif [[ $cur == : ]]; then
			cur=
			curOpt="$curOpt:"
		elif [[ $curOpt == : ]]; then
			curOpt=${COMP_WORDS[COMP_CWORD - 2]}:
		fi
	fi
%(debug)s
	cmdOpts=( )
	optEnums=( )
	fixedWords=( )
	case $cmd in
%(cases)s\
	*)
		cmdOpts=(--help -h)
		;;
	esac

	IFS=$'\\n'
	if [[ ${#fixedWords[@]} -eq 0 ]] && [[ ${#optEnums[@]} -eq 0 ]] && [[ $cur != -* ]]; then
		case $curOpt in
			tag:|*..tag:)
				fixedWords=( $(bzr tags 2>/dev/null | sed 's/  *[^ ]*$//; s/ /\\\\\\\\ /g;') )
				;;
		esac
		case $cur in
			[\\"\\']tag:*)
				fixedWords=( $(bzr tags 2>/dev/null | sed 's/  *[^ ]*$//; s/^/tag:/') )
				;;
			[\\"\\']*..tag:*)
				fixedWords=( $(bzr tags 2>/dev/null | sed 's/  *[^ ]*$//') )
				fixedWords=( $(for i in "${fixedWords[@]}"; do echo "${cur%%..tag:*}..tag:${i}"; done) )
				;;
		esac
	elif [[ $cur == = ]] && [[ ${#optEnums[@]} -gt 0 ]]; then
		# complete directly after "--option=", list all enum values
		COMPREPLY=( "${optEnums[@]}" )
		return 0
	else
		fixedWords=( "${cmdOpts[@]}"
		             "${globalOpts[@]}"
		             "${optEnums[@]}"
		             "${fixedWords[@]}" )
	fi

	if [[ ${#fixedWords[@]} -gt 0 ]]; then
		COMPREPLY=( $( compgen -W "${fixedWords[*]}" -- $cur ) )
	fi

	return 0
}
"""     % {
            "cmds": self.command_names(),
            "function_name": self.function_name,
            "cases": self.command_cases(),
            "global_options": self.global_options(),
            "debug": self.debug_output(),
        })
        # Help Emacs terminate strings: "

    def command_names(self):
        return " ".join(self.data.all_command_aliases())

    def debug_output(self):
        if not self.debug:
            return ''
        else:
            return (r"""
	# Debugging code enabled using the --debug command line switch.
	# Will dump some variables to the top portion of the terminal.
	echo -ne '\e[s\e[H'
	for (( i=0; i < ${#COMP_WORDS[@]}; ++i)); do
		echo "\$COMP_WORDS[$i]='${COMP_WORDS[i]}'"$'\e[K'
	done
	for i in COMP_CWORD COMP_LINE COMP_POINT COMP_TYPE COMP_KEY cur curOpt; do
		echo "\$${i}=\"${!i}\""$'\e[K'
	done
	echo -ne '---\e[K\e[u'
""")

    def bzr_version(self):
        bzr_version = brzlib.version_string
        if not self.data.plugins:
            bzr_version += "."
        else:
            bzr_version += " and the following plugins:"
            for name, plugin in sorted(self.data.plugins.iteritems()):
                bzr_version += "\n# %s" % plugin
        return bzr_version

    def global_options(self):
        return " ".join(sorted(self.data.global_options))

    def command_cases(self):
        cases = ""
        for command in self.data.commands:
            cases += self.command_case(command)
        return cases

    def command_case(self, command):
        case = "\t%s)\n" % "|".join(command.aliases)
        if command.plugin:
            case += "\t\t# plugin \"%s\"\n" % command.plugin
        options = []
        enums = []
        for option in command.options:
            for message in option.error_messages:
                case += "\t\t# %s\n" % message
            if option.registry_keys:
                for key in option.registry_keys:
                    options.append("%s=%s" % (option, key))
                enums.append("%s) optEnums=( %s ) ;;" %
                             (option, ' '.join(option.registry_keys)))
            else:
                options.append(str(option))
        case += "\t\tcmdOpts=( %s )\n" % " ".join(options)
        if command.fixed_words:
            fixed_words = command.fixed_words
            if isinstance(fixed_words, list):
                fixed_words = "( %s )" + ' '.join(fixed_words)
            case += "\t\tfixedWords=%s\n" % fixed_words
        if enums:
            case += "\t\tcase $curOpt in\n\t\t\t"
            case += "\n\t\t\t".join(enums)
            case += "\n\t\tesac\n"
        case += "\t\t;;\n"
        return case


class CompletionData(object):

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

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


class CommandData(object):

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


class PluginData(object):

    def __init__(self, name, version=None):
        if version is None:
            try:
                version = brzlib.plugin.plugins()[name].__version__
            except:
                version = 'unknown'
        self.name = name
        self.version = version

    def __str__(self):
        if self.version == 'unknown':
            return self.name
        return '%s %s' % (self.name, self.version)


class OptionData(object):

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

    def __str__(self):
        return self.name

    def __cmp__(self, other):
        return cmp(self.name, other.name)


class DataCollector(object):

    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 = set([x.replace('-', '_')
                                         for x in selected_plugins])

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

    def global_options(self):
        re_switch = re.compile(r'\n(--[A-Za-z0-9-_]+)(?:, (-\S))?\s')
        help_text = help_topics.topic_registry.get_detail('global-options')
        for long, short in re_switch.findall(help_text):
            self.data.global_options.add(long)
            if short:
                self.data.global_options.add(short)

    def aliases(self):
        for alias, expansion in config.GlobalConfig().get_aliases().iteritems():
            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.iteritems()):
            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.name: 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, 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 bash_completion_function(out, function_name="_bzr", function_only=False,
                             debug=False,
                             no_plugins=False, selected_plugins=None):
    dc = DataCollector(no_plugins=no_plugins, selected_plugins=selected_plugins)
    data = dc.collect()
    cg = BashCodeGen(data, function_name=function_name, debug=debug)
    if function_only:
        res = cg.function()
    else:
        res = cg.script()
    out.write(res)


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

    This command generates a shell function which can be used by bash to
    automatically complete the currently typed command when the user presses
    the completion key (usually tab).
    
    Commonly used like this:
        eval "`bzr bash-completion`"
    """

    takes_options = [
        option.Option("function-name", short_name="f", type=str, argname="name",
               help="Name of the generated function (default: _bzr)"),
        option.Option("function-only", short_name="o", type=None,
               help="Generate only the shell function, don't enable it"),
        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']
        bash_completion_function(sys.stdout, **kwargs)


if __name__ == '__main__':

    import locale
    import optparse

    def plugin_callback(option, opt, value, parser):
        values = parser.values.selected_plugins
        if value == '-':
            del values[:]
        else:
            values.append(value)

    parser = optparse.OptionParser(usage="%prog [-f NAME] [-o]")
    parser.add_option("--function-name", "-f", metavar="NAME",
                      help="Name of the generated function (default: _bzr)")
    parser.add_option("--function-only", "-o", action="store_true",
                      help="Generate only the shell function, don't enable it")
    parser.add_option("--debug", action="store_true",
                      help=optparse.SUPPRESS_HELP)
    parser.add_option("--no-plugins", action="store_true",
                      help="Don't load any bzr plugins")
    parser.add_option("--plugin", metavar="NAME", type="string",
                      dest="selected_plugins", default=[],
                      action="callback", callback=plugin_callback,
                      help="Enable completions for the selected plugin"
                      + " (default: all plugins)")
    (opts, args) = parser.parse_args()
    if args:
        parser.error("script does not take positional arguments")
    kwargs = dict()
    for name, value in opts.__dict__.iteritems():
        if value is not None:
            kwargs[name] = value

    locale.setlocale(locale.LC_ALL, '')
    if not kwargs.get('no_plugins', False):
        plugin.load_plugins()
    commands.install_bzr_command_hooks()
    bash_completion_function(sys.stdout, **kwargs)
