/*
* npc.cpp by Keith Fulton <keith@paqrat.com>
*
* Copyright (C) 2003 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 <csutil/csstring.h>
#include <iutil/document.h>
#include <iutil/vfs.h>
#include <iutil/object.h>
#include <csgeom/vector3.h>
#include <propclass/linmove.h>
#include <propclass/mesh.h>
#include <physicallayer/entity.h>
#include <physicallayer/propclas.h>
#include <iengine/mesh.h>
#include <iengine/movable.h>
#include <ivaria/collider.h>
#include <cstool/collider.h>

#include "net/msghandler.h"
#include "net/npcmessages.h"
#include "npc.h"
#include "util/log.h"
#include "npcclient.h"
#include "globals.h"
#include "gem.h"
#include "util/psdatabase.h"
#include "util/location.h"

extern iDataConnection *db;


NPC::NPC(): checked(false) 
{ 
    brain=NULL; 
    id=0; 
    last_update=0; 
    entity=NULL; 
    linmove=NULL; 
    DRcounter=0; 

    ang_vel=vel=999; 
    region=NULL; 
    last_perception=NULL; 
    is_debugging=false; 
    alive=false; 
    owner_id=(uint32_t)-1;
    target_id=(uint32_t)-1;
    tribe=NULL;
    checkedSector=NULL;
}

NPC::~NPC()
{
    if (brain)
    {
        delete brain; 
    }
}


bool NPC::Load(iResultRow& row,BinaryRBTree<NPCType>& npctypes)
{
    name = row["name"];
    id   = row.GetInt("char_id");
    if ( id == 0 )
    {
        Error1("NPC has no id attribute. Error in XML");
        return false;
    }

    type = row["npctype"];
    if ( type.Length() == 0 )
    {
        Error1("NPC has no type attribute. Error in XML");
        return false;
    }

    region_name = row["region"]; // optional

    NPCType key(type),*t;
    t = npctypes.Find(&key);
    if (!t)
    {
        Error2("NPC type '%s' is not found. Error in XML",(const char *)type);
        return false;
    }

    if (row.GetFloat("ang_vel_override"))
        ang_vel = row.GetFloat("ang_vel_override");

    if (row.GetFloat("move_vel_override"))
        vel = row.GetFloat("move_vel_override");

    const char *d = row["console_debug"];
    is_debugging = (d && *d=='Y');

    owner_id = row.GetInt("char_id_owner");

    brain = new NPCType(*t); // deep copy constructor

    return true; // success
}

bool NPC::InsertCopy(int use_char_id)
{
    int r = db->Command("insert into sc_npc_definitions "
                        "(name, char_id, npctype, region, ang_vel_override, move_vel_override, console_debug, char_id_owner) values "
                        "('%s', %d,      '%s',    '%s',     %f,               %f, '%c',%d)",
                        name.GetData(), use_char_id, type.GetData(), region_name.GetData(), ang_vel, vel, is_debugging?'Y':'N', owner_id);

    if (r!=1)
    {
        Error3("Error in InsertCopy: %s->%s",db->GetLastQuery(),db->GetLastError() );
    }
    else
    {
        Error2("Inserted %s",db->GetLastQuery());
    }
    return (r==1);
}

void NPC::SetEntity(iCelEntity* ent)
{
    entity = ent;

    // Initialize active location to a known ok value
    if (ent)
    {
        iSector *sector;
        psGameObject::GetPosition(entity,active_locate_pos,active_locate_angle,sector);

        csRef<iPcLinearMovement> lm = CEL_QUERY_PROPCLASS(ent->GetPropertyClassList(), 
                                      iPcLinearMovement);
        linmove = lm;
    }
    else
        linmove = NULL;
}


void NPC::Advance(csTicks when,EventManager *eventmgr)
{
    if (!entity || !alive)
    {
        last_update = 0;
        return;
    }

    if (last_update)
        brain->Advance(when-last_update,this,eventmgr);

    last_update = when;
}

void NPC::ResumeScript(EventManager *eventmgr,Behavior *which)
{
    if (alive)
        brain->ResumeScript(this,eventmgr,which);
}

void NPC::TriggerEvent(Perception *pcpt,EventManager *eventmgr)
{
    Printf("Got event '%s'.\n",pcpt->GetName() );
    brain->FirePerception(this,eventmgr,pcpt);
}

void NPC::SetLastPerception(Perception *pcpt)
{
    if (last_perception)
        delete last_perception;
    last_perception = pcpt;
}

iCelEntity *NPC::GetMostHated(float range)
{
    iSector *sector=NULL;
    csVector3 pos;
    float yrot;
    psGameObject::GetPosition(entity,pos,yrot,sector);

    return hatelist.GetMostHated(sector,pos,range);
}

void NPC::AddToHateList(iCelEntity *attacker,float delta)
{
    CPrintf(CON_DEBUG, "Adding %1.2f to hatelist score for %s.\n",delta,attacker->GetName() );
    hatelist.AddHate(attacker->GetID(),delta);
    if(is_debugging)
        DumpHateList();
}

void NPC::RemoveFromHateList(PS_ID who)
{
    if (hatelist.Remove(who))
        CPrintf(CON_DEBUG, "Removed %d from hate list.\n",who );
}

bool NPC::WaypointListActivateFirst()
{
    Waypoint * wp = WaypointListGetNext();
    if (wp)
    {
        SetActiveLocate(wp->loc.pos,0); // Angle set to 0, should it be ???
        return true;
    }

    return false;
}

Waypoint* NPC::WaypointListGetNext()
{
    if (waypoint_list.IsEmpty()) return NULL;

    Waypoint * wp;
    wp = waypoint_list.Front();
    if (wp)
    {
        waypoint_list.PopFront();
        return wp;
    }
    return NULL;
}


float NPC::GetEntityHate(iCelEntity *ent)
{
    return hatelist.GetHate( ent->GetID() );
}

LocationType *NPC::GetRegion()
{
    if (region)
        return region;
    else
    {
        region = npcclient->FindRegion(region_name);
        return region;
    }
}

void NPC::DumpBehaviorList(bool force)
{
    if (is_debugging || force)
    {
        CPrintf(CON_CMDOUTPUT, "\nBehaviors for %s (%d)\n----------------------------------------\n",
                name.GetData(),id );

        brain->DumpBehaviorList();
    }
}

void NPC::DumpHateList(bool force)
{
    if (is_debugging || force)
    {
        CPrintf(CON_CMDOUTPUT, "\nHate list for %s (%d)\n----------------------------------------\n",
                name.GetData(),id );

        hatelist.DumpHateList();
    }
}

void NPC::ClearState()
{
    brain->ClearState();
    last_perception = NULL;
    hatelist.Clear();
}

void NPC::GetNearestEntity(uint32_t& target_id,csVector3& dest,csString& name,float range)
{
    csVector3 loc;
    iSector* sector;
    float rot,min_range;
    target_id = (uint32_t)-1;

    psGameObject::GetPosition(entity,loc,rot,sector);

    csRef<iCelEntityList> nearlist = npcclient->GetPlLayer()->FindNearbyEntities(sector,loc,range);
    if (nearlist)
    {
        min_range=range;
        for (size_t i=0; i<nearlist->GetCount(); i++)
        {
            iCelEntity *ent = nearlist->Get(i);
            if(ent == entity)
                continue;
            csVector3 loc2;
            iSector *sector2;
            float rot2;
            psGameObject::GetPosition(ent,loc2,rot2,sector2);

            float dist = psGameObject::Distance(loc, sector,loc2, sector2);
            if (dist < min_range)
            {
                min_range = dist;
                dest = loc2;
                name = ent->GetName();
                target_id = ent->GetID();
            }
        }
    }
}

iCelEntity* NPC::GetNearestVisibleFriend(float range)
{
    csVector3 loc;
    iSector* sector;
    float rot,min_range;
    iCelEntity *friendEnt = NULL;

    psGameObject::GetPosition(entity,loc,rot,sector);

    csRef<iCelEntityList> nearlist = npcclient->GetPlLayer()->FindNearbyEntities(sector,loc,range);
    if (nearlist)
    {
        min_range=range;
        for (size_t i=0; i<nearlist->GetCount(); i++)
        {
            iCelEntity *ent = nearlist->Get(i);
            NPC* npcFriend = npcclient->FindAttachedNPC(ent);

            if (!npcFriend || npcFriend == this)
                continue;

            csVector3 loc2, isect;
            iSector *sector2;
            float rot2;
            psGameObject::GetPosition(ent,loc2,rot2,sector2);

            float dist = (loc2 - loc).Norm();
            if(min_range < dist)
                continue;

            // Is this friend visible?
            csIntersectingTriangle closest_tri;
            iMeshWrapper* sel = 0;
            
            dist = csColliderHelper::TraceBeam (npcclient->GetCollDetSys(), sector,
                loc + csVector3(0, 0.6f, 0), loc2 + csVector3(0, 0.6f, 0), true, closest_tri, isect, &sel);
            // Not visible
            if (dist > 0)
                continue;

            min_range = (loc2 - loc).Norm();
            friendEnt = ent;
        }
    }
    return friendEnt;
}

void NPC::Printf(const char *msg,...)
{
    if (!is_debugging)
        return;
    if (!entity)
        return;

    char str[1024];
    va_list args;

    va_start(args, msg);
    vsprintf(str, msg, args);
    va_end(args);

    CPrintf(CON_CMDOUTPUT, "%s (%d)> %s\n",entity->GetName(),id,str);
}

iCelEntity *NPC::GetTarget()
{
    // If something is targeted, use it.
    if (target_id != -1 && target_id != 0)
    {
        // Check if visible
        gemNPCObject * obj = npcclient->FindEntityID(target_id);
        if (obj && !obj->IsVisible()) return NULL;

        iCelEntity *target = npcclient->FindEntity(target_id);
        return target;
    }
    else  // if not, try the last perception entity
    {
        if (GetLastPerception())
        {
            iCelEntity *target = GetLastPerception()->GetEntity();
            CPrintf(CON_NOTIFY,"GetTarget returning last perception entity: %s\n",target ? target->GetName() : "None specified");
            return target;
        }
        return NULL;
    }
}

void NPC::SetTarget(iCelEntity *ent)
{
    if (ent == NULL)
        target_id = (uint32_t)~0;
    else
        target_id = ent->GetID();
}

iCelEntity *NPC::GetOwner()
{
    if (owner_id)
    {
        gemNPCObject *obj = npcclient->FindEntityID( owner_id );
        if (obj)
        {
            return obj->GetEntity();
        }
        else
        {
            return NULL;
        }
    }        
    else
        return NULL;        
}

csString NPC::GetOwnerName()
{
    if (owner_id)
    {
        gemNPCObject *obj = npcclient->FindEntityID( owner_id );
        if (obj)
        {
            return obj->GetName();
        }
    }        

    return "";        
}

void NPC::SetTribe(psTribe * new_tribe)
{
    tribe = new_tribe;
}

psTribe * NPC::GetTribe()
{
    return tribe;
}


void NPC::CheckPosition()
{
    // We only need to check the position once
    csRef<iPcMesh> pcmesh = CEL_QUERY_PROPCLASS(entity->GetPropertyClassList(), 
    iPcMesh);

    if(checked)
    {
        if(checkedPos == pcmesh->GetMesh()->GetMovable()->GetPosition() && checkedSector == pcmesh->GetMesh()->GetMovable()->GetSectors()->Get(0))
        {
            SetAlive(checkedResult);
            CPrintf(CON_NOTIFY,"Extrapolation skipped, result of: %s\n", checkedResult ? "Alive" : "Dead");
            return;
        }
    }
    if(!alive || linmove->IsPath())
        return;

    // Give the npc a jump start to make sure gravity will be applied.
    csVector3 startVel(0.0f,1.0f,0.0f);
    csVector3 vel;

    linmove->AddVelocity(startVel);
    linmove->SetOnGround(false);
    csVector3 pos(pcmesh->GetMesh()->GetMovable()->GetPosition());
    // See what happens in the next 10000 ticks.
    linmove->ExtrapolatePosition(10);
    linmove->GetVelocity(vel);
    // Bad starting position - npc is falling at high speed, server should automatically kill it
    if(vel.y < -50)
    {
        CPrintf(CON_ERROR,"Got bad starting location %f %f %f, killing %s (%i).\n", pos.x,pos.y,pos.z,name.GetData(),id);
        SetAlive(false);
    }

    if(vel == startVel)
    {
        // Collision detection is not being applied!
        linmove->SetVelocity(csVector3(0.0f, 0.0f, 0.0f));
        psGameObject::SetPosition(entity, pos);
    }
    checked = true;
    checkedPos = pos;
    checkedSector = pcmesh->GetMesh()->GetMovable()->GetSectors()->Get(0);
    checkedResult = alive;
}

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


void HateList::AddHate(int entity_id,float delta)
{
    HateListEntry *h = hatelist.Get(entity_id, 0);
    if (!h)
    {
        h = new HateListEntry;
        h->entity_id   = entity_id;
        h->hate_amount = delta;
        hatelist.Put(entity_id,h);
    }
    else
    {
        h->hate_amount += delta;
    }
}

iCelEntity *HateList::GetMostHated(iSector *sector, csVector3& pos, float range)
{
    iCelEntity *most = NULL;
    float most_hate_amount=0;

    csRef<iCelEntityList> list = npcclient->GetPlLayer()->FindNearbyEntities(sector,pos,range);
    for (size_t i=0; i<list->GetCount(); i++)
    {
        HateListEntry *h = hatelist.Get( list->Get(i)->GetID(),0 );
        if (h)
        {
            // Skipp if not visible
            gemNPCObject * obj = npcclient->FindEntityID(list->Get(i)->GetID());
            if (obj && !obj->IsVisible()) continue;
            
            if (!most || h->hate_amount > most_hate_amount)
            {
                most = list->Get(i);
                most_hate_amount = h->hate_amount;
            }
        }
    }
    return most;
}

bool HateList::Remove(int entity_id)
{
    return hatelist.DeleteAll(entity_id);
}

void HateList::Clear()
{
    hatelist.DeleteAll();
}

float HateList::GetHate(int ent)
{
    HateListEntry *h = hatelist.Get(ent, 0);
    if (h)
        return h->hate_amount;
    else
        return 0;
}

void HateList::DumpHateList()
{
    csHash<HateListEntry*>::GlobalIterator iter = hatelist.GetIterator();

    while (iter.HasNext())
    {
        HateListEntry *h = (HateListEntry *)iter.Next();
        csVector3 pos(9.9f,9.9f,9.9f);
        gemNPCObject* obj = npcclient->FindEntityID(h->entity_id);
        csString sectorName;
        float yrot;

        if (obj)
        {
            iSector* sector;
            psGameObject::GetPosition(obj->GetEntity(),pos,yrot,sector);
            if(sector)
                sectorName = sector->QueryObject()->GetName();

            pos = obj->pcmesh->GetMesh()->GetMovable()->GetPosition();
            CPrintf(CON_CMDOUTPUT, "Entity: %d Hated: %.1f\tPos: %.2f %.2f %.2f Sector: %s\n",
                    h->entity_id,h->hate_amount,pos.x,pos.y,pos.z,sectorName.GetDataSafe());
        }
        else
        {
            // This is an error situation. Should not hate something that isn't online.
            CPrintf(CON_CMDOUTPUT, "Entity: %d Hated: %.1f\n",
                    h->entity_id,h->hate_amount);
        }

    }
    CPrintf(CON_CMDOUTPUT, "\n");
}
