########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Lib/CommandLine/Command.py,v 1.13 2004-08-06 04:57:08 mbrown Exp $
"""
Superclass for a command that can be invoked by a command-line script.

Copyright 2004 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

import string, sys

import Options
import Arguments
import CommandLineUtil
import FancyGetOpt

from Ft.Lib.CommandLine import CONSOLE_WIDTH

class Command:
    """
    Superclass for a command that can be invoked by a command-line script.
    Most commands won't need to subclass this.

    A Command object encapsulates, for a particular command, a description,
    usage example, a set of valid options & arguments, methods for
    validating the actual options and arguments entered, a function for
    command invocation, and an association with subordinate Commands.

    A tree of commands can be created by associating each Command instance
    with its subordinates. Typically, only the leaves of the tree will have
    functionality; the branches just provide ways of grouping the leaves
    and will not need to encapsulate the invocation functions themselves.
    """ # -- clear as mud. how about some examples
    def __init__(self,
                 name,
                 description,
                 example,
                 verbose_description,
                 function = None,
                 options=None,
                 arguments = None,
                 subCommands = None,
                 authenticate = 0,
                 heading = None,
                 fileName = None,
                 ):

        self.name = name
        self.description = description
        self.function = function
        self.example = example
        self.verbose_description = verbose_description
        self.options = options or Options.Options()
        self.arguments = arguments or []
        self.subCommands = subCommands or {}
        self.authenticate = authenticate
        self.heading = heading
        self._fileName = fileName

        if type(self.subCommands) == type([]):
            cmds = {}
            for c in self.subCommands:
                cmds[c.name] = c
            self.subCommands = cmds


        #Validate Options
        if not isinstance(self.options,Options.Options):
            raise Exception("Options not an instance of Options.Options")

        for a in self.arguments:
            if not isinstance(a,Arguments.Argument):
                raise Exception("Argument is not an Argument: %s" % str(a))

    def build_parent_relationship(self):
        for c in self.subCommands.values():
            c.parent = self
            c.build_parent_relationship()

    def break_parent_relationship(self):
        for c in self.subCommands.values():
            if hasattr(c,'parent'):
                delattr(c,'parent')
            c.break_parent_relationship()


    def flatten_command_tree(self,level,previousName=''):
        if previousName:
            fName = previousName + '.' + self.name
        else:
            fName = self.name

        res = [(level,self,fName)]
        names = self.subCommands.keys()
        names.sort()
        for name in names:
            res.extend(self.subCommands[name].flatten_command_tree(
                level+1,previousName = fName))
        return res


    def run_command(self,auth):
        if self.authenticate:
            self.clOptions['AUTHENTICATION'] = auth(
                options=self.clOptions,
                level=self.authenticate)
        if self.heading:
            sys.stderr.write(self.heading)
        self.function(self.clOptions, self.clArguments)
        return


    def validate_arguments(self,args):
        eatenArgs = {}
        uneatenArgs = filter(string.strip, args)
        for arg in self.arguments:
            eaten,uneatenArgs = arg.validate(self,uneatenArgs)
            if eaten is not None:
                eatenArgs[arg.name] = eaten

        if len(uneatenArgs):
            raise CommandLineUtil.ArgumentError(self,
                "Unknown Argument %s" % uneatenArgs[0])
        return eatenArgs

    def validate_options(self,options):
        if options.get('help'):
            raise CommandLineUtil.ArgumentError(self,'')
        for opt in self.options:
            opt.apply_options(options)
        for opt in self.options:
            opt.validate()
        return 1


    def _gen_usage(self,command_string):
        """
        Generates the usage summary, example command line, and
        descriptions of options and subcommands or arguments
        """
        lines = self.__gen_command_line_help(command_string)

        # saved for use in the example
        command_size = len(command_string)

        # generate the example, if available
        if self.example is not None:
            lines.append('\nExample:')
            text_width = CONSOLE_WIDTH - command_size
            text = CommandLineUtil.wrap_text(self.example, text_width)
            lines.append(command_string + text[0])
            indent = ' ' * command_size
            lines.extend([ indent + s for s in text[1:] ])

        # generate the option descriptions
        option_desc = self.options.generate_help()
        if option_desc:
            lines.append('\nOptions:')
            lines.extend(option_desc)

        # generate the subcommand descriptions
        if self.subCommands:
            max_cmd = 0
            for cmd in self.subCommands.keys():
                if len(cmd) > max_cmd:
                    max_cmd = len(cmd)

            lines.append('\nSubcommands:')
            # column size = indent + longest command + gutter
            indent = ' ' * (2 + max_cmd + 2) + '  '
            text_width = CONSOLE_WIDTH - len(indent)
            names = self.subCommands.keys()
            names.sort()
            for name in names:
                cmd = self.subCommands[name]
                text = CommandLineUtil.wrap_text(cmd.description,
                                                 text_width)
                lines.append('  %-*s  %s' % (max_cmd, name, text[0]))
                lines.extend([ indent + s for s in text[1:] ])

        # generate the argument descriptions
        if self.arguments:
            max_arg = 0
            for arg in self.arguments:
                if len(arg.name) > max_arg:
                    max_arg = len(arg.name)

            lines.append('\nArguments:')
            # column size = indent + longest command + gutter
            indent = ' ' * (2 + max_arg + 2)
            text_width = CONSOLE_WIDTH - len(indent)
            for arg in self.arguments:
                text = CommandLineUtil.wrap_text(arg.description,
                                                 text_width)
                lines.append('  %-*s  %s' % (max_arg, arg.name,
                                             text[0]))
                lines.extend([ indent + s for s in text[1:] ])

        lines.append('')    # add a blank line
        return lines


    def _parse_command_opts(self, args):
        (options, args) = FancyGetOpt.FancyGetopt(self, self.options,
                                                  args)

        if not self.validate_options(options):return None
        self.clOptions = options

        # Args can either be a subcommand, or the start of the argument
        # list. Subcommand takes precedence.
        if len(args) and self.subCommands.has_key(args[0]):
            #It is a subcommand
            cmd = self.subCommands[args[0]]
            finalCmd = cmd._parse_command_opts(args[1:],)
        else:
            #They are arguments
            finalCmd = self
            self.clArguments = self.validate_arguments(args)
        return finalCmd


    def __gen_command_line_help(self, command_string):
        """
        Generates the indented usage summary only.
        """
        # command_string looks like this: '  4ss create user '
        # i.e., it is '4ss create user', indented and ready to
        # have the option & argument syntax appended like so:
        # '  4ss create user (syntax line 1)
        #                    (syntax line 2)
        #                    (syntax line 3)'

        # generate the option/arg syntax string, which looks like this:
        # '[--help] [[--username=<USER>] | [--anonymous]]
        #  [--password=<PASSWORD>] [--port=<PORT>] [--host=<HOST>]
        #  [--container=<PATH>] userName '
        syntax_string = ''
        for opt in self.options:
            syntax_string += opt.gen_command_line() + ' '
        if self.subCommands:
            if len(self.arguments):
                syntax_string += '['
            syntax_string += '<subcommand> '
            if len(self.arguments):
                syntax_string += '['
            syntax_string += '<subcommand> '
            if len(self.arguments):
                syntax_string += '] | ['
        if self.arguments:
            for arg in self.arguments:
                syntax_string += arg.gen_command_line() + ' '
            if self.subCommands:
                syntax_string += ']'

        # wrap the syntax line to a width that won't exceed the console
        # width when appended to command_string
        command_size = len(command_string)
        syntax_string_lines = CommandLineUtil.wrap_text(
            syntax_string, CONSOLE_WIDTH - command_size)

        # append first line of wrapped syntax string to command_string
        # and indent the remaining lines appropriately
        lines = [command_string + syntax_string_lines[0]]
        indent = ' ' * command_size
        lines.extend([ indent + s for s in syntax_string_lines[1:] ])


        return lines
