#!/usr/bin/python
################################################################################
# Filename: T2CFR.py
# Created: ?? May 2017
# Author : Zeke Steer 
# Purpose: Take a text string from "Speech to Text" and use TextRazor to
#          convert to CFR format.  
################################################################################
# Updates:
# 22 Nov 2018 Derek - removed duplicate "def" get_actions_by_verb.
#                   - added "file exits" check for training verbs file
# 
# 25 Oct 2018 Derek - Fixed bug where client.analyze(s) would cease python program
#                     with no msgs if it was passed unlikely words. The case was 
#                     "Bangalore bengaluru" On TextRazor.com demo page this produces
#                     an error which I could not trap with the documented error name.
# 
# 23 Nov 2017 Derek - Implement "Python Support Library" ROS package  
#                     First shared module is tag_topics.py
#
# 07 Aug 2017 Derek - Make competion data path seperate from the one used to test
#                     the code using runmode = TH eg test harness.
#
# 11 Jul 2017 Derek - Removed debug/dev print statements
#
# 26 Jun 2017 Derek - add results files for mic input as needed by ERL competition
#         
################################################################################
import rospy 
from std_msgs.msg import String 
import operator
import random
import textrazor
import python_support_library.tag_topics   as TT
import python_support_library.text_colours as TC
import os


o_tt = TT.tag_topics()
prt  = TC.tc()

run_mode        = rospy.get_param('SR_TH')
ERLDATAPATHOUT  = rospy.get_param("SR_ERL_DATAPATHOUT")

class Command:

    def __init__(self):
        self.verb = None
        self.action = None
        self.theme = None
        self.conjunction = None
        self.goal = None
        self.source = None
        self.ground = None
        self.beneficiary = None
        self.path = None
    
    def __str__(self):
        s = self.action + "("
        has_args = False
        if not self.beneficiary is None:
            s += "beneficiary:\"%s\"," %self.beneficiary
            has_args = True
        if not self.theme is None:
            s += "theme:\"%s\"," %self.theme
            has_args = True
        if not self.goal is None:
            s += "goal:\"%s\"," %self.goal
            has_args = True
        if not self.source is None:
            s += "source:\"%s\"," %self.source
            has_args = True
        if not self.ground is None:
            s += "ground:\"%s\"," %self.ground
            has_args = True
        if not self.path is None:
            s += "path:\"%s\"," %self.path
            has_args = True
        if has_args:    
            s = s[:len(s) - 1]
        s += ")"
        return s

def get_root(words):
    for word in words:
        if word.parent is None:
            return word
    return None

def get_goal_source_theme(parent, tokens):
    n_tokens = len(tokens)

    if (parent.relation_to_parent == "nn" or parent.relation_to_parent == "amod") and n_tokens > 0:
        tokens.insert(n_tokens - 1, parent.token)
    else:
        # add proposition e.g. to, of, etc.
        if not parent.parent is None and parent.relation_to_parent == "pobj" and (n_tokens == 0 or parent.parent.token != tokens[n_tokens - 1]):
            #if parent.parent.parent is None or parent.parent.parent.relation_to_parent != "ccomp":
            if parent.parent.parent is None or parent.parent.parent.relation_to_parent != "ccomp" or parent.parent.token == "for":
                tokens.append(parent.parent.token)

        # add determiner e.g. the, my, etc.
        for child in parent.children:
            if child.relation_to_parent == "det" or child.relation_to_parent == "poss":
                if len(child.children) > 0:
                    for grandchild in child.children:
                        # combine any possessives
                        if grandchild.relation_to_parent == "possessive":
                            tokens.append(child.token + grandchild.token)
                            break
                        tokens.append(child.token)
                        break
                else:
                    tokens.append(child.token)

        tokens.append(parent.token)

    for child in parent.children:
        if child.relation_to_parent == "pobj" or child.relation_to_parent == "prep" or child.relation_to_parent == "nn" or child.relation_to_parent == "amod":
             get_goal_source_theme(child, tokens)

def get_goal_source_theme_excluded(parent, tokens, exclude):
    n_tokens = len(tokens)

    if (parent.relation_to_parent == "nn" or parent.relation_to_parent == "amod") and n_tokens > 0:
        tokens.insert(n_tokens - 1, parent.token)
    else:
        # add proposition e.g. to, of, etc.
        if not parent.parent is None and parent.relation_to_parent == "pobj" and (n_tokens == 0 or parent.parent.token != tokens[n_tokens - 1]):
            #if parent.parent.parent is None or parent.parent.parent.relation_to_parent != "ccomp":
            if parent.parent.parent is None or parent.parent.parent.relation_to_parent != "ccomp" or parent.parent.token == "for":
                tokens.append(parent.parent.token)

        # add determiner e.g. the, my, etc.
        for child in parent.children:
            if child.relation_to_parent == "det" or child.relation_to_parent == "poss":
                if len(child.children) > 0:
                    for grandchild in child.children:
                        # combine any possessives
                        if grandchild.relation_to_parent == "possessive":
                            tokens.append(child.token + grandchild.token)
                            break
                        tokens.append(child.token)
                        break
                else:
                    tokens.append(child.token)

        tokens.append(parent.token)

    for child in parent.children:
        if child.relation_to_parent == "pobj" or child.relation_to_parent == "prep" or child.relation_to_parent == "nn" or child.relation_to_parent == "amod"  or child.relation_to_parent == "poss":
             if not child is exclude:
                 get_goal_source_theme(child, tokens)

def get_arg(parent):
    tokens = [ ]
    get_goal_source_theme(parent, tokens)
    return ' '.join(tokens)

def get_arg_excluded(parent, exclude):
    tokens = [ ]
    get_goal_source_theme_excluded(parent, tokens, exclude)
    return ' '.join(tokens)

# should only generate bringing goal if this method returns false, otherwise should append to theme
def get_prep(parent):
    if parent.relation_to_parent == "prep":
        return parent

    for child in parent.children:
        prep = get_prep(child)
        if not prep is None:
            return prep
    
    return None

def get_child_prep(parent):
    for child in parent.children:
        prep = get_prep(child)
        if not prep is None:
            return prep

    return None

def set_cmd(child, cmd):
    if child.relation_to_parent == "dobj" or child.relation_to_parent == "pobj": 
        if cmd.action == "MOTION":
            cmd.goal = get_arg(child)  
        elif cmd.action == "BRINGING":
            cmd.theme = get_arg(child)
        elif cmd.action == "SEARCHING":
            cmd.theme = get_arg(child)
        elif cmd.action == "PLACING":
            cmd.theme = get_arg(child)
        elif cmd.action == "TAKING":
            cmd.theme = get_arg(child)
    elif child.relation_to_parent == "cc":
        cmd.conjunction = child.token
    elif child.relation_to_parent == "prep": 
        if len(child.children) > 0:
            if cmd.action == "MOTION":
                if child.token == "in" or child.token == "into" or child.token == "to" or child.token == "near":
                    if cmd.goal is None: 
                        cmd.goal = get_arg(child.children[0])
                    else:
                        cmd.goal = cmd.goal + " " + get_arg(child.children[0])
                else:
                    if child.token == "from":
                        cmd.path = child.token + " " + get_arg(child.children[0])
                    else:
                        cmd.path = get_arg(child.children[0])
            elif cmd.action == "BRINGING":
                if child.token == "from":
                    cmd.source = get_arg(child.children[0])
                else:
                    child_prep = get_child_prep(child)
                    if child_prep is None:
                        # e.g. "onto"
                        cmd.goal = get_arg(child.children[0])
                    else:
                        cmd.theme = cmd.theme + " " + get_arg_excluded(child.children[0], child_prep)
                        set_cmd(child_prep, cmd)
            elif cmd.action == "SEARCHING":
                if child.token == "for":
                    cmd.theme = get_arg(child.children[0])
                else:
                    cmd.ground = get_arg(child.children[0])
            elif cmd.action == "PLACING":
                cmd.goal = get_arg(child.children[0])
            elif cmd.action == "TAKING":
                if child.token == "with":
                    cmd.theme = cmd.theme + " " + get_arg(child.children[0])
                else:
                    cmd.source = get_arg(child.children[0])
        else:
            if cmd.action == "MOTION":
                if child.token == "around":
                    cmd.path = child.token
    elif child.relation_to_parent == "iobj":
        cmd.beneficiary = child.token
    elif child.relation_to_parent == "acomp":
        if (len(child.children) > 0) and child.token == "next":
            cmd.goal = "next " + get_arg(child.children[0])
    elif child.relation_to_parent == "expl":
        if child.token == "there":
            cmd.goal = child.token
    elif child.relation_to_parent == "advmod":
        if (len(child.children) > 0) and (child.token == "next" or child.token == "away"):
            cmd.goal = child.token + " " + get_arg(child.children[0])
    elif child.relation_to_parent == "nsubj":
        if child.token == "it":
            cmd.theme = child.token

placing = 0

def get_cmds(verb, actions_by_verb, cmds):
    global placing
     
    if verb.token == "go" and verb.parent != None and verb.parent.token == "let":
        action = ''
        placing = 1
    elif placing == 1 and verb.parent != None and verb.parent.token == "go":
        action = "PLACING"
        vb = "let go"
        placing = 0
    elif verb.token in actions_by_verb:
        action = actions_by_verb[verb.token]
        placing = 0
        vb = verb.token
    else:
        action = ''
        placing = 0

    cmd = None

    if action != '':
        cmd = Command()
        cmd.action = action
        cmd.verb = vb
        cmds.append(cmd)
        
    for child in verb.children:
        if cmd != None and child.token == "close":
            cmd.goal = get_arg(child)
        elif cmd != None and child.token == "please" and (len(child.children) == 1):
            set_cmd(child.children[0], cmd)
        elif placing or child.token in actions_by_verb.keys():
            get_cmds(child, actions_by_verb, cmds)       
        elif cmd != None:
            set_cmd(child, cmd)
                    
def get_actions_by_verb():
    actions_by_verb = { }
    VERBSFILE=rospy.get_param('SR_VERBSFILE')

    if os.path.isfile(VERBSFILE):
        with open(VERBSFILE, "r") as file:
            for line in file.readlines():
                tokens = line.strip().split(",") 
                actions_by_verb[tokens[0]] = tokens[1]
    else:
        prt.error("\nError in "+cname+": Verbs file not found\n"+"\n"+VERBSFILE+"\n")
        #rospy.signal_shutdown("########## Forced Shutdown #########")
        prt.info("\n##### KILLing ALL Nodes but .....\n#     please use CTRL-C to complete the node shutdown process\n")
        os.system("rosnode kill -a")
        

    return actions_by_verb

def get_cfr(s):
    try:
        response = client.analyze(s)
    except:
        #except TextRazorAnalysisException, rc: # didn't work - inknown global name???
        prt.error("##### Error in TextRazor: client.analyze(s) Failed!")
        prt.error("##### text used was, s= "+s)
        response = None
        
    if response != None:
        actions_by_verb = get_actions_by_verb()
        root = get_root(response.words())
        cmds = [ ]
        get_cmds(root, actions_by_verb, cmds)

        indexes_by_cmd = { }
        for cmd in cmds:
            index = 0
            while (index != -1):
                index = s.find(cmd.verb, index)
                if index in indexes_by_cmd.values():
                    index = index + 1
                else:
                    indexes_by_cmd[cmd] = index
                    break
        sorted_cmds = sorted(indexes_by_cmd.items(), key=operator.itemgetter(1))

        cmd_strs = [ ]
        for cmd in sorted_cmds:
            cmd_strs.append(str(cmd[0]))

        cfr = "#".join(cmd_strs)
        
    else:
        cfr = "ERROR IN TEXTRAZOR()" #  "()"   used to trigger "NO INTERPRETATION"
        
    return cfr


# def kill_nodes():    

#     nodes = os.popen("rosnode list").readlines()

#     for i in range(len(nodes)):
#         nodes[i] = nodes[i].replace("\n","")
#         prt.debug("i: "+str(i)+"  "+nodes[i])

#     os.system("rosnode kill -a")


pub_topic = "CFR_Out"
sub_topic = "/hearts/stt"

# zeke's key:
#textrazor.api_key = "9a7bb531e0ea81b43e8fba17a517152d4d016847ab88a63a54c6cb36"
# alex's key:
textrazor.api_key = "211ef8b5891adff67b329277425597a000d96bd443eeca0fe977819e"
client = textrazor.TextRazor(extractors=["entities", "dependency-trees", "phrases", "words"])

pub = rospy.Publisher(pub_topic, String, queue_size=10) 

rospy.init_node('cfr_node', anonymous=True)

def writeresults(WAV,S2T,CFR):
    S2T = S2T.lower()
    resultsfile = ERLDATAPATHOUT+"results.txt"
    # ERL only want raw "file name" on USB stick
    wavfile = os.path.basename(WAV)
    fh = open (resultsfile,'a')
    str = wavfile+'|'+S2T.strip()+'|'+CFR+'\n'
    fh.write(str)
    fh.close() 

def callback(s):
 
    rospy.loginfo(rospy.get_name()+": Received text message: %s"%s.data)
    #TOPIC is in format <Test from speech string> ~ <wav file name> 
    str1, str2 = s.data.split('~')

    if(run_mode == 'TH'): 
        # strip off #index# prefix used by TH mode 
        index, str1 = o_tt.get_key(str1)

    prt.info("*** T2CFR.py - calling    msg.data = get_cfr(str1)\n*** to derive CFR syntax from interpreted Text")
    msg = String()

    msg.data = get_cfr(str1)
  
 # check the CFR command has arguments ie NOT Motion()
 #            OR not zero length
    if "()" not in msg.data:        
       if len(msg.data) > 0 : 
           pass
       else:
           msg.data = 'NO_INTERPRETATION'
    else:
        msg.data = 'NO_INTERPRETATION'

    if(run_mode == 'TH'): 
        # add back #index# prefix used by TH mode 
        rtndata = o_tt.add_key(index,msg.data)
    else: 
        rtndata = msg.data
 
    pub.publish(rtndata)
    rospy.loginfo(rospy.get_name()+": Published CFR message: %s"%msg.data+'\nmaybe changed to: '+rtndata)
    
    # ERL formatted results file
    writeresults(str2,str1,msg.data)  
    
cname = "t2cfr.py"
rospy.Subscriber(sub_topic, String, callback) 
rospy.spin()
