# Copyright (c) 2004 DoCoMo Euro-Labs GmbH (Munich, Germany).
# Copyright (c) 2000-2004 LOGILAB S.A. (Paris, FRANCE).
#
# http://www.docomolab-euro.com/ -- mailto:tarlano@docomolab-euro.com
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
"""some demos and no more used actions
"""

__revision__ = '$Id: Chat.py,v 1.11 2004/04/02 10:07:31 syt Exp $'

import re

from narval.public import AL_NS, TYPE_NS
from narval.reader import REGISTRY
from narval.interfaces.base import IOpen, IData, IURL
from narval.elements.core import StartPlanElement
from narval.elements.base import CategoryElement, DataElement, FileElement, \
     EventElement
from narval.actions.Learning import get_classifier, get_log_file, log_sentence, \
     do_log_command, YES, NO, UNSURE
MOD_XML = '''<?xml version="1.0" encoding="ISO-8859-1"?>
<module name="JabberTest" xmlns:al='%s'>''' % AL_NS

# remember sentences / context for the learning_categorize action
LEARNING_SENTENCES = {}
# remember (sentences, category) / context for the controled_categorize action
CONTROLED_SENTENCES = {}


def extract_command(sentence, myid, mtype):
    """try to extract a command from a sentence. If found, return it as a string
    (try to normalize it), else return None

    :type sentence: str
    :param sentence: the user sentence

    :type myid: str
    :param myid: the bot jabber user id

    :rtype: str or None
    :return: the normalized command or None if the sentence was not a command
    """
    if re.match('%s\s*:' % myid, sentence):
        # and then check we are understanding it
        command = sentence.split(':', 1)[1].strip().lower()
    elif mtype != 'groupchat':
        command = sentence.strip()
    else:
        return None
    # normalize command
    if command in ('log', 'write'):
        return ('log',)
    if command in ('ignore', 'pass', 'skip'):
        return ('ignore',)
    if command in ('unlearn', 'stupid', 'balot', 'grrr'):
        return ('unlearn',)
    if command.startswith('search') or command.startswith('grep'):
        return ('search', command.split(' ', 1)[1].strip())
    if command.startswith('date') or command.startswith('meeting'):
        return ('date', command.split(' ', 1)[1].strip())
    if mtype != 'groupchat':
        return None
    return (command,)

def categorize(inputs):
    """a sentence categorization action used after learning

    this action produce only optional elements:
    - an answer to inform about the action taken
    - an optional IFile element with the sentence if it should be logged
    """
    o = {}
    ctrl = inputs['control']
    msg = inputs['msg']
    text = msg.get_body()
    # if the user asked to log, we log
    if get_classifier(inputs['datafile']).categorize(text) == YES:
        o['data'] = log_sentence(text, msg.get_from(), msg.get_from_user(), msg.get_type(),
                                 inputs['discussion-log-base'])
        if ctrl and ctrl.verbose:
            o['answer'] = msg.build_reply('%s:sentence logged'
                                          % msg.get_from_user())
    return o
    
MOD_XML = MOD_XML + """
<al:action name='categorize' func='categorize'>
  <al:description lang='en'>Process a chat sentence</al:description>

  <al:input id='msg' use='yes'>
    <al:match>IIMessage(elmt) and elmt.type == 'incoming'</al:match>
  </al:input>
  <al:input id='control' optional='yes'>
    <al:match>isinstance(elmt, BotConfigurationElement)</al:match>
  </al:input>
  <al:input id='datafile' optional='yes'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:datafile'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>
  <al:input id='discussion-log-base' optional='yes'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:discussion-log-base'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>
  
  <al:output id='answer' optional='yes'>
    <al:match>IIMessage(elmt) and elmt.type == 'outgoing'</al:match>
  </al:output>
  <al:output id='data' optional='yes'>
    <al:match>IData(elmt)</al:match>
  </al:output>

</al:action>"""

def learning_categorize(inputs):
    """a categorize action used for learning

    the result for a sentence is delayed until the next sentence, since the
    categorisation is done according to the user's feedback :
    1) check if the sentence is a user instruction (ie ask to log)
    2.1) if it is, output the latest remembered sentence with a "log" category
    2.2) if it is not, output the latest remembered sentence with a "no log" category
    3) remember the sentence

    this action produce only optional elements:
    - an answer to inform about the action taken
    - the latest sentence and its category if any
    - an optional IFile element with the latest sentence if it should be logged    
    """
    o = {}
    ctrl = inputs['control']
    myuser = ctrl and ctrl.myuser
    verbose = ctrl and ctrl.verbose
    msg = inputs['msg']
    text = msg.get_body()
    from_user = msg.get_from_user()
    latest = LEARNING_SENTENCES.get(msg.context)
    if latest is None:
        LEARNING_SENTENCES[msg.context] = text
        if verbose:
            o['answer'] = msg.build_reply('remembering for the next time')            
        return o
    category = None
    command = extract_command(text, msg.get_to(), msg.get_type())
    if command is not None:
        # this is an order, check it comes from our user
        if myuser and from_user != myuser:
            return {'answer': msg.build_reply(
                '%s: only %s can give me some order' % (from_user, myuser))}
        # and then check we are understanding it
        if command != 'log':
            return {'answer': msg.build_reply('i don\'t understand this order')}
        category = "log"
        LEARNING_SENTENCES[msg.context] = None
    else:
        # this is not a command, remember it for the next time
        LEARNING_SENTENCES[msg.context] = text
    # now we should at least output the categorization element
    o['category'] = cat_elmt = CategoryElement(name=category)
    if category is None:
        o['data'] = tolog = DataElement
        tolog.data = '<%s> %s\n' % (from_user, text)
        if verbose:
            o['answer'] = msg.build_reply('ignored and learnt %r' % latest)
    else:
        # if the user asked to log, we log
        o['data'] = log_sentence(latest, msg.get_from(), from_user, msg.get_type(),
                                 inputs['discussion-log-base'])
        if verbose:
            o['answer'] = msg.build_reply('logged and learnt %r' % latest)
    return o

MOD_XML = MOD_XML + """
<al:action name='learning-categorize' func='learning_categorize'>
  <al:description lang='en'>Process a chat sentence in the learning process</al:description>

  <al:input id='msg' use='yes'>
    <al:match>IIMessage(elmt) and elmt.type == 'incoming'</al:match>
  </al:input>
  <al:input id='discussion-log-base' optional='yes'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:discussion-log-base'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>
  <al:input id='control' optional='yes'>
    <al:match>isinstance(elmt, BotConfigurationElement)</al:match>
  </al:input>

  <al:output id='answer' optional='yes'>
    <al:match>IIMessage(elmt) and elmt.type == 'outgoing'</al:match>
  </al:output>
  <al:output id='category' optional='yes'>
    <al:match>ICategory(elmt)</al:match>
  </al:output>
  <al:output id='data' optional='yes'>
    <al:match>IData(elmt)</al:match>
  </al:output>

</al:action>"""


def controled_categorize(inputs):
    """a sentence categorization action used after learning, but with user
    interaction:
    - the user can ask to log /unlearn a phrase explicitly
    - the user set the confidence thresholds, and the bot will ask for user
      feedback when its confidence level is betwen the two specified treshold

    Each categorized sentence is learned to improve the knowledge base.
    
    this action produce only optional elements:
    - an answer to inform about the action taken
    - an optional IFile element with the sentence if it should be logged
    """
    o = {}
    ctrl = inputs['control']
    myuser = ctrl.myuser
    msg = inputs['msg']
    text = msg.get_body()
    from_user = msg.get_from_user()
    from_id = msg.get_from()
    latest_sentence, latest_category = CONTROLED_SENTENCES.get(msg.context,
                                                               (None, None))
    classifier = get_classifier(inputs['datafile'])
    category, answer = None, None
    command = extract_command(text, msg.get_to(), msg.get_type())
    if command is not None:
        # this is an order, related to the latest sentence processed
        # check it comes from our user
        if myuser and from_user != myuser:
            return {'answer': msg.build_reply(
                '%s: only %s can give me some order' % (from_user, myuser))}
        # special case of the search command (returned as a tuple)
        if command[0] == 'search':
            # trick: create a Ifile with no data, so it shouldn't be take
            #        as something to log in latter actions
            # FIXME: this rely on the log action's prototype...
            logfile = get_log_file(from_id, msg.get_type(),
                                   inputs['discussion-log-base'])
            data = DataElement()
            data.data = command[1]
            data.setattr((TYPE_NS, 'name'), 'uri:memory:search-pattern')
            return {'search': data, 'data': logfile}
        if command[0] == 'date':
            try:
                o['event'] = event = EventElement()
                event.from_time, other = event.extract_time_from_string(command[1])
                event.subject = ' '.join(other.split())
            except Exception, ex:
                import sys
                log_traceback(LOG_ERR, sys.exc_info())
                return {'answer': msg.build_reply(
                    '%s: error while parsing event: %r' % (from_user, ex))}
            command = ('log',)
            latest_sentence, latest_category = text, None
        elif latest_sentence is None:
            return {'answer': msg.build_reply(
                '%s: nothing to process !' % from_user)}
            
        try:
            category, answer = do_log_command(command[0], classifier,
                                              latest_sentence, latest_category,
                                              ctrl.verbose)
            text = latest_sentence
            CONTROLED_SENTENCES[msg.context] = (None, None)
        except Exception, ex:
            category, answer = None, str(ex)
    else:
        category = classifier.fuzzy_categorize(text)
        if category >= ctrl.min_treshold and category <= ctrl.max_treshold:
            category = UNSURE
            answer = "don't know what to do (%.2f)... please tell me" % category
        else:
            if category < ctrl.min_treshold:
                category = NO
            else: # category > ctrl.max_treshold
                category = YES
            #classifier.learn(text, category)
            if ctrl.verbose:
                answer = category and 'logged' or 'ignored'
        # this is not a command, remember it for the next time
        CONTROLED_SENTENCES[msg.context] = (text, category)
        
    if answer is not None:
        o['answer'] = msg.build_reply(answer)
    if category == YES:
        # if the user asked to log, we log
        o['data'] = log_sentence(text, from_id, from_user, msg.get_type(),
                                 inputs['discussion-log-base'])
    return o
    
MOD_XML = MOD_XML + """
<al:action name='controled-categorize' func='controled_categorize'>
  <al:description lang='en'>Process a chat sentence</al:description>

  <al:input id='msg' use='yes'>
    <al:match>IIMessage(elmt) and elmt.type == 'incoming'</al:match>
  </al:input>
  <al:input id='control'>
    <al:match>isinstance(elmt, BotConfigurationElement)</al:match>
  </al:input>
  <al:input id='datafile' optional='yes'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:datafile'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>
  <al:input id='discussion-log-base'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:discussion-log-base'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>
  
  <al:output id='answer' optional='yes'>
    <al:match>IIMessage(elmt) and elmt.type == 'outgoing'</al:match>
  </al:output>
  <al:output id='data' optional='yes'>
    <al:match>IFile(elmt)</al:match>
  </al:output>
  <al:output id='search' optional='yes'>
    <al:match>IData(elmt) and elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:search-pattern'</al:match>
  </al:output>
  <al:output id='event' optional='yes'>
    <al:match>IEvent(elmt)</al:match>
  </al:output>
</al:action>"""


def learn(inputs):
    """train the classifier according to a sentence and a predetermined category
    'log' -> YES
    whatever else -> NO
    """
    category = (inputs['category'] == 'log') and YES or NO
    get_classifier(inputs['datafile']).learn(IData(inputs['data']).data, category)
    return {}
    
MOD_XML = MOD_XML + """
<al:action name='learn' func='learn'>
  <al:description lang='en'>Process a chat sentence</al:description>

  <al:input id='data' use='yes'>
    <al:match>IData(elmt)</al:match>
  </al:input>
  <al:input id='category' use='yes'>
    <al:match>ICategory(elmt)</al:match>
  </al:input>
  <al:input id='datafile' optional='yes'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:datafile'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>

</al:action>"""


# #############################################################################

CONTEXTS = {}

from_regex = re.compile("\s*from\s+(\w*)")

_predifined_answer = {
    'hello' : 'hello ! How are you ?',
    'hi': 'hi ! Glad to see you !',
    'bye': 'don\'t leave me',
    'stupid': 'I hope I\'ll do better next time...',
    'bonjour' : 'bonjour !',
    'salut': 'salut !',
    }

def demo_f(inputs):
    """get raw_phrase and interprete it, then build an answer to be sent by
    jabber
    """
    log(LOG_DEBUG, 'in HERE')
    msg = inputs['input']
    context = msg.context
    output = {'output': msg}
    request = msg.request
    text = str(request.body)

    if text[:10] == 'start-plan':
        recipe = text.split()[-1]
        answer = "Starting plan %s" % recipe
        output['startplan'] = StartPlanElement(recipe=recipe)
        
    elif text.find('from ') >= 0 :
        name = from_regex.findall(text)[0]
        recipe, data = create_recipe(name, request['from'], request['to'])
        output['recipe'] = REGISTRY.from_string(data)
        output['startplan'] = StartPlanElement(recipe=recipe)
        answer = 'No problem, I\'ll tell you as soon as a mail from %s has arrived' % name

    elif text[:10] == 'let\'s play':
        g = Guesser()
        answer = g.intro()
        CONTEXTS[context] = g
        
    elif text.find('stop')>=0 or text.find('lost')>=0 or text.find('won')>=0:
        try:
            del CONTEXTS[context]
            answer = 'Once again I won :-)'
        except:
            answer = 'But we are not playing !'
    elif text.find('clear')>=0:
        del CONTEXTS[context]
        answer = 'alright, I\'ve cleared the context'
        
    elif CONTEXTS.has_key(context):
        try:
            answer = str(CONTEXTS[context].guess_what(text))
        except Exception, e:
            answer = str(e)
    else:
        answer = _predifined_answer.get(
            text.lower(), 'unfortunately I don\'t know what to answer you here')
    # log(LOG_DEBUG, answer)    
    msg = msg.build_reply(answer)
    return output

MOD_XML = MOD_XML + """
<al:action name='chat-demo' func='demo_f'>
  <al:description lang='en'>Jabber demo</al:description>
  <al:input id='input'>
    <al:match>isinstance(elmt, JabberRequest)</al:match>
    <al:match>elmt.type == 'incoming'</al:match>
  </al:input>
  <al:output id='output'>
    <al:match>isinstance(elmt, JabberRequest) and elmt.type == 'outgoing'</al:match>
  </al:output>
  <al:output id='startplan' optional='yes'>
    <al:match>isinstance(elmt, StartPlanElement)</al:match>
  </al:output>
  <al:output id='recipe' optional='yes'>
    <al:match>isinstance(elmt, RecipeElement)</al:match>
  </al:output>
</al:action>"""


def process_sentence_f(inputs):
    """get a chat sentence and interprete it, then build an answer to
    be sent by jabber or log the input sentence to a file or do nothing
    """
    msg = inputs['msg']
    context = msg.context
    text = msg.get_body()
    url = IURL(inputs['logfile'])
    keywords = IOpen(inputs['keyword']).open()
    myid = msg.get_to()
    answer = None
    output = {}
    if (text == 'stop' or text.startswith(myid) and text.find('stop') > 1) \
           and CONTEXTS.has_key(context):
        del CONTEXTS[context]
        answer = 'alright, I stop logging what you say'
    elif text == 'write' or text.startswith(myid) and text.find('write') > 1:
        CONTEXTS[context] = 1
        answer = 'ok boss'
    elif CONTEXTS.has_key(context) or should_log(text, keywords):
        output['logdata'] = tolog = FileElement
        fromid = msg.get_from()
        tolog.address = '%s.%s' % (url.address, fromid)
        tolog.mode = 'a'
        tolog.data = '%s: %s\n' % (fromid, text)
        tolog.encoding = url.encoding
    if answer is not None:
        output['answer'] = msg.build_reply(answer)
    return output

def should_log(sentence, keywords):
    """return true if the given sentence should be logged"""
    words = sentence.split()
    keywords = [w.strip() for w in list(keywords)]
    for word in words:
        if word in keywords:
            return True
    return False

MOD_XML += """<al:action name='process_sentence' func='process_sentence_f'>
  <al:description lang='en'>Process a chat sentence</al:description>

  <al:input id='msg'>
    <al:match>IIMessage(elmt) and elmt.type == 'incoming'</al:match>
  </al:input>
  <al:input id='logfile'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:logfile'</al:match>
    <al:match>IURL(elmt)</al:match>
  </al:input>
  <al:input id='keyword'>
    <al:match>elmt.getattr((TYPE_NS, 'name')) == 'uri:memory:keywords'</al:match>
    <al:match>IOpen(elmt)</al:match>
  </al:input>

  <al:output id='answer' optional='yes'>
    <al:match>IIMessage(elmt) and elmt.type == 'outgoing'</al:match>
  </al:output>
  <al:output id='logdata' optional='yes'>
    <al:match>implements(elmt, IFile) and elmt.mode == 'a'</al:match>
  </al:output>
</al:action>"""


MOD_XML +=  "</module>"



# waitmail recipe generator ###################################################

def create_recipe(waited_name, from_id, to_id):
    recipe_name = "process_email_from_%s" % waited_name
    recipe = "generated.%s" % recipe_name
    return recipe, """<al:recipe name="%s" group="generated" restart="no" 
 xmlns:al="http://www.logilab.org/namespaces/Narval/1.2"
 xmlns:jabber="jabber:client">
 
  <al:step target="Basic.NOP" type="action" id="begin"/>
  <al:transition id="Process Email.2">
    <al:in idref="begin"/>
    <al:out idref="end"/>
    <al:condition use="yes">
      <al:match>message/email/headers[contains(string(from),"%s")]</al:match>
      <al:match>message/email/@type="incoming"</al:match>
    </al:condition>
  </al:transition>
  <al:step target="Basic.mirror" type="action" id="end">
    <al:input id="input"><al:match>jabber:jabber-request</al:match></al:input>
    <al:output id="output"><al:match>jabber:jabber-request</al:match></al:output>
    <al:arguments>
<jabber:jabber-request type="outgoing" xmlns:jabber="jabber:client">
<message from="%s" to="%s">
  <subject>An event occurs</subject>
  <body>
You have just received a mail from %s
  </body>
</message>
</jabber:jabber-request>
    </al:arguments>
  </al:step>
</al:recipe>"""%(recipe_name, waited_name, from_id, to_id, waited_name)


MIN = 0
MAX = 65535

class Guesser:
    """class to guess a number, keeping latest guess"""
    
    def __init__(self):
        self._min = None
        self._max = None
        self._last = None

    def intro(self):
        """display the game's rules"""
        self.new_value()
        return '''think to a number between %s and %s, I\'ll try to guess it in the fewer questions as possible.
What about %s ?
Tell me if your number is bigger by typing >, smaller by typing < or equal by typing =.
'''% (MIN,MAX, self._last)
    
    def guess_what(self, msg):
        """update state and return an answer"""
        if msg == '<':
            self._max = self._last
        elif msg == '>':
            self._min = self._last
        elif msg == '=' or self._min == self._max:
            return self._last
        else:
            raise Exception('Answer only with >, < or = please')
        self.new_value()
        return self._last
    
    def new_value(self):
        """return a value equals to (current max- current min) / 2"""
        if self._last is None:
            self._last =  (MAX-MIN)/2
        elif self._min is None:
            self._last = MIN
        elif self._max is None:
            self._last = MAX
        else:
            self._last = (self._max+self._min)/2

if __name__ == '__main__':
    g = Guesser()
    print g.intro()
    import sys
    line = sys.stdin.readline()
    while line:
        line = line.strip()
        print g.guess_what(line)
        line = sys.stdin.readline()
