# Copyright (c) 2001 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# 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.,
# 59 Temple Place - Suite 330, Boston, MA  02111-1307
# USA.

"""
HornCli is the command-line interface to Narval
"""

__revision__ = "$Id: HornCli.py,v 1.26 2002/02/27 17:45:58 alf Exp $"

__all__ = ["HornCli"]

from HornActioner import *
from shlex import shlex
from StringIO import StringIO
import string
from narval.gui import unicode_to_gtk
from xml.dom.ext import PrettyPrint

# Completer ####################################################################

class Completer :

    def __init__(self,commands) :
        self.list = commands
        
    def complete(self,text,state) :
        n = len(text)
        matches = []
        for cmd in self.list :
            if cmd[:n] == text :
                matches.append(cmd)
        try:
            return matches[state]
        except IndexError:
            return None

# HornCli #######################################################################

class HornCommands :
    """
    HornCommands regroups commands that can be executed from the Horn shell
    """

    def __init__(self,actioner) :
        self._hactioner = actioner
        self.topics = ( unicode_to_gtk(_("Package")),
                   unicode_to_gtk(_("Narval")),
                   unicode_to_gtk(_("Memory")),
                   unicode_to_gtk(_("Debug")),
                   unicode_to_gtk(_("Recipe")),
                   unicode_to_gtk(_("Logging")),
                   unicode_to_gtk(_("Others")),
                   )
        print self.topics

    def __del__(self) :
        self._hactioner.disconnect_from_narval()

    def _print_help(self,((cmd,syntax,explanation),nb_args)):
        print unicode_to_gtk(_('Command %s'))%cmd
        print unicode_to_gtk(_('Syntax:')),syntax
        print '\t',explanation
        print

    def do_help(self, command=None) :

        if command==None or command not in self.topics:
            print unicode_to_gtk(_("Use help <topic>."))
            print unicode_to_gtk(_("Available topics are:"))
            for t in self.topics:
                print '\t',t
        else:
            print unicode_to_gtk(_('Available commands about %s'))%command
            if command == self.topics[0]:
                self._print_help(self.help_install())
                self._print_help(self.help_uninstall())
            elif command == self.topics[1]:
                self._print_help(self.help_connect_to())
                self._print_help(self.help_disconnect())
                self._print_help(self.help_local())
            elif command == self.topics[2]:
                self._print_help(self.help_get_memory())
                self._print_help(self.help_select_elements())
                self._print_help(self.help_get_element())
                self._print_help(self.help_add_element())
                self._print_help(self.help_remove_element())
                self._print_help(self.help_replace_element())
                self._print_help(self.help_start_plan())
                self._print_help(self.help_save_recipes())
                self._print_help(self.help_save_transforms())
            elif command == self.topics[3]:
                self._print_help(self.help_debug_step_one_step())
                self._print_help(self.help_debug_suspend())
                self._print_help(self.help_debug_continue())
            elif command == self.topics[4]:
                self._print_help(self.help_new_recipe())
                self._print_help(self.help_save_recipe())
                self._print_help(self.help_add_action_to_recipe())
                self._print_help(self.help_add_transform_to_recipe())
                self._print_help(self.help_add_recipe_to_recipe())
                self._print_help(self.help_add_argument_to_step())
                self._print_help(self.help_connect_elements())
                self._print_help(self.help_add_transition_to_recipe())
                self._print_help(self.help_remove_element_from_recipe())
                self._print_help(self.help_remove_connection_from_recipe())
                self._print_help(self.help_connect_elements())
                self._print_help(self.help_add_attribute_to_recipe())
                self._print_help(self.help_add_condition_to_transition())
            elif command == self.topics[5]:
                self._print_help(self.help_put_into_logfile())
                self._print_help(self.help_close_logfile())
            elif command == self.topics[6]:
                self._print_help((("help","help [topic]",unicode_to_gtk(_("print this help message"))),1))
                self._print_help((("quit","quit",unicode_to_gtk(_("quit"))),0))

    # install
    def do_install(self,pkgfile) :
        self._hactioner.install(pkgfile)

    def help_install(self) :
        return (('install',
                "install [name.npm]",
                unicode_to_gtk(_("install package"))),1)
        
    # uninstall
    def do_uninstall(self,manifest) :
        self._hactioner.uninstall(manifest)

    def help_uninstall(self) :
        return (('uninstall',
                 "uninstall [name.manifest]",
                 unicode_to_gtk(_("uninstall installed package"))), 1)

    # local
    def do_local(self) :
        self._hactioner.start_local()

    def help_local(self) :
        return (('local',"local",unicode_to_gtk(_("load data from NARVAL_HOME"))), 0)

    # connect-to
    def do_connect_to(self,host,port,protocol=RPC_PROTO) :
        self._hactioner.connect_to_narval(host,int(port),protocol)

    def help_connect_to(self) :
        return (("connect_to","connect_to [host]:[port] <protocol>",
                unicode_to_gtk(_("connect to Narval"))),2)

    # disconnect
    def do_disconnect(self) :
        self._hactioner.disconnect_from_narval()

    def help_disconnect(self) :
        return (("disconnect","disconnect",unicode_to_gtk(_("disconnect from Narval"))),0)
        
    # get-memory
    def do_get_memory(self) :
        PrettyPrint(self._hactioner.getMemory())

    def help_get_memory(self) :
        return (("get_memory","get_memory",
                unicode_to_gtk(_("get the whole memory and print it. This can take time"))), 0)
        
    # select-elements
    def do_select_elements(self,xpath) :
        print self._hactioner.selectElements(xpath)

    def help_select_elements(self) :
        return (("select_elements","select_elements [xpath]",
                unicode_to_gtk(_("selects all elements matching xpath."))),1)

    # get-element
    def do_get_element(self,id) :
        print self._hactioner.getElement(id)

    def help_get_element(self) :
        return (("get_element","get_element [id]",
                unicode_to_gtk(_("print the element with given id"))),1)

    # add-element
    def do_add_element(self,xmlstr) :
        self._hactioner.addElement(xmlstr)

    def help_add_element(self) :
        return (("add_element","add_element [xml]",
                unicode_to_gtk(_("add element described by the xml string"))),1)

    # remove-element
    def do_remove_element(self,id) :
        self._hactioner.removeElement(id)

    def help_remove_element(self) :
        return (("remove_element","remove_element [id]",
                unicode_to_gtk(_("remove element with given id"))),1)

    # replace-element
    def do_replace_element(self,id,xmlstr) :
        self._hactioner.replaceElement(id,xmlstr)

    def help_replace_element(self):
        return (("replace_element","replace_element [id] [xml]",
                unicode_to_gtk(_("replace element with given id by the xml one"))),2)

    # save-recipes
    def do_save_recipes(self) :
        self._hactioner.saveRecipes()

    def help_save_recipes(self) :
        return (("save_recipes","save_recipes",
                unicode_to_gtk(_("save all recipes to NARVAL_HOME"))),0)

    # save-transforms
    def do_save_transforms(self) :
        self._hactioner.saveTransforms()

    def help_save_transforms(self) :
        return (("save_transforms","save_transforms",
                unicode_to_gtk(_("save all transforms to NARVAL_HOME"))),0)

    # start plan
    def do_start_plan(self,name) :
        self._hactioner.startPlan(name)

    def help_start_plan(self) :
        return (("start_plan","start_plan [name]",unicode_to_gtk(_("start plan with given name"))),1)

    # debug step_one_step
    def do_debug_step_one_step(self):
        self._hactioner.debugStepOneStep()

    def help_debug_step_one_step(self):
        return (("debug_step_one_step","debug_step_one_step",
                unicode_to_gtk(_("interpreter take one step and exec next event"))),0)

    # debug suspend
    def do_debug_suspend(self):
        self._hactioner.debugSuspend()

    def help_debug_suspend(self):
        return (("debug_suspend","debug_suspend",
                 unicode_to_gtk(_("suspend the execution of the interpreter"))),0)

    # debug continue
    def do_debug_continue(self):
        self._hactioner.debugContinue()

    def help_debug_continue(self):
        return (("debug_continue","debug_continue",
                unicode_to_gtk(_("resume the execution of the interpreter"))),0)

    # new recipe
    def do_new_recipe(self,recipe,cookbook):
        self._hactioner.add_new_recipe(recipe,cookbook)

    def help_new_recipe(self):
        return (("new_recipe","new_recipe [name] [cookbook]",
                unicode_to_gtk(_("add a new recipe in memory"))),2)

    # save recipe
    def do_save_recipe(self,name):
        import string
        name = string.split(name,'.')
        MemoryController().save_cookbook(name[0])

    def help_save_recipe(self):
        return (("save_recipe","save_recipe [group.recipe]",
                unicode_to_gtk(_("commit changes made to the recipe"))),1)

    # add action to recipe
    def do_add_action_to_recipe(self,recipe,action,id=None):
        self._hactioner.add_element_to_recipe(recipe,action,'action',id)

    def help_add_action_to_recipe(self):
        return (("add_action_to_recipe",
                "add_action_to_recipe [group.recipe] [module.action] (id)",
                unicode_to_gtk(_("add an action to the recipe"))),3)

    # add transform to recipe
    def do_add_transform_to_recipe(self,recipe,transform,id=None):
        self._hactioner.add_element_to_recipe(recipe,transform,'transform',id)

    def help_add_transform_to_recipe(self):
        return (("add_transform_to_recipe",
                "add_transform_to_recipe [group.recipe] [transform] (id)",
                unicode_to_gtk(_("add a transform to the recipe"))),3)

    # add recipe to recipe
    def do_add_recipe_to_recipe(self,recipe,r,id=None):
        self._hactioner.add_element_to_recipe(recipe,r,"recipe",id)

    def help_add_recipe_to_recipe(self):
        return (("add_recipe_to_recipe",
                "add_recipe_to_recipe [group.recipe] [group.recipe] (id)",
                unicode_to_gtk(_("add a recipe to the recipe as step"))),3)

    # add argument to step
    def do_add_argument_to_step(self,recipe,id ,arg_xml):
        self._hactioner.add_argument_to_step(recipe,id,arg_xml)

    def help_add_argument_to_step(self):
        return (("add_argument_to_step",
                "add_argument_to_step [group.recipe] [step id] [<xml>]",
                unicode_to_gtk(_("add an argument to a step"))),3)
    
    # add connection into recipe
    def do_connect_elements(self,recipe,elt1,elt2):
        self._hactioner.connect_elements(recipe,elt1,elt2)
    
    def help_connect_elements(self):
        return (('connect_elements',
                'connect_elements [group.recipe] [id_elt1] [id_elt2]',
                unicode_to_gtk(_('connect two elements in recipe'))),3)

    # add transition into recipe
    def do_add_transition_to_recipe(self,recipe,id=None):
        self._hactioner.add_element_to_recipe(recipe,None,'transition',id)

    def help_add_transition_to_recipe(self):
        return (("add_transition_to_recipe",
                "add_transition_to_recipe [group.recipe] (id)",
                unicode_to_gtk(_("add a transition into recipe"))),2)

    # remove element into recipe
    def do_remove_element_from_recipe(self,recipe,id):
        self._hactioner.remove_element_to_recipe(recipe,id)

    def help_remove_element_from_recipe(self):
        return (("remove_element_from_recipe",
                "remove_element_from_recipe [group.recipe] [id_elt]",
                unicode_to_gtk(_("remove an element (step/transition) in recipe"))),2)

    # remove connection into recipe
    def do_remove_connection_from_recipe(self,recipe,id_elt1,id_elt2):
        self._hactioner.remove_connection_to_recipe(recipe,elt1_id,elt2_id)

    def help_remove_connection_from_recipe(self):
        return (("remove_connection_from_recipe",
                "remove_connection_from_recipe [group.recipe] [id_elt1] [id_elt2]",
                unicode_to_gtk(_("remove a connection between two elements"))),3)
    
    # add attribute to recipe
    def do_add_attribute_to_recipe(self,recipe,tag,attr):
        self._hactioner.add_attribute_to_recipe(recipe,tag,attr)

    def help_add_attribute_to_recipe(self):
        return (("add_attribute_to_recipe",
                "add_attribute_to_recipe [group.recipe] [tag] [attr]",
                unicode_to_gtk(_("add an attribute to recipe"))),3)

    # add condition to transition
    def do_add_condition_to_transition(self,recipe,id_trans,cond):
        self._hactioner.add_transition_to_recipe(recipe,id_trans,cond)

    def help_add_condition_to_transition(self):
        return (("add_condition_to_transition",
                "add_condition_to_transition [group.recipe] [id_trans] [cond]",
                unicode_to_gtk(_("add a condition to transition"))),3)

    # create a log file which contains messages from Narval

    def do_put_into_logfile(self):
        from EngineController import EngineController
        EngineController().open_logfile()

    def help_put_into_logfile(self):
        return (("put_into_logfile",
                "put_into_logfile",
                unicode_to_gtk(_("put messages from Narval into a log file"))),0)

    def do_close_logfile(self):
        from EngineController import EngineController
        EngineController().close_logfile()

    def help_close_logfile(self):
        return (("close_logfile","close_logfile",unicode_to_gtk(_("close log file"))),0)

#
# CliThread
#

from threading import *

class CliThread(Thread):
    """
    A subclass of Thread to write a new run() function
    """

    def __init__(self,cli):
        Thread.__init__(self)

        self.cli = cli

    def run(self):
        """
        Start shell
        """
        if self.cli.script :
            # batch mode
            for line in self.cli.script :
                self.cli.exec_cmd(line)
        else :
            # interactive mode
            self.cli.try_using_readline()
            try:
                cmd = raw_input(">>> ")
                while cmd != 'quit' :
                    try:
                        self.cli.exec_cmd(cmd)
                    except Exception,e :
                        print e
                    cmd = raw_input(">>> ")
                from HornModel import HornModel
                if HornModel().narval is not None:
                    self.cli.exec_cmd('disconnect')
            except EOFError :
                pass # used ctrl-D
            
#
# HornCli
#

class HornCli:
    """
    HornCli is like a python shell, using singleton 'pattern'
    """

    _DICT_ = {}

    def __init__(self):
        self.__dict__ = self._DICT_

        if not self.__dict__ :
            self.hci = HornCommands(HornActionerCli())
            self.script = None
            self.thread = CliThread(self)

    ### PUBLIC ###

    def set_script(self,script):
        self.script = script

    def try_using_readline(self) :
        try:
            import readline
        except ImportError:
            pass # readline not available
        else :
            readline.parse_and_bind("tab: complete")
            readline.set_completer(Completer(self.get_commands()).complete)

    def exec_cmd(self,cmdline):
        """
        Execute command
        """
        cmd = self._get_tokens(string.strip(cmdline))
        # empty line
        if not cmd :
            pass
        # help
        elif cmd[0] in (unicode_to_gtk(_("help")),"?") and len(cmd) == 1 :
            self.help()
        elif cmd[0] in (unicode_to_gtk(_("help")),"?") and len(cmd) == 2 :
            self.print_help(cmd[1])
        
        # correct command
        elif self.is_command(cmd[0]) :
            try:
                apply(getattr(self.hci,'do_'+cmd[0]),cmd[1:])
            except Exception, e :
                print e

        # bad command
        else :
            print unicode_to_gtk(_("Bad command")),cmd,unicode_to_gtk(_(". Try help."))

    ### PRIVATE ###

    def _get_tokens(self,line) :
        """
        Parse input command line and return tokens

        '#' comments out the rest of the line
        """
        lexer = shlex(StringIO(line),'unknown')
        lexer.wordchars += '/.#-'
        tokens = []
        while 1 :
            t = lexer.read_token()
            if not t or t[0] == "#" : break
            if t[0] == t[-1] and t[0] in lexer.quotes :
                t = t[1:-1]
            tokens.append(t)
        return tokens

    ### commands ###
            
    def get_commands(self) :
        attrs = dir(self.hci.__class__)
        return map(lambda x: x[3:], filter(lambda x: x[:3] == 'do_', attrs))

    def get_helps(self) :
        attrs = dir(self.hci.__class__)
        return map(lambda x: x[5:], filter(lambda x: x[:5] == 'help_', attrs))

    def is_command(self, cmd) :
        return hasattr(self.hci,'do_'+cmd)

    def has_help(self,cmd) :
        return hasattr(self.hci,'help_'+cmd) 

    def has_topic_help(self,topic):
        return topic in self.hci.topics

    def print_help(self,cmd) :
        if self.has_help(cmd) :
            print '\t',apply(getattr(self.hci,'help_'+cmd),())[0]
        elif self.has_topic_help(cmd):
            self.hci.do_help(cmd)
        else :
            print '\t',cmd,unicode_to_gtk(_('is not documented'))

    def help(self) :
        if self.is_command('help') :
            self.hci.do_help()
        else :
            print unicode_to_gtk(_("Horn commands are:"))
            map(self.print_help, self.get_commands())


