/*
===========================================================================
Copyright (C) 2023 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
===========================================================================
*/

// g_mmove.cpp : AI/Path movement code.
//

#include "g_local.h"
#include "entity.h"
#include "game.h"

typedef struct {
    qboolean validGroundTrace;
    trace_t  groundTrace;
    float    previous_origin[3];
    float    previous_velocity[3];
} mml_t;

mmove_t *mm;
mml_t    mml;

void MM_ClipVelocity(float *in, float *normal, float *out, float overbounce)
{
    float backoff;
    float dir_z;
    float normal2[3];

    if (normal[2] >= MIN_WALK_NORMAL) {
        if (in[0] == 0.0f && in[1] == 0.0f) {
            VectorClear(out);
            return;
        }

        normal2[0] = in[0] * DotProduct2D(in, normal);
        normal2[1] = in[1] * DotProduct2D(in, normal);
        normal2[2] = normal[2] * DotProduct2D(in, in);

        VectorNormalize(normal2);

        dir_z = -normal2[2];

        out[0] = in[0];
        out[1] = in[1];
        out[2] = DotProduct2D(in, normal2) / dir_z;
    } else {
        backoff = DotProduct(in, normal);

        if (backoff < 0) {
            backoff *= overbounce;
        } else {
            backoff /= overbounce;
        }

        out[0] = in[0] - normal[0] * backoff;
        out[1] = in[1] - normal[1] * backoff;
        out[2] = in[2] - normal[2] * backoff;
    }
}

qboolean MM_AddTouchEnt(int entityNum)
{
    int      i;
    qboolean blockEnt;
    Entity  *ent;

    if (entityNum == ENTITYNUM_NONE || entityNum == ENTITYNUM_WORLD) {
        return qtrue;
    }

    ent = G_GetEntity(entityNum);

    blockEnt = ent->BlocksAIMovement();

    if (!blockEnt) {
        if (ent->IsSubclassOfPlayer()) {
            mm->hit_temp_obstacle |= 1;
        } else if (ent->IsSubclassOfDoor()) {
            mm->hit_temp_obstacle |= 2;
        }
    }

    if (mm->numtouch == MAXTOUCH) {
        return blockEnt;
    }

    // see if it is already added
    for (i = 0; i < mm->numtouch; i++) {
        if (mm->touchents[i] == entityNum) {
            return blockEnt;
        }
    }

    // add it
    mm->touchents[mm->numtouch] = entityNum;
    mm->numtouch++;

    return blockEnt;
}

qboolean MM_SlideMove(qboolean gravity)
{
    int      bumpcount;
    vec3_t   dir;
    float    d;
    int      numplanes;
    vec3_t   planes[5];
    vec3_t   clipVelocity;
    int      i;
    int      j;
    int      k;
    trace_t  trace;
    vec3_t   end;
    float    time_left;
    qboolean bBlockEnt;

    if (gravity) {
        mm->velocity[2] = mm->velocity[2] - mm->frametime * sv_gravity->integer;
        if (mm->groundPlane) {
            MM_ClipVelocity(mm->velocity, mm->groundPlaneNormal, mm->velocity, OVERCLIP);
        }
    }

    time_left = mm->frametime;

    if (mm->groundPlane) {
        numplanes = 1;
        VectorCopy(mm->groundPlaneNormal, planes[0]);
    } else {
        numplanes = 0;
    }

    // never turn against original velocity
    VectorNormalize2(mm->velocity, planes[numplanes]);
    numplanes++;

    for (bumpcount = 0; bumpcount < 4; bumpcount++) {
        // calculate position we are trying to move to
        VectorMA(mm->origin, time_left, mm->velocity, end);

        // see if we can make it there
        gi.trace(&trace, mm->origin, mm->mins, mm->maxs, end, mm->entityNum, mm->tracemask, qtrue, qfalse);

        if (trace.allsolid) {
            break;
        }

        if (trace.fraction > 0) {
            // actually covered some distance
            VectorCopy(trace.endpos, mm->origin);
        }

        if (trace.fraction == 1) {
            return bumpcount != 0;
        }

        // save entity for contact
        bBlockEnt = MM_AddTouchEnt(trace.entityNum);

        if (trace.plane.normal[2] < MIN_WALK_NORMAL) {
            if (trace.plane.normal[2] > -0.999f && bBlockEnt && mm->groundPlane) {
                if (!mm->hit_obstacle) {
                    mm->hit_obstacle = true;
                    VectorCopy(mm->origin, mm->hit_origin);
                }

                VectorAdd(mm->obstacle_normal, trace.plane.normal, mm->obstacle_normal);
            }
        } else {
            memcpy(&mml.groundTrace, &trace, sizeof(mml.groundTrace));
            mml.validGroundTrace = true;
        }

        time_left -= time_left * trace.fraction;

        if (numplanes >= MAX_CLIP_PLANES) {
            VectorClear(mm->velocity);
            return qtrue;
        }

        //
        // if this is the same plane we hit before, nudge velocity
        // out along it, which fixes some epsilon issues with
        // non-axial planes
        //
        for (i = 0; i < numplanes; i++) {
            if (DotProduct(trace.plane.normal, planes[i]) > 0.99) {
                VectorAdd(trace.plane.normal, mm->velocity, mm->velocity);
                break;
            }
        }

        if (i >= numplanes) {
            //
            // modify velocity so it parallels all of the clip planes
            //
            VectorCopy(trace.plane.normal, planes[numplanes]);
            numplanes++;

            // find a plane that it enters
            for (i = 0; i < numplanes; i++) {
                if (DotProduct(mm->velocity, planes[i]) >= 0.1) {
                    continue; // move doesn't interact with the plane
                }

                // slide along the plane
                MM_ClipVelocity(mm->velocity, planes[i], clipVelocity, OVERCLIP);

                // see if there is a second plane that the new move enters
                for (j = 0; j < numplanes; j++) {
                    if (j == i) {
                        continue;
                    }

                    if (DotProduct(clipVelocity, planes[j]) >= 0.1) { 
                        continue; // move doesn't interact with the plane
                    }

                    // slide along the plane
                    MM_ClipVelocity(clipVelocity, planes[j], clipVelocity, OVERCLIP);

                    if (DotProduct(clipVelocity, planes[i]) >= 0) {
                        continue; // move doesn't interact with the plane
                    }

                    // slide the original velocity along the crease
                    CrossProduct(planes[i], planes[j], dir);
                    VectorNormalize(dir);
                    d = DotProduct(dir, mm->velocity);
                    VectorScale(dir, d, clipVelocity);

                    // see if there is a third plane the the new move enters
                    for (k = 0; k < numplanes; k++) {
                        if (k == i || k == j) {
                            continue;
                        }

                        if (DotProduct(clipVelocity, planes[k]) >= 0.1) {
                            continue; // move doesn't interact with the plane
                        }

                        // stop dead at a tripple plane interaction
                        VectorClear(mm->velocity);
                        return qtrue;
                    }
                }

                // if we have fixed all interactions, try another move
                VectorCopy(clipVelocity, mm->velocity);
                break;
            }
        }
    }

    if (mm->velocity[0] || mm->velocity[1]) {
        if (mm->groundPlane) {
            VectorCopy(mm->velocity, dir);
            VectorNegate(dir, dir);
            VectorNormalize(dir);

            if (MM_AddTouchEnt(trace.entityNum)) {
                if (!mm->hit_obstacle) {
                    mm->hit_obstacle = true;
                    VectorCopy(mm->origin, mm->hit_origin);
                }

                VectorAdd(mm->obstacle_normal, dir, mm->obstacle_normal);
            }
        }

        VectorClear(mm->velocity);
        return true;
    }

    mm->velocity[2] = 0;
    return false;
}

void MM_GroundTraceInternal(void)
{
    if (mml.groundTrace.fraction == 1.0f) {
        mm->groundPlane = qfalse;
        mm->walking     = qfalse;
        return;
    }

    if (mm->velocity[2] > 0.0f) {
        if (DotProduct(mm->velocity, mml.groundTrace.plane.normal) > 10.0f) {
            mm->groundPlane = qfalse;
            mm->walking     = qfalse;
            return;
        }
    }

    // slopes that are too steep will not be considered onground
    if (mml.groundTrace.plane.normal[2] < MIN_WALK_NORMAL) {
        vec3_t oldvel;
        float  d;

        VectorCopy(mm->velocity, oldvel);
        VectorSet(mm->velocity, 0, 0, -1.0f / mm->frametime);
        MM_SlideMove(qfalse);

        d = VectorLength(mm->velocity);
        VectorCopy(oldvel, mm->velocity);

        if (d > (0.1f / mm->frametime)) {
            mm->groundPlane = qtrue;
            mm->walking     = qfalse;
            VectorCopy(mml.groundTrace.plane.normal, mm->groundPlaneNormal);
            return;
        }
    }

    mm->groundPlane = qtrue;
    mm->walking     = qtrue;
    VectorCopy(mml.groundTrace.plane.normal, mm->groundPlaneNormal);

    MM_AddTouchEnt(mml.groundTrace.entityNum);
}

void MM_GroundTrace(void)
{
    float point[3];

    point[0] = mm->origin[0];
    point[1] = mm->origin[1];
    point[2] = mm->origin[2] - 0.25f;

    gi.trace(&mml.groundTrace, mm->origin, mm->mins, mm->maxs, point, mm->entityNum, mm->tracemask, qtrue, qfalse);
    MM_GroundTraceInternal();
}

void MM_StepSlideMove(void)
{
    vec3_t   start_o;
    vec3_t   start_v;
    vec3_t   nostep_o;
    vec3_t   nostep_v;
    trace_t  trace;
    qboolean bWasOnGoodGround;
    vec3_t   up;
    vec3_t   down;
    qboolean start_hit_wall;
    vec3_t   start_wall_normal;
    qboolean first_hit_wall;
    vec3_t   first_wall_normal;
    vec3_t   start_hit_origin;
    vec3_t   first_hit_origin;
    trace_t  nostep_groundTrace;

    VectorCopy(mm->origin, start_o);
    VectorCopy(mm->velocity, start_v);
    start_hit_wall = mm->hit_obstacle;
    VectorCopy(mm->hit_origin, start_hit_origin);
    VectorCopy(mm->obstacle_normal, start_wall_normal);

    if (MM_SlideMove(qtrue) == 0) {
        if (mml.validGroundTrace) {
            MM_GroundTraceInternal();
        } else {
            MM_GroundTrace();
        }

        return;
    }

    VectorCopy(start_o, down);
    down[2] -= STEPSIZE;
    gi.trace(&trace, start_o, mm->mins, mm->maxs, down, mm->entityNum, mm->tracemask, qtrue, qfalse);
    VectorSet(up, 0, 0, 1);

    // never step up when you still have up velocity
    if (mm->velocity[2] > 0 && (trace.fraction == 1.0f || DotProduct(trace.plane.normal, up) < MIN_WALK_NORMAL)) {
        if (mml.validGroundTrace) {
            MM_GroundTraceInternal();
        } else {
            MM_GroundTrace();
        }

        return;
    }

    if (mm->groundPlane && mm->groundPlaneNormal[2] >= MIN_WALK_NORMAL) {
        bWasOnGoodGround = qtrue;
    } else {
        bWasOnGoodGround = qfalse;
    }

    VectorCopy(start_o, up);
    up[2] += 18;

    VectorCopy(mm->origin, nostep_o);
    VectorCopy(mm->velocity, nostep_v);
    memcpy(&nostep_groundTrace, &mml.groundTrace, sizeof(trace_t));

    VectorCopy(up, mm->origin);
    VectorCopy(start_v, mm->velocity);

    first_hit_wall = mm->hit_obstacle;
    VectorCopy(mm->hit_origin, first_hit_origin);
    VectorCopy(mm->obstacle_normal, first_wall_normal);

    mm->hit_obstacle = start_hit_wall;
    VectorCopy(start_hit_origin, mm->hit_origin);
    VectorCopy(start_wall_normal, mm->obstacle_normal);
    MM_SlideMove(qtrue);

    VectorCopy(mm->origin, down);
    down[2] -= STEPSIZE * 2;

    // test the player position if they were a stepheight higher
    gi.trace(&trace, mm->origin, mm->mins, mm->maxs, down, mm->entityNum, mm->tracemask, qtrue, qfalse);
    if (trace.entityNum != ENTITYNUM_WORLD && trace.entityNum != ENTITYNUM_NONE) {
        VectorCopy(nostep_o, mm->origin);
        VectorCopy(nostep_v, mm->velocity);
        memcpy(&mml.groundTrace, &nostep_groundTrace, sizeof(mml.groundTrace));
        mm->hit_obstacle = first_hit_wall;
        VectorCopy(first_hit_origin, mm->hit_origin);
        VectorCopy(first_wall_normal, mm->obstacle_normal);

        if (mml.validGroundTrace) {
            MM_GroundTraceInternal();
        } else {
            MM_GroundTrace();
        }

        return;
    }

    if (!trace.allsolid) {
        memcpy(&mml.groundTrace, &trace, sizeof(mml.groundTrace));
        mml.validGroundTrace = qtrue;

        if (bWasOnGoodGround && trace.fraction < 1 && trace.plane.normal[2] < MIN_WALK_NORMAL) {
            VectorCopy(nostep_o, mm->origin);
            VectorCopy(nostep_v, mm->velocity);

            if (first_hit_wall) {
                mm->hit_obstacle = first_hit_wall;
                VectorCopy(first_hit_origin, mm->hit_origin);
                VectorCopy(first_wall_normal, mm->obstacle_normal);
            }

            MM_GroundTraceInternal();
            return;
        }

        VectorCopy(trace.endpos, mm->origin);
    }

    if (trace.fraction < 1) {
        MM_ClipVelocity(mm->velocity, trace.plane.normal, mm->velocity, OVERCLIP);
    }

    if (mml.validGroundTrace) {
        MM_GroundTraceInternal();
    } else {
        MM_GroundTrace();
    }
}

void MM_ClipVelocity2D(float *in, float *normal, float *out, float overbounce)
{
    float backoff;
    float dir_z;
    float normal2[3];

    if (normal[2] >= MIN_WALK_NORMAL) {
        if (in[0] == 0.0f && in[1] == 0.0f) {
            VectorClear(out);
            return;
        }

        normal2[0] = in[0] * DotProduct2D(in, normal);
        normal2[1] = in[1] * DotProduct2D(in, normal);
        normal2[2] = normal[2] * DotProduct2D(in, in);

        VectorNormalize(normal2);

        dir_z = -normal2[2];

        out[0] = in[0];
        out[1] = in[1];
        out[2] = DotProduct2D(in, normal2) / dir_z;
    } else {
        backoff = DotProduct2D(in, normal);

        if (backoff < 0) {
            backoff *= overbounce;
        } else {
            backoff /= overbounce;
        }

        out[0] = in[0] - normal[0] * backoff;
        out[1] = in[1] - normal[1] * backoff;
        out[2] = -(backoff * normal[2]);
    }
}

void MmoveSingle(mmove_t *mmove)
{
    float   point[3];
    trace_t trace;

    mm = mmove;

    mmove->numtouch  = 0;
    mm->hit_obstacle = false;
    VectorCopy(vec3_origin, mm->obstacle_normal);
    mm->hit_temp_obstacle = false;

    memset(&mml, 0, sizeof(mml_t));

    VectorCopy(mm->origin, mml.previous_origin);
    VectorCopy(mm->velocity, mml.previous_velocity);

    if (mm->walking) {
        if (mm->desired_speed < 1.0f) {
            VectorClear2D(mm->velocity);
            MM_GroundTrace();
            return;
        }

        vec3_t wishdir;

        MM_ClipVelocity2D(mm->desired_dir, mm->groundPlaneNormal, wishdir, OVERCLIP);
        VectorNormalize(wishdir);

        mm->velocity[0] = mm->desired_speed * wishdir[0];
        mm->velocity[1] = mm->desired_speed * wishdir[1];
    } else if (mm->groundPlane) {
        MM_ClipVelocity(mm->velocity, mm->groundPlaneNormal, mm->velocity, OVERCLIP);
    }

    MM_StepSlideMove();

    if (!mm->walking && mml.previous_velocity[2] >= 0.0f && mm->velocity[2] <= 0.0f) {
        point[0] = mm->origin[0];
        point[1] = mm->origin[1];
        point[2] = mm->origin[2] - 18.0f;

        gi.trace(&trace, mm->origin, mm->mins, mm->maxs, point, mm->entityNum, mm->tracemask, qtrue, qfalse);

        if (trace.fraction < 1.0f && !trace.allsolid) {
            VectorCopy(trace.endpos, mm->origin);
            MM_GroundTrace();
        }
    }
}
