/*
===========================================================================
Copyright (C) 2015 the OpenMoHAA team

This file is part of OpenMoHAA source code.

OpenMoHAA source code 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; either version 2 of the License,
or (at your option) any later version.

OpenMoHAA source code 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 OpenMoHAA source code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

// gravpath.cpp: Gravity path - Used for underwater currents and wells.

#include "g_local.h"
#include "entity.h"
#include "gravpath.h"
#include "container.h"
#include "navigate.h"
#include "misc.h"
#include "player.h"
#include "g_phys.h"
#include "debuglines.h"

GravPathManager gravPathManager;

CLASS_DECLARATION(Class, GravPathManager, NULL) {
    {NULL, NULL}
};

GravPathManager::~GravPathManager()
{
    Reset();
}

void GravPathManager::Reset(void)
{
    while (pathList.NumObjects() > 0) {
        delete (GravPath *)pathList.ObjectAt(1);
    }

    pathList.FreeObjectList();
}

void GravPathManager::AddPath(GravPath *p)
{
    pathList.AddObject(p);
}

void GravPathManager::RemovePath(GravPath *p)
{
    pathList.RemoveObject(p);
}

void GravPathManager::DrawGravPaths(void)
{
    int i;
    int num = pathList.NumObjects();

    for (i = 1; i <= num; i++) {
        GravPath *p = (GravPath *)pathList.ObjectAt(i);

        p->DrawPath(1, 0, 0);
    }
}

Vector GravPathManager::CalculateGravityPull(Entity& ent, Vector pos, qboolean *force, float *max_speed)
{
    int           i, num;
    GravPath     *p;
    GravPathNode *node;
    Vector        point;
    Vector        newpoint;
    Vector        dir;
    float         bestdist = 99999;
    float         dist;
    float         speed;
    float         radius;
    Vector        velocity;
    int           bestpath = 0;
    int           entity_contents, grav_contents;

    num = pathList.NumObjects();
    if (!num) {
        return vec_zero;
    }

    entity_contents = gi.pointcontents(ent.origin, 0);

    for (i = 1; i <= num; i++) {
        p = (GravPath *)pathList.ObjectAt(i);

        if (!p) {
            continue;
        }

        // Check to see if path is active
        node = p->GetNode(1);
        if (!node || !node->active) {
            continue;
        }

        // Check to see if the contents are the same
        grav_contents = gi.pointcontents(node->origin, 0);

        // If grav node is in water, make sure ent is too.
        if ((grav_contents & CONTENTS_WATER) && !(entity_contents & CONTENTS_WATER)) {
            continue;
        }

        // Test to see if we are in this path's bounding box
        if ((pos.x < p->maxs.x) && (pos.y < p->maxs.y) && (pos.z < p->maxs.z) && (pos.x > p->mins.x)
            && (pos.y > p->mins.y) && (pos.z > p->mins.z)) {
            point = p->ClosestPointOnPath(pos, ent, &dist, &speed, &radius);

            // If the closest distance on the path is greater than the radius, then
            // do not consider this path.

            if (dist > radius) {
                continue;
            } else if (dist < bestdist) {
                bestpath = i;
                bestdist = dist;
            }
        }
    }

    if (!bestpath) {
        return vec_zero;
    }

    p = (GravPath *)pathList.ObjectAt(bestpath);
    if (!p) {
        return velocity;
    }
    *force   = p->force;
    dist     = p->DistanceAlongPath(pos, &speed);
    newpoint = p->PointAtDistance(pos, dist + speed, ent.isSubclassOf(Player), max_speed);
    dir      = newpoint - pos;
    dir.normalize();
    velocity = dir * speed;

    //velocity *= .75;
    return velocity;
}

/*****************************************************************************/
/*QUAKED info_grav_pathnode (0 0 .5) (-16 -16 0) (16 16 32) HEADNODE FORCE PULL_UPWARDS
 "radius" Radius of the effect of the pull (Default is 256)
 "speed"  Speed of the pull (Use negative for a repulsion) (Default is 100)

  Set HEADNODE to signify the head of the path.
  Set FORCE if you want un-fightable gravity ( i.e. can't go backwards )
  Set PULL_UPWARDS if you want the gravnodes to pull you upwards also
******************************************************************************/

#define PULL_UPWARDS (1 << 2)

Event EV_GravPath_Create
(
    "gravpath_create",
    EV_DEFAULT,
    NULL,
    NULL,
    "Create the grav path.",
    EV_NORMAL
);
Event EV_GravPath_Activate
(
    "activate",
    EV_DEFAULT,
    NULL,
    NULL,
    "Activate the grav path.",
    EV_NORMAL
);
Event EV_GravPath_Deactivate
(
    "deactivate",
    EV_DEFAULT,
    NULL,
    NULL,
    "Deactivate the grav path.",
    EV_NORMAL
);
Event EV_GravPath_SetSpeed
(
    "speed",
    EV_DEFAULT,
    "f",
    "speed",
    "Set the speed of the grav path.",
    EV_NORMAL
);
Event EV_GravPath_SetMaxSpeed
(
    "maxspeed",
    EV_DEFAULT,
    "f",
    "maxspeed",
    "Set the max speed of the grav path.",
    EV_NORMAL
);
Event EV_GravPath_SetRadius
(
    "radius",
    EV_DEFAULT,
    "f",
    "radius",
    "Set the radius of the grav path.",
    EV_NORMAL
);

CLASS_DECLARATION(Entity, GravPathNode, "info_grav_pathnode") {
    {&EV_GravPath_Create,      &GravPathNode::CreatePath },
    {&EV_GravPath_Activate,    &GravPathNode::Activate   },
    {&EV_GravPath_Deactivate,  &GravPathNode::Deactivate },
    {&EV_GravPath_SetSpeed,    &GravPathNode::SetSpeed   },
    {&EV_GravPath_SetMaxSpeed, &GravPathNode::SetMaxSpeed},
    {&EV_GravPath_SetRadius,   &GravPathNode::SetRadius  },
    {NULL,                     NULL                      }
};

GravPathNode::GravPathNode()
{
    if (LoadingSavegame) {
        // all data will be setup by the archive function
        return;
    }

    setMoveType(MOVETYPE_NONE);
    setSolidType(SOLID_NOT);
    hideModel();

    speed     = 100.0f;
    max_speed = 200.0f;
    radius    = 256.0f;
    headnode  = spawnflags & 1;
    active    = true;

    // This is the head of a new path, post an event to create the path
    if (headnode) {
        PostEvent(EV_GravPath_Create, 0);
    }
}

void GravPathNode::SetSpeed(Event *ev)
{
    speed = ev->GetFloat(1);
}

void GravPathNode::SetMaxSpeed(Event *ev)
{
    max_speed = ev->GetFloat(1);
}

void GravPathNode::SetRadius(Event *ev)
{
    radius = ev->GetFloat(1);
}

float GravPathNode::Speed(void)
{
    if (active) {
        return speed;
    } else {
        return 0;
    }
}

float GravPathNode::MaxSpeed(void)
{
    return max_speed;
}

void GravPathNode::Activate(Event *ev)
{
    GravPathNode *node;
    Entity       *ent;
    const char   *target;

    active = true;
    node   = this;
    // Go through the entire path and activate it
    target = node->Target();
    while (target[0]) {
        ent = (Entity *)G_FindTarget(NULL, target);
        if (ent) {
            node = (GravPathNode *)ent;
            assert(node);
            node->active = true;
        } else {
            gi.Error(ERR_DROP, "GravPathNode::CreatePath: target %s not found\n", target);
        }
        target = node->Target();
    }
}

void GravPathNode::Deactivate(Event *ev)
{
    GravPathNode *node;
    Entity       *ent;
    const char   *target;

    active = false;
    node   = this;
    // Go through the entire path and activate it
    target = node->Target();
    while (target[0]) {
        ent = (Entity *)G_FindTarget(NULL, target);
        if (ent) {
            node = (GravPathNode *)ent;
            assert(node);
            node->active = false;
        } else {
            gi.Error(ERR_DROP, "GravPathNode::CreatePath: target %s not found\n", target);
        }
        target = node->Target();
    }
}

void GravPathNode::CreatePath(Event *ev)
{
    const char   *target;
    GravPath     *path = new GravPath;
    GravPathNode *node;
    Entity       *ent;

    ClearBounds(path->mins, path->maxs);

    // This node is the head of a path, create a new path in the path manager.
    // and add it in, then add all of it's children in the path.
    node = this;
    path->AddNode(node);
    path->force = spawnflags & 2;

    // Make the path from the targetlist.
    target = node->Target();
    while (target[0]) {
        ent = (Entity *)G_FindTarget(NULL, target);
        if (ent) {
            node = (GravPathNode *)ent;
            assert(node);
            path->AddNode(node);
        } else {
            gi.Error(ERR_DROP, "GravPathNode::CreatePath: target %s not found\n", target);
        }
        target = node->Target();
    }

    // Set the origin.
    path->origin = path->mins + path->maxs;
    path->origin *= 0.5f;
}

CLASS_DECLARATION(Listener, GravPath, NULL) {
    {NULL, NULL}
};

GravPath::GravPath()
{
    pathlength = 0;
    from       = NULL;
    to         = NULL;
    nextnode   = 1;

    if (!LoadingSavegame) {
        gravPathManager.AddPath(this);
    }
}

GravPath::~GravPath()
{
    pathlength = 0;
    from       = NULL;
    to         = NULL;
    nextnode   = 1;
    gravPathManager.RemovePath(this);
}

void GravPath::Clear(void)
{
    nextnode   = 1;
    pathlength = 0;
    from       = NULL;
    to         = NULL;
    pathlist.FreeObjectList();
}

void GravPath::Reset(void)
{
    nextnode = 1;
}

GravPathNode *GravPath::Start(void)
{
    return from;
}

GravPathNode *GravPath::End(void)
{
    return to;
}

void GravPath::AddNode(GravPathNode *node)
{
    int    num;
    Vector r, addp;

    if (!from) {
        from = node;
    }

    to = node;
    pathlist.AddObject(GravPathNodePtr(node));

    num = NumNodes();
    if (num > 1) {
        pathlength += (node->origin - GetNode(num)->origin).length();
    }

    r.setXYZ(node->Radius(), node->Radius(), node->Radius());
    addp = node->origin + r;
    AddPointToBounds(addp, mins, maxs);
    addp = node->origin - r;
    AddPointToBounds(addp, mins, maxs);
}

GravPathNode *GravPath::GetNode(int num)
{
    return pathlist.ObjectAt(num);
}

GravPathNode *GravPath::NextNode(void)
{
    if (nextnode <= NumNodes()) {
        return pathlist.ObjectAt(nextnode++);
    }
    return NULL;
}

Vector GravPath::ClosestPointOnPath(Vector pos, Entity& ent, float *ret_dist, float *speed, float *radius)
{
    GravPathNode *s;
    GravPathNode *e;
    int           num;
    int           i;
    float         bestdist;
    Vector        bestpoint;
    float         dist;
    float         segmentlength;
    Vector        delta;
    Vector        p1;
    Vector        p2;
    Vector        p3;
    float         t;
    trace_t       trace;

    num = NumNodes();
    s   = GetNode(1);
    trace =
        G_Trace(pos, ent.mins, ent.maxs, s->origin, &ent, MASK_PLAYERSOLID, false, "GravPath::ClosestPointOnPath 1");
    bestpoint = s->origin;
    delta     = bestpoint - pos;
    bestdist  = delta.length();
    *speed    = s->Speed();
    *radius   = s->Radius();

    for (i = 2; i <= num; i++) {
        e = GetNode(i);

        // check if we're closest to the endpoint
        delta = e->origin - pos;
        dist  = delta.length();

        if (dist < bestdist) {
            trace = G_Trace(
                pos, ent.mins, ent.maxs, e->origin, &ent, MASK_PLAYERSOLID, false, "GravPath::ClosestPointOnPath 2"
            );
            bestdist  = dist;
            bestpoint = e->origin;
            *speed    = e->Speed();
            *radius   = e->Radius();
        }

        // check if we're closest to the segment
        p1            = e->origin - s->origin;
        segmentlength = p1.length();
        p1 *= 1 / segmentlength;
        p2 = pos - s->origin;

        t = p1 * p2;
        if ((t > 0) && (t < segmentlength)) {
            p3 = (p1 * t) + s->origin;

            delta = p3 - pos;
            dist  = delta.length();
            if (dist < bestdist) {
                trace = G_Trace(
                    pos, ent.mins, ent.maxs, p3, &ent, MASK_PLAYERSOLID, false, "GravPath::ClosestPointOnPath 3"
                );
                bestdist  = dist;
                bestpoint = p3;
                *speed    = (e->Speed() * t) + (s->Speed() * (1.0f - t));
                *radius   = (e->Radius() * t) + (s->Radius() * (1.0f - t));
            }
        }

        s = e;
    }
    *ret_dist = bestdist;
    return bestpoint;
}

float GravPath::DistanceAlongPath(Vector pos, float *speed)
{
    GravPathNode *s;
    GravPathNode *e;
    int           num;
    int           i;
    float         bestdist;
    float         dist;
    float         segmentlength;
    Vector        delta;
    Vector        segment;
    Vector        p1;
    Vector        p2;
    Vector        p3;
    float         t;
    float         pathdist;
    float         bestdistalongpath;
    float         oosl;
    pathdist = 0;

    num               = NumNodes();
    s                 = GetNode(1);
    delta             = s->origin - pos;
    bestdist          = delta.length();
    bestdistalongpath = 0;
    *speed            = s->Speed();

    for (i = 2; i <= num; i++) {
        e = GetNode(i);

        segment       = e->origin - s->origin;
        segmentlength = segment.length();

        // check if we're closest to the endpoint
        delta = e->origin - pos;
        dist  = delta.length();

        if (dist < bestdist) {
            bestdist          = dist;
            bestdistalongpath = pathdist + segmentlength;
            *speed            = e->Speed();
        }

        // check if we're closest to the segment
        oosl = (1 / segmentlength);
        p1   = segment * oosl;
        p1.normalize();
        p2 = pos - s->origin;

        t = p1 * p2;
        if ((t > 0) && (t < segmentlength)) {
            p3 = (p1 * t) + s->origin;

            delta = p3 - pos;
            dist  = delta.length();
            if (dist < bestdist) {
                bestdist          = dist;
                bestdistalongpath = pathdist + t;

                t *= oosl;
                *speed = (e->Speed() * t) + (s->Speed() * (1.0f - t));
            }
        }

        s = e;
        pathdist += segmentlength;
    }

    return bestdistalongpath;
}

Vector GravPath::PointAtDistance(Vector pos, float dist, qboolean is_player, float *max_speed)
{
    GravPathNode *s;
    GravPathNode *e;
    int           num;
    int           i;
    Vector        delta;
    Vector        p1;
    float         t;
    float         pathdist;
    float         segmentlength;

    num      = NumNodes();
    s        = GetNode(1);
    pathdist = 0;

    for (i = 2; i <= num; i++) {
        e = GetNode(i);

        delta         = e->origin - s->origin;
        segmentlength = delta.length();

        if ((pathdist + segmentlength) > dist) {
            t = dist - pathdist;

            p1 = delta * (t / segmentlength);
            //         return p1 + s->origin;

            if (e->spawnflags & PULL_UPWARDS && is_player) {
                p1.z = p1.length() / 2;
            }

            *max_speed = e->MaxSpeed();

            return p1 + pos;
        }

        s = e;
        pathdist += segmentlength;
    }

    *max_speed = s->MaxSpeed();

    // cap it off at start or end of path
    return s->origin;
}

void GravPath::DrawPath(float r, float g, float b)
{
    Vector        s;
    Vector        e;
    Vector        offset;
    GravPathNode *node;
    int           num;
    int           i;

    num  = NumNodes();
    node = GetNode(1);
    s    = node->origin;
    G_DebugBBox(s, Vector(8, 8, 8), Vector(-8, -8, -8), 0, 1, 0, 1);
    offset = Vector(r, g, b) * 4 + Vector(0, 0, 0);
    offset = Vector(0, 0, 0);

    for (i = 2; i <= num; i++) {
        node = GetNode(i);
        e    = node->origin;

        G_DebugLine(s + offset, e + offset, r, g, b, 1);
        G_DebugBBox(e, Vector(8, 8, 8), Vector(-8, -8, -8), 0, 1, 0, 1);
        s = e;
    }

    G_DebugBBox(origin, mins - origin, maxs - origin, 1, 0, 0, 1);
}

int GravPath::NumNodes(void)
{
    return pathlist.NumObjects();
}

float GravPath::Length(void)
{
    return pathlength;
}
