/*
 * PaladinJr.cpp - Author: Andrew Dai
 *
 * Copyright (C) 2002 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 <iengine/movable.h>

#include "engine/celbase.h"
#include <propclass/linmove.h>
#include <propclass/colldet.h>
#include <iutil/object.h>
#include "util/serverconsole.h"
#include "gem.h"
#include "client.h"
#include "clients.h"
#include "globals.h"
#include "psserver.h"
#include "playergroup.h"

#include "paladinjr.h"

/*
 * This is the maximum ms of latency that a client is presumed to have.
 */
#define MAX_ACCUMULATED_LAG 10000

void PaladinJr::Initialize(EntityManager* celbase)
{
    iConfigManager* configmanager = psserver->GetConfig();
    enabled = configmanager->GetBool("Planeshift.Paladin.Enabled");

    maxVelocity.x = configmanager->GetFloat("Planeshift.Paladin.MaxXVelocity", 2.0f);
    maxVelocity.y = configmanager->GetFloat("Planeshift.Paladin.MaxYVelocity", 3.5f);
    maxVelocity.z = configmanager->GetFloat("Planeshift.Paladin.MaxZVelocity", 4.0f);

    // Running forward while strafing
    maxSpeed = sqrt(pow(maxVelocity.z, 2.0f) + pow((maxVelocity.z/2.0f), 2.0f));


    watchTime = configmanager->GetInt("Planeshift.Paladin.WatchTime", 30000);
   
    target = NULL;
    entitymanager = celbase;
}

void PaladinJr::PredictClient(Client* client, psDRMessage& currUpdate)
{
    // Speed check always enabled
    SpeedCheck(client, currUpdate);

    if (!enabled)
        return;

    checkClient = false;

    if (target && (csGetTicks() - started > watchTime))
    {
        checked.Add(target->GetClientNum());
        target = NULL;
        started = csGetTicks();
    }

    if (checked.In(client->GetClientNum()))
    {
        if (!target && csGetTicks() - started > PALADIN_MAX_SWITCH_TIME)
        {
            // We have checked every client online
            started = csGetTicks();
            target = client;
            lastUpdate = currUpdate;
#ifdef PALADIN_DEBUG
            CPrintf(CON_DEBUG, "Now checking client %d", target->GetClientNum());
#endif
            checked.DeleteAll();
        }
        return;
    }

    if (!target)
    {
        started = csGetTicks();
        target = client;
        lastUpdate = currUpdate;
#ifdef PALADIN_DEBUG
        CPrintf(CON_DEBUG, "Now checking client %d", target->GetClientNum());
#endif
        return;
    }
    else if (target != client)
        return;

    float yrot;
    iSector* sector;


    client->GetActor()->SetDRData(lastUpdate);

    origPos = lastUpdate.pos;
    vel = lastUpdate.vel;
    angVel = lastUpdate.ang_vel;

    //if (vel.x == 0 && vel.z == 0)
    //{
    //    // Minimum speed to cope with client-side timing discrepencies
    //    vel.x = vel.z = -1;
    //}

    csRef<iPcCollisionDetection> colldet = CEL_QUERY_PROPCLASS(client->GetActor()->GetEntity()->GetPropertyClassList(), iPcCollisionDetection);

    // Paladin Jr needs CD enabled on the entity.
    colldet->UseCD(true);

    client->GetActor()->pcmove->SetVelocity(vel);

    // TODO: Assuming maximum lag, need to add some kind of lag prediction here.
    // Note this ignores actual DR packet time interval because we cannot rely
    // on it so must assume a maximal value.
    //
    // Perform the extrapolation here:
    client->GetActor()->pcmove->UpdateDRDelta(2000);

    // Find the extrapolated position
    client->GetActor()->pcmove->GetLastPosition(predictedPos,yrot,sector);

#ifdef PALADIN_DEBUG
    CPrintf(CON_DEBUG, "Predicted: pos.x = %f, pos.y = %f, pos.z = %f\n",predictedPos.x,predictedPos.y,predictedPos.z);
#endif

    maxmove = predictedPos-origPos;

    // No longer need CD checking
    colldet->UseCD(false);

    lastUpdate = currUpdate;
    checkClient = true;
    return;
}

void PaladinJr::CheckClient(Client* client)
{
    if (!enabled || !checkClient)
        return;

    csVector3 pos;
    float yrot;
    iSector* sector;
    csVector3 posChange;

    csRef<iPcCollisionDetection> colldet = CEL_QUERY_PROPCLASS(client->GetActor()->GetEntity()->GetPropertyClassList(), iPcCollisionDetection);

    client->GetActor()->GetPosition(pos,yrot,sector);
    posChange = pos-origPos;

//#ifdef PALADIN_DEBUG
//    CPrintf(CON_DEBUG, "Actual: pos.x = %f, pos.y = %f, pos.z = %f\nDifference = %f\n",pos.x,pos.y,pos.z, (predictedPos - pos).Norm());
//        CPrintf(CON_DEBUG, "Actual displacement: x = %f y = %f z = %f\n", posChange.x, posChange.y, posChange.z);
//        CPrintf(CON_DEBUG, "Maximum extrapolated displacement: x = %f y = %f z = %f\n",maxmove.x, maxmove.y, maxmove.z);
//#endif

    // TODO:
    // Height checking disabled for now because jump data is not sent.
    if (fabs(posChange.x) > fabs(maxmove.x) || fabs(posChange.z) > fabs(maxmove.z))
    {
#ifdef PALADIN_DEBUG
        CPrintf(CON_DEBUG, "CD violation registered for client %s.\n", client->GetName());
        CPrintf(CON_DEBUG, "Details:\n");
        CPrintf(CON_DEBUG, "Original position: x = %f y = %f z = %f\n", origPos.x, origPos.y, origPos.z);
        CPrintf(CON_DEBUG, "Actual displacement: x = %f y = %f z = %f\n", posChange.x, posChange.y, posChange.z);
        CPrintf(CON_DEBUG, "Maximum extrapolated displacement: x = %f y = %f z = %f\n",maxmove.x, maxmove.y, maxmove.z);
        CPrintf(CON_DEBUG, "Previous velocity: x = %f y = %f z = %f\n", vel.x, vel.y, vel.z);
#endif

        csString buf;
        buf.Format("%s, %s, %s, %.3f %.3f %.3f, %.3f %.3f %.3f, %.3f %.3f %.3f, %.3f %.3f %.3f, %.3f %.3f %.3f, %s", 
                   client->GetName(), "CD violation", sector->QueryObject()->GetName(),origPos.x, origPos.y, origPos.z, 
                   maxmove.x, maxmove.y, maxmove.z, posChange.x, posChange.y, posChange.z, vel.x, vel.y, vel.z, 
                   angVel.x, angVel.y, angVel.z, PALADIN_VERSION);
        psserver->GetLogCSV()->Write(CSV_PALADIN, buf);
    }
}

void PaladinJr::SpeedCheck(Client* client, psDRMessage& currUpdate)
{
    csVector3 oldpos;
    // Dummy variables
    float yrot;
    iSector* sector;

    client->GetActor()->pcmove->GetLastPosition (oldpos, yrot, sector);

    float dist = sqrt (pow((currUpdate.pos.x - oldpos.x), 2.0f) +
        pow((currUpdate.pos.z - oldpos.z), 2.0f));

    csTicks timedelta = client->GetActor()->pcmove->TimeDiff();

    float max_noncheat_distance=maxSpeed*timedelta/1000;

    if (fabs(currUpdate.vel.x) <= maxVelocity.x && currUpdate.vel.y <= maxVelocity.y && fabs(currUpdate.vel.z) <= maxVelocity.z && dist<max_noncheat_distance
        + maxSpeed*client->accumulatedLag/1000)
    {
        if (dist < max_noncheat_distance)
        {
            // Calculate the "unused movement time" here and add it to the
            // accumulated lag.
            client->accumulatedLag += (csTicks)((max_noncheat_distance-dist)
                * 1000.0f/maxSpeed);
            if (client->accumulatedLag > MAX_ACCUMULATED_LAG)
                client->accumulatedLag = MAX_ACCUMULATED_LAG;
        }
        else
        {   
            // Subtract from the accumulated lag.
            client->accumulatedLag-=(csTicks)((dist-max_noncheat_distance)
                * 1000.0f/maxSpeed);
        }
    }
    else
    {

        // Report cheater
        csVector3 vel;
        csVector3 angVel;
        csString buf;
        csString type;
        csString sectorName(sector->QueryObject()->GetName());

        // Player has probably been warped
        if (sector != currUpdate.sector)
        {
            return;
            //sectorName.Append(" to ");
            //sectorName.Append(currUpdate.sectorName);
            //type = "Possible Speed Violation";
        }
        else if(fabs(currUpdate.vel.x) > maxVelocity.x || currUpdate.vel.y > maxVelocity.y || fabs(currUpdate.vel.z) > maxVelocity.z)
            type = "Speed Violation (Hack confirmed)";
        else
            type = "Speed Violation";

        client->GetActor()->pcmove->GetVelocity(vel);
        client->GetActor()->pcmove->GetAngularVelocity(angVel);
        buf.Format("%s, %s, %s, %.3f %.3f %.3f, %.3f 0 %.3f, %.3f %.3f %.3f, %.3f %.3f %.3f, %.3f %.3f %.3f, %s",
                   client->GetName(), type.GetData(), sectorName.GetData(),oldpos.x, oldpos.y, oldpos.z,
                   max_noncheat_distance, max_noncheat_distance, 
                   currUpdate.pos.x - oldpos.x, currUpdate.pos.y - oldpos.y, currUpdate.pos.z - oldpos.z,
                   vel.x, vel.y, vel.z, angVel.x, angVel.y, angVel.z, PALADIN_VERSION);
        psserver->GetLogCSV()->Write(CSV_PALADIN, buf);

        Debug5(LOG_CHEAT, client->GetClientNum(),"Player %s traversed %1.2fm in %u msec with an accumulated lag allowance of %u ms. Cheat detected!\n",
            client->GetName (),dist,timedelta,client->accumulatedLag);
    }
}

