/*
* psnpcdialog.cpp
*
* Copyright (C) 2001 Atomic Blue (info@planeshift.it, http://www.atomicblue.org)
*
*
* 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 (version 2 of the License)
* 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.
*
*/
#include <psconfig.h>
#include <ctype.h>
//#include "psstdint.h"

//CS includes
#include <csutil/randomgen.h>
#include <csutil/sysfunc.h>
#include <iutil/objreg.h>
#include <iutil/event.h>
#include <iutil/eventq.h>
#include <iutil/evdefs.h>
#include <iutil/virtclk.h>

#include <imesh/sprite3d.h>

#include <csutil/debug.h>
#include <csutil/databuf.h>
#include <csutil/plugmgr.h>
#include <iengine/movable.h>
#include <iengine/engine.h>
#include <cstool/collider.h>
#include <ivaria/collider.h>

//CEL includes
// #include <physicallayer/pl.h>
#include <physicallayer/entity.h>
#include <propclass/mesh.h>
//#include <behaviourlayer/behave.h>


// PS Includes
#include "util/strutil.h"
#include "util/psstring.h"
#include "util/serverconsole.h"

#include "psnpcdialog.h"
#include "util/log.h"
#include "../iserver/idal.h"

#include "dictionary.h"
#include "../psserver.h"
#include "util/psdatabase.h"
#include "../gem.h"
#include "../globals.h"
#include "../playergroup.h"
#include "psraceinfo.h"

//----------------------------------------------------------------------------

void NpcTriggerSentence::AddToSentence(NpcTerm *next_word)
{ 
    assert(next_word!=NULL); 
    terms.Push(next_word); 
    str="";
}


const csString& NpcTriggerSentence::GetString()
{
    if (str.Length())
        return str;

    for (size_t i=0; i<terms.Length(); i++)
    {
        str.Append(terms[i]->term);
        if (i<terms.Length()-1)
            str.Append(' ');
    }
    return str;
}

const char* NpcTriggerSentence::GeneralizeTerm(NPCDialogDict *dict,size_t which, size_t depth)
{
    if (which >= terms.Length())
        return false;
    
    str = "";

    return terms[which]->GetInterleavedHypernym(depth);
}




//----------------------------------------------------------------------------



psNPCDialog::psNPCDialog(gemNPC *npc)
{
    db = NULL;
    randomgen = NULL;
    self = npc;
}

psNPCDialog::~psNPCDialog()
{
}

bool psNPCDialog::Initialize( iDataConnection *db )
{
    this->db = db;

    if (!dict)
    {
        dict = new NPCDialogDict;

        if (!dict->Initialize(db))
        {
            delete dict;
            dict=NULL;
            return false;
        }
    }
    return true;
}


bool psNPCDialog::Initialize(iDataConnection *db,int NPCID)
{
    randomgen = psserver->rng;
    this->db = db;
    // Initialize base dictionary
    if (!dict)
    {
        dict = new NPCDialogDict;

        if (!dict->Initialize(db))
        {
            delete dict;
            dict=NULL;
            return false;
        }
    }
    else
        dict->IncRef();

    return LoadKnowledgeAreas(NPCID);
}

bool psNPCDialog::LoadKnowledgeAreas(int NPCID)
{
    Result result(db->Select("select area,priority"
        "  from npc_knowledge_areas"
        " where player_id=%d",NPCID));

    if (!result.IsValid() )
    {
        Error1("Cannot load knowledge areas into dictionary from database.");
        return false;
    }

    for (unsigned int i=0; i<result.Count(); i++)
    {
        KnowledgeArea *newarea = new KnowledgeArea;

        newarea->area = result[i]["area"];
        newarea->priority = result[i].GetInt("priority");

        knowareas.Insert(newarea,TREE_OWNS_DATA); // TRUE means tree owns data
    }
    return true;
}

bool psNPCDialog::CheckPronouns(psString& text)
{
    int wordnum=1;
    psString word("temp");

    while (word.Length())
    {
        word = GetWordNumber(text,wordnum++);

        if (word == "him" || word=="he")
        {
            if (!antecedent_him.IsEmpty())
                text.ReplaceSubString(word,antecedent_him);
            else
            {
                return false;
            }
        }

        if (word == "her" || word=="she")
        {
            if (!antecedent_her.IsEmpty())
                text.ReplaceSubString(word,antecedent_her);
            else
            {
                return false;
            }
        }

        if (word == "them" || word=="they")
        {
            if (!antecedent_them.IsEmpty())
                text.ReplaceSubString(word,antecedent_them);
            else
            {
                return false;
            }
        }

        if (word == "it")
        {
            if (!antecedent_them.IsEmpty())
                text.ReplaceSubString(word,antecedent_them);
            else
            {
                return false;
            }
        }
    }
    return true;
}

void psNPCDialog::CleanPunctuation(psString& str)
{
    for (unsigned int i=0; i<str.Length(); i++)
    {
        if (ispunct(str.GetAt(i)) && str.GetAt(i)!='\'')
        {
            str.DeleteAt(i);
            i--;
        }
    }

    // we also use this function to remove the NPC's own name from the dialog
    psString wordInName,npc_name;
    wordInName = "bla";
    npc_name = self->GetEntity()->GetName();
    int num = 1;
    while (wordInName.Length())
    {
        wordInName = GetWordNumber(npc_name,num);
        if (!wordInName.Length())
            break;
        int pos = (int)str.FindSubString(wordInName,0,XML_CASE_INSENSITIVE);
        if (pos != -1)
        {
            str.DeleteAt(pos,wordInName.Length());
        }
        num++;
    }
}

void psNPCDialog::FilterKnownTerms(const psString & text, NpcTriggerSentence &trigger)
{
    const size_t MAX_SENTENCE_LENGTH = 4;
    NpcTerm* term;

    WordArray words(text);
    size_t numWordsInPhrase = words.GetCount();
    size_t firstWord=0;
    csString candidate;
    
    if (!dict)   // Pointless to try if no dictionary loaded.
        return;
    
    Debug2(LOG_NPC, 0,"Recognizing phrases in '%s'", text.GetData());
    
    while (firstWord<words.GetCount() && trigger.TermLength()<MAX_SENTENCE_LENGTH)
    {
        candidate = words.GetWords(firstWord, firstWord+numWordsInPhrase);
        term  = dict->FindTermOrSynonym(candidate);
        if (term)
        {
            trigger.AddToSentence(term);
            firstWord += numWordsInPhrase;
            numWordsInPhrase = words.GetCount() - firstWord;
        }
        else if (numWordsInPhrase > 1)
            numWordsInPhrase--;
        else
        {
            firstWord ++;
            numWordsInPhrase = (int)words.GetCount() - firstWord;
        }
    }
    
    Debug2(LOG_NPC, 0,"Phrases recognized: '%s'", trigger.GetString().GetData());
}

void psNPCDialog::AddBadText(const char *text, const char *trigger)
{
    csString escText,escTrigger;
    csString escName;
    csString escSelfName;
    
    db->Escape( escText, text );
    db->Escape( escName, currentClient->GetName() );
    db->Escape( escSelfName, self->GetEntity()->GetName() );
    db->Escape( escTrigger, trigger);
    int ret = db->Command("insert into npc_bad_text "
                          "(badtext,triggertext,player,npc,occurred) "
                          "values ('%s','%s','%s','%s',Now() )",
                          escText.GetData(),
                          escTrigger.GetData(),
                          escName.GetData(),
                          escSelfName.GetData());
    if (ret == QUERY_FAILED)
    {
        Error2("Inserting npc_bad_text failed: %s",db->GetLastError() );
    }
}


NpcResponse *psNPCDialog::FindResponse(csString& trigger,const char *text)
{
    BinaryRBIterator<KnowledgeArea> loop(&knowareas);
    KnowledgeArea *area;
    NpcResponse *resp = NULL;
    csString trigger_error;
	
    if(trigger.GetData() == NULL) return NULL;
	
    trigger.Downcase();

    trigger_error = trigger;
    trigger_error.Append(" error");

    for (area = loop.First(); area; area = ++loop)
    {
      Debug4(LOG_NPC, 0,"NPC checking %s for trigger %s , with lastResponseID %d...",
                (const char *)area->area,(const char *)trigger,currentClient->GetLastResponse());

        resp = dict->FindResponse(self, area->area,trigger,0,currentClient->GetLastResponse(),currentClient);
        if (!resp) // If no response found, try search for error trigger
        {
            resp = dict->FindResponse(self, area->area,trigger_error,0,currentClient->GetLastResponse(),currentClient);
            if (!resp) // If no respons found, try search without last response
            {
                if (currentClient->GetLastResponse() == -1)
                {
                    // No point testing without last response
                    // if last response where no last response.
                    continue;
                }

                resp = dict->FindResponse(self, area->area,trigger,0,-1,currentClient);
                if (!resp) // If no respons found, try search for error trigger without last response
                {
                    resp = dict->FindResponse(self, area->area,trigger_error,0,-1,currentClient);
                    if (resp)
                    {
                        // Force setting of type Error if error trigger found
                        resp->type = NpcResponse::ERROR_RESPONSE;
                    }
                }
            }
            else
            {
                // Force setting of type Error if error trigger found
                resp->type = NpcResponse::ERROR_RESPONSE;
            }
        }
        
        if (resp)
        {
            Debug3(LOG_NPC, 0,"Found response %d: %s",resp->id,resp->GetResponse());

            UpdateAntecedents(resp);
            break;
        }
    }
    return resp;
}

void psNPCDialog::SubstituteKeywords(Client * player, csString& resp) const
{
    psString dollarsign("$"),response(resp);
    int where = response.FindSubString(dollarsign);

    while (where!=-1)
    {
        psString word2,word;
        response.GetWord(where+1,word2,psString::NO_PUNCT);

        word = "$";
        word.Append(word2);  // include $sign in subst.

        if (strcmp(word.GetData(),"$playername")==0)
        {
            if (!response.ReplaceSubString(word,player->GetName()))
            {
                Error4("Failed to replace substring %s in %s with %s",word.GetData(),response.GetData(),player->GetName());
            }
        }
        else if (strcmp(word.GetData(),"$playerrace")==0)
        {            
            if (!response.ReplaceSubString(word, player->GetCharacterData()->raceinfo->name))
            {
                Error4("Failed to replace substring %s in %s with %s",word.GetData(),response.GetData(),player->GetName());
            }
        }
        else if (strcmp(word.GetData(),"$sir")==0)
        {
            const char* sir;
            if ( player->GetCharacterData()->raceinfo->gender == PSCHARACTER_GENDER_FEMALE )
                sir = "Madam";
            else
                sir = "Sir";
                
            if (!response.ReplaceSubString(word,sir))
            {
                Error4("Failed to replace substring %s in %s with %s",word.GetData(),response.GetData(),player->GetName());
            }
        }
        where = response.FindSubString(dollarsign,where+1);
    }
    resp = response;
}

NpcResponse *psNPCDialog::FindOrGeneralizeTrigger(Client *client,NpcTriggerSentence& trigger,
                                                  const csArray<int>& gen_terms)
{
    NpcResponse *resp;
    csStringArray generalized;

    bool hit;

    // Perform breadth-first search on generalisations

    // Copy all terms into stringarray
    for(size_t i=0;i<trigger.TermLength();i++)
        generalized.Push(trigger.Term(i)->term);
    
    // We do at least one search with no generalisations
    size_t depth = 0;

    do
    {
        hit = false;

        for(size_t i=0;i<gen_terms.Length();i++)
        {
            const char* hypernym = trigger.Term(gen_terms[i])->GetInterleavedHypernym(depth);
            if(hypernym)
            {
                generalized.Put(gen_terms[i],hypernym);
                hit = true;
            }

            csString generalized_copy;

            // Merge string
            for(size_t i=0;i<generalized.Length();i++)
            {
                generalized_copy.Append(generalized[i]);
            }
            
            dict->CheckForTriggerGroup(generalized_copy);  // substitute master trigger if this is child trigger in group

            resp = FindResponse( generalized_copy, trigger.GetString());
            if (resp)
            {
                Debug2(LOG_NPC, 0,"Found response to: '%s'", generalized_copy.GetData());

                //         // Removed till we find a better way to manage repeated responses
                //         // At the moment are annoying since you cannot restart the conversation from
                //         // a certain point
                //         int times;
                //         csTicks when;
                //         if (dialogHistory.EverSaid(client->GetPlayerID(), resp->id, when, times))
                //         {
                //             return RepeatedResponse(trigger.GetString(), resp, when, times);
                //         }
                //         else
                //             dialogHistory.AddToHistory(client->GetPlayerID(), resp->id, csGetTicks() );


                return resp; // Found what we are looking for
            }
        }
        depth++;
    } while (hit == true);

    return NULL;

#ifdef USE_OLD_SERACH
    // This code was replaced with the code abou at 7th dec. 2005.
    NpcResponse *resp;

    csString generalized_copy = generalized.GetString();

    dict->CheckForTriggerGroup(generalized_copy);  // substitute master trigger if this is child trigger in group

    resp = FindResponse( generalized_copy, trigger.GetString());
    if (resp != NULL)
    {
        Debug2(LOG_NPC, 0, "Found response to: '%s'", generalized.GetString().GetData());
        
//         // Removed till we find a better way to manage repeated responses
//         // At the moment are annoying since you cannot restart the conversation from
//         // a certain point
//         int times;
//         csTicks when;
//         if (dialogHistory.EverSaid(client->GetPlayerID(), resp->id, when, times))
//         {
//             return RepeatedResponse(trigger.GetString(), resp, when, times);
//         }
//         else
//             dialogHistory.AddToHistory(client->GetPlayerID(), resp->id, csGetTicks() );


        return resp;
    }
    else // Try a further generalizations at lower levels, then this level
    {
        if (level < trigger.TermLength()-1)
        {
            resp = FindOrGeneralizeTrigger(client,trigger,generalized,level+1);
            if (resp)
                return resp;
        }
        // Reset later words back to original so re-search covers all again
        for (int i=(int)level+1; i<(int)trigger.TermLength(); i++)
            generalized.Term(i) = trigger.Term(i);

        // Attempt to move up a level with this term
        bool found_more_general = generalized.GeneralizeTerm(dict,level);
        if (!found_more_general)  // no generalization possible, so give up at this level
            return NULL;
        else
            return FindOrGeneralizeTrigger(client,trigger,generalized,level);
    }
#endif
}

NpcResponse *psNPCDialog::Respond(const char * text,Client *client)
{
    NpcResponse *resp;
    NpcTriggerSentence trigger,generalized;
    psString pstext(text);
    
    currentplayer = client->GetActor();
    currentClient = client;

    // Removes everything except alphanumeric character and spaces
    // Removes the NPC name
    CleanPunctuation(pstext);

    // Replace him/he,her/she,them/they,it with the stored
    // antecedent
    if (!CheckPronouns(pstext))
    {
        Debug2(LOG_NPC, 0,"Failed pronouns check for \"%s\"",pstext.GetDataSafe());
        return ErrorResponse(pstext,"(none)");
    }

    // Replace custom known terms to get standard terms
    // eg. hello is replaced with gretting
    FilterKnownTerms(pstext, trigger);

    if (trigger.TermLength() == 0)
    {
        Debug1(LOG_NPC, 0,"Failed filter known terms check");
        return ErrorResponse(pstext,"(no known words)");
    }

    csString copy;
    copy = trigger.GetString();
    dict->CheckForTriggerGroup(copy);  // substitute master trigger if this is child trigger in group
    resp = FindResponse(copy, trigger.GetString());

    if (resp)
    {
        Debug2(LOG_NPC, 0,"Found response to: '%s'", copy.GetData());

        //         // Removed till we find a better way to manage repeated responses
        //         // At the moment are annoying since you cannot restart the conversation from
        //         // a certain point
        //         int times;
        //         csTicks when;
        //         if (dialogHistory.EverSaid(client->GetPlayerID(), resp->id, when, times))
        //         {
        //             return RepeatedResponse(trigger.GetString(), resp, when, times);
        //         }
        //         else
        //             dialogHistory.AddToHistory(client->GetPlayerID(), resp->id, csGetTicks() );


        Debug3(LOG_NPC, 0,"Setting last response %d: %s",resp->id,resp->GetResponse());
        currentClient->SetLastResponse(resp->id);
        return resp; // Found what we are looking for
    }
    else
    {
        Debug1(LOG_NPC, 0,"No response found");
        return ErrorResponse(pstext,trigger.GetString() );
    }
}

NpcResponse *psNPCDialog::FindXMLResponse(Client *client, csString trigger)
{
    if(!client) return NULL;
    currentplayer = client->GetActor();
    currentClient = client;

    return FindResponse(trigger, trigger.GetDataSafe());
}

NpcResponse *psNPCDialog::RepeatedResponse(const psString& text, NpcResponse *resp, csTicks when, int times)
{
    const char *time_description;
    if (when < 30000)  // 30 seconds
        time_description = "just now";
    else if (when < 300000) // 5 minutes
        time_description = "recently";
    else
        time_description = "already";

    csString key;
    key.Format("repeat %s %d",time_description,times);
    resp = FindResponse(key,text);
    if (!resp)
    {
        key.Format("repeat %s",time_description);
        resp = FindResponse(key,text);
        if (!resp)
        {
            key = "repeat";
            resp = FindResponse(key,text);
        }
    }
    return resp;
}

NpcResponse *psNPCDialog::ErrorResponse(const psString & text, const char *trigger)
{
    AddBadText(text,trigger);
    psString error("error");
    NpcResponse * resp = FindResponse(error,text);
    return resp;
}

void psNPCDialog::UpdateAntecedents(NpcResponse *resp)
{
    // later this will need to be kept on a per player basis
    // but for now, its just one set

    if (resp->her.Length())
        antecedent_her = resp->her;

    if (resp->him.Length())
        antecedent_him = resp->him;

    if (resp->it.Length())
        antecedent_it = resp->it;

    if (resp->them.Length())
        antecedent_them = resp->them;

}

bool psNPCDialog::AddWord(const char *)
{
    return false;
}

bool psNPCDialog::AddSynonym(const char *,const char *)
{
    return false;
}

bool psNPCDialog::AddKnowledgeArea(const char *)
{
    return false;
}

bool psNPCDialog::AddResponse(const char *area,const char *words,const char *response,const char *minfaction)
{
    (void) area;
    (void) words;
    (void) response;
    (void) minfaction;
    return false;
}

bool psNPCDialog::AssignNPCArea(const char *npcname,const char *areaname)
{
    (void) npcname;
    (void) areaname;
    return false;
}


void psNPCDialog::AddNewTrigger( int databaseID )
{
    if ( !dict )
        return;
    else
        dict->AddTrigger( db, databaseID );
}

void psNPCDialog::AddNewResponse ( int databaseID )
{
    if ( !dict )
        return;
    else
        dict->AddResponse( db, databaseID );
}



void DialogHistory::AddToHistory(int playerID, int responseID, csTicks when)
{
    DialogHistoryEntry entry;
    entry.playerID  = playerID;
    entry.responseID= responseID;
    entry.when      = when;

    if (history.Length() < MAX_HISTORY_LEN)
        history.Push(entry);
    else
    {
        history.Put(counter,entry);
        counter++;
        if (counter == MAX_HISTORY_LEN)
            counter=0;
    }
}

bool DialogHistory::EverSaid(int playerID, int responseID, csTicks& howLongAgo, int& times)
{
    times      = 0;
    howLongAgo = 0;

    for (size_t i=0; i<history.Length(); i++)
    {
        if (history[i].playerID   == playerID &&
            history[i].responseID == responseID)
        {
            times++;
            if (history[i].when > howLongAgo)
                howLongAgo = history[i].when;
        }
    }
    if (howLongAgo)
        howLongAgo = csGetTicks() - howLongAgo;  // need the delta here

    return (times > 0);
}


