/*
===========================================================================
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
===========================================================================
*/

#include "vehicle.h"
#include "player.h"
#include "debuglines.h"
#include "g_phys.h"

CLASS_DECLARATION(DrivableVehicle, VehicleWheelsX2, "VehicleWheelsX2") {
    {&EV_Damage, &VehicleWheelsX2::EventDamage},
    {&EV_Killed, &VehicleWheelsX2::Killed     },
    {NULL,       NULL                         }
};

VehicleWheelsX2::VehicleWheelsX2()
{
    gravity              = 1.0;
    m_fDifferentialRatio = 4.88f;
    m_fGearEfficiency    = 0.7f;
    m_fGearRatio[0]      = -2.95f;
    m_fGearRatio[1]      = 2.95f;
    m_fGearRatio[2]      = 1.95f;
    m_fGearRatio[3]      = 1.0f;
    m_iGear              = 1;
    m_iRPM               = 0;
    m_fAccelerator       = 0.0f;
    m_bAutomatic         = qtrue;
    m_bBackSlipping      = qfalse;
    m_bFrontSlipping     = qfalse;
}

void VehicleWheelsX2::Killed(Event *ev)
{
    deadflag = DEAD_DEAD;
}

float VehicleWheelsX2::TorqueLookup(int rpm)
{
    if (rpm < 2100) {
        return 100.0;
    } else if (rpm >= 3600) {
        return 0.0;
    } else {
        return (100 * (3600 - rpm)) / 1000.f;
    }
}

void VehicleWheelsX2::UpdateVariables(
    Vector *acceleration, Vector *vpn, Vector *vup, Vector *vright, Vector *t_vpn, Vector *t_vup, Vector *t_vright
)
{
    int    i;
    Vector vNewCG;
    Vector vBoxSize;
    float  longspeed;
    Vector _i, _j, _k;
    Vector r_i, r_j, r_k;
    Vector r_velocity;
    float  dots[4];

    r_velocity = velocity;

    m_bIsSkidding = false;
    vBoxSize      = maxs - mins;

    SetControllerAngles(0, Vector(0, turnangle, 0));
    SetControllerAngles(1, Vector(0, turnangle, 0));

    vNewCG = localorigin;

    if (vNewCG != m_vCG) {
        m_vCG = vNewCG;
        vNewCG -= localorigin;

        m_fWheelBase      = vBoxSize.x;
        m_fWheelFrontDist = vBoxSize.x * 0.5 - vNewCG.x;
        m_fWheelBackDist  = vBoxSize.x * -0.5 + vNewCG.x;
        m_fTrackWidth     = vBoxSize.y;
    }

    Vector vNewVpn = angles;
    vNewVpn.AngleVectorsLeft(&r_i, &r_j, &r_k);
    vNewVpn.y += turnangle;
    vNewVpn.AngleVectorsLeft(&_i, &_j, &_k);

    dots[3] = r_velocity * *vpn * 0.02;
    dots[2] = r_velocity * *vright * 0.02;
    dots[1] = r_velocity * *t_vpn * 0.02;
    dots[0] = r_velocity * *t_vright * 0.02;

    m_sWheels[0].fTraction  = 0.8f;
    m_sWheels[1].fTraction  = 0.9f;
    m_sWheels[0].fYawOffset = 0;
    m_sWheels[1].fYawOffset = DEG2RAD(turnangle);
    m_sWheels[0].fLongDist  = m_fWheelBackDist * 0.02;
    m_sWheels[1].fLongDist  = m_fWheelFrontDist * 0.02;
    m_sWheels[0].fLatDist   = m_fTrackWidth * -0.02;
    m_sWheels[1].fLatDist   = m_fTrackWidth * 0.02;

    // initialize the wheels
    for (i = 0; i < 2; i++) {
        m_sWheels[i].fLongForce = 0;
        m_sWheels[i].fForce     = 0;

        m_sWheels[i].vLongForce  = vec_zero;
        m_sWheels[i].vLatForce   = vec_zero;
        m_sWheels[i].vForce      = vec_zero;
        m_sWheels[i].vVelocity   = vec_zero;
        m_sWheels[i].bSkidding   = false;
        m_sWheels[i].bSpinning   = false;
        m_sWheels[i].fTorque     = 0;
        m_sWheels[i].fLatTorque  = 0;
        m_sWheels[i].fLongTorque = 0;
    }

    for (i = 0; i < 2; i++) {
        Vector p1, p3, p4;
        Vector vTp, vTr, vTu;
        vec3_t p2;

        p3 = Corners[i];
        p3.normalize();

        vectoangles(p3, p2);
        p1    = vec_zero;
        p1[1] = angles[1] + p2[1] + 90;
        p1.AngleVectorsLeft(&vTp, &vTr, &vTu);

        m_sWheels[i].vVelocity = vTp * RAD2DEG(m_vAngularVelocity.y) * fabs(m_sWheels[i].fLongDist) * level.frametime;

        G_LineWidth(5.0);

        G_DebugLine(
            origin + Corners[i][0] * r_i + Corners[i][1] * r_j,
            origin + Corners[i][0] * r_i + Corners[i][1] * r_j + m_sWheels[i].vVelocity * 50,
            0.7f,
            0.5f,
            1.0f,
            1.0f
        );

        p1    = vec_zero;
        p1[1] = angles[1] + RAD2DEG(m_sWheels[i].fYawOffset);
        p1.AngleVectorsLeft(&vTp, &vTr, &vTu);

        m_sWheels[i].vOrientation = vTp;

        G_LineWidth(5.0);

        G_DebugLine(
            origin + Corners[i][0] * r_i + Corners[i][1] * r_j,
            origin + Corners[i][0] * r_i + Corners[i][1] * r_j + m_sWheels[i].vOrientation * 50,
            0.7f,
            0.0f,
            1.0f,
            1.0f
        );
    }

    Vector  start, end;
    Vector  boxoffset;
    Vector  t_mins(-8, -8, -8);
    Vector  t_maxs(8, 8, 8);
    trace_t trace;

    start = origin + *vpn * (m_vCG[0] - localorigin[0]) + *vright * (m_vCG[1] - localorigin[1])
          + *vup * (m_vCG[2] - localorigin[2]);

    end = start;
    end[2] -= maxtracedist;

    trace = G_Trace(start, t_mins, t_maxs, end, this, MASK_SOLID, qfalse, "Vehicle::PostThink Corners");

    m_fInertia        = m_fMass * 100;
    m_fWheelFrontLoad = m_fMass * 9.81 * fabs(m_fWheelFrontDist / m_fWheelBase);
    m_fWheelBackLoad  = m_fMass * 9.81 * fabs(m_fWheelBackDist / m_fWheelBase);

    longspeed = m_vAcceleration.length() * 0.02 * m_fMass / m_fWheelBase;
    m_fWheelFrontLoad -= longspeed;
    m_fWheelBackLoad += longspeed;
    m_sWheels[0].fLoad = m_fWheelBackLoad;
    m_sWheels[1].fLoad = m_fWheelFrontLoad;

    float fMassShifted     = m_fTireRadius * M_PI;
    m_fTireRotationalSpeed = dots[3] / (fMassShifted + fMassShifted);
    m_iRPM                 = m_fTireRotationalSpeed * m_fGearRatio[m_iGear] * m_fDifferentialRatio * 10;
    m_fMaxTraction =
        TorqueLookup(m_iRPM) * m_fGearRatio[m_iGear] * m_fDifferentialRatio * m_fGearEfficiency / m_fTireRadius;
    m_fTractionForce = m_fAccelerator * m_fMaxTraction;

    if (m_bIsBraking && dots[3] > 0) {
        m_fTractionForce = -fSign(dots[3]) * 1000 * dots[3];
    }

    for (i = 0; i < 2; i++) {
        float maxgrip;

        maxgrip = m_sWheels[i].fTraction * m_sWheels[i].fLoad * 9.81;
        if (m_fTractionForce > maxgrip) {
            m_fTractionForce       = maxgrip * 0.6;
            m_sWheels[i].bSpinning = true;
            m_sWheels[i].bSkidding = true;
            Com_Printf("Skidding!\n");
        }

        if (m_fTractionForce < -maxgrip) {
            m_fTractionForce       = -maxgrip * 0.6;
            m_sWheels[i].bSkidding = true;
            Com_Printf("Skidding!\n");
        }

        m_sWheels[i].fLongForce = this->m_sWheels[i].fLongForce + m_fTractionForce;
        m_sWheels[i].vLongForce += m_sWheels[i].vOrientation * m_sWheels[i].fLongForce;
    }

    m_vResistance[0] =
        -(velocity[0] * 0.02 * m_fRollingResistance + velocity[0] * 0.02 * m_fDrag * fabs(velocity[0] * 0.02));
    m_vResistance[1] =
        -(velocity[1] * 0.02 * m_fRollingResistance + velocity[1] * 0.02 * m_fDrag * fabs(velocity[1] * 0.02));
    m_vResistance[2] =
        -(velocity[2] * 0.02 * m_fRollingResistance + velocity[2] * 0.02 * m_fDrag * fabs(velocity[2] * 0.02));

    m_fBrakingPerformance = -m_fRollingResistance - *vpn * Vector(0, 0, -1) * m_fMass;
    if (dots[3] == 0) {
        m_fBrakingPerformance = 0;
    } else {
        m_fBrakingPerformance = (dots[3] - (m_fTireRotationalSpeed * m_fTireRadius - m_fBrakingPerformance)) / dots[3];
        m_fBrakingPerformance = (1.0 - m_fBrakingPerformance) * -(-m_fFrontBrakingForce - m_fBackBrakingForce);
    }

    if (r_velocity.length() * 0.02 > 0.01) {
        m_fTangForce = 0;
        m_vTangForce = vec_zero;

        for (i = 0; i < 2; i++) {
            Vector va, vr;
            Vector vVel;
            Vector v1, v2;

            va   = angles;
            va.x = 0;
            va.y += RAD2DEG(m_sWheels[i].fYawOffset);
            va.z = 0;
            va.AngleVectorsLeft(NULL, &vr, NULL);

            vVel = m_sWheels[i].vVelocity + velocity;
            // get the direction
            v1 = vVel;
            v1.normalize();
            // get the angles
            vectoangles(v1, v2);

            m_sWheels[i].fSlipAngle = DEG2RAD(angledist(va.y - v2.y));
            m_sWheels[i].fGripCoef  = m_sWheels[i].fSlipAngle * 0.15;
            m_sWheels[i].fGripCoef  = Q_clamp_float(m_sWheels[i].fGripCoef, -1.2f, 1.2f);
            m_sWheels[i].fLatForce += m_sWheels[i].fGripCoef * m_sWheels[i].fLoad * 9.81;

            if (m_sWheels[i].fLoad * 2 * m_sWheels[i].fTraction < fabs(m_sWheels[i].fLatForce)) {
                m_sWheels[i].fLatForce = m_sWheels[i].fLatForce / 9.81;
                m_sWheels[i].bSkidding = true;
                Com_Printf("Skidding %i!\n", i);
            }

            m_sWheels[i].vLatForce += vr * m_sWheels[i].fLatForce;

            G_LineWidth(3.0);

            G_DebugLine(
                origin + Corners[i][0] * r_i + Corners[i][1] * r_j,
                origin + Corners[i][0] * r_i + Corners[i][1] * r_j + m_sWheels[i].vLatForce * 50 * (1.0 / m_fMass),
                0.7f,
                0.5f,
                0.2f,
                1.0f
            );
        }
    } else {
        m_fTangForce = 0;
        m_vTangForce = vec_zero;
    }

    m_vForce = vec_zero;

    for (i = 0; i < 2; i++) {
        m_sWheels[i].vForce = m_sWheels[i].vLatForce + m_sWheels[i].vLongForce;
        m_vForce += m_sWheels[i].vForce;
    }

    m_vForce += m_vResistance;

    G_LineWidth(5.0);

    G_DebugLine(origin, origin + m_vTangForce * (1.0 / m_fMass) * 50, 1.0, 0.0, 1.0, 1.0);

    m_vAcceleration = m_vForce * (1.0 / m_fMass) * 50;
    m_fTorque       = 0;

    for (i = 0; i < 2; i++) {
        m_sWheels[i].fTorque += m_sWheels[i].fLongTorque + m_sWheels[i].fLatTorque;
        m_fTorque += m_sWheels[i].fTorque;
    }

    m_vAngularAcceleration[1] = angledist(RAD2DEG(m_fTorque));
    m_vAngularVelocity += m_vAngularAcceleration * level.frametime;
    m_vAngularVelocity *= 0.825f;
    avelocity = m_vAngularVelocity;

    if (m_bAutomatic) {
        //
        // set the gear
        //
        if (m_iRPM >= 2100 && m_iGear <= 2) {
            m_iGear++;
        }
        if (m_iRPM <= 600 && m_iGear > 1) {
            m_iGear--;
        }
    }

    prev_moveimpulse = moveimpulse;
}

void VehicleWheelsX2::Think()
{
    vmove_t vm;

    flags |= FL_POSTTHINK;
    prev_velocity = velocity;

    SetMoveInfo(&vm);
    VmoveSingle(&vm);
    GetMoveInfo(&vm);
}

void VehicleWheelsX2::Postthink()
{
    float        turn;
    Vector       i, j, k;
    int          index;
    trace_t      trace;
    Vector       normalsum;
    Vector       normalsum_Front;
    int          numnormals_Front;
    Vector       normalsum_Back;
    int          numnormals_Back;
    Vector       temp;
    Vector       pitch, roll;
    Vector       acceleration;
    Vector       atmp, atmp2, aup;
    VehicleBase *v;
    VehicleBase *last;
    float        drivespeed;
    Vector       primal_angles;
    Vector       vTmp;
    Vector       n_angles;

    primal_angles = angles;

    if (drivable) {
        currentspeed = moveimpulse;
        turnangle    = turnangle * 0.25f + turnimpulse;
        turnangle    = Q_clamp_float(turnangle, -maxturnrate, maxturnrate);

        if (turnangle > maxturnrate) {
            turnangle = maxturnrate;
        } else if (turnangle < -maxturnrate) {
            turnangle = -maxturnrate;
        } else if (fabs(turnangle) < 2) {
            turnangle = 0;
        }

        real_velocity     = origin - prev_origin;
        prev_origin       = origin;
        prev_acceleration = real_acceleration;
        real_acceleration = real_velocity - prev_velocity;
        prev_velocity     = real_velocity;
        acceleration      = real_acceleration - prev_acceleration;

        angles[0] = 0;
        angles[2] = 0;

        temp[0] = angles[0];
        temp[1] = angles[1];
        temp[2] = angles[2];
        temp.AngleVectorsLeft(&i, &j, &k);

        j = vec_zero - j;

        index            = 0;
        numnormals_Front = 0;
        numnormals_Back  = 0;

        for (index = 0; index < 4; index++) {
            Vector start, end;
            Vector boxoffset;
            Vector t_mins(-8, -8, -8);
            Vector t_maxs(8, 8, 8);

            boxoffset = Corners[index];

            start = origin + i * boxoffset[0] + j * boxoffset[1] + k * boxoffset[2];
            end   = start;
            end[2] -= maxtracedist;

            trace = G_Trace(start, mins, maxs, end, this, MASK_SOLID, qfalse, "Vehicle::PostThink Corners");

            if (trace.fraction != 1 && !trace.startsolid) {
                if (index > 2) {
                    normalsum_Front += trace.plane.normal;
                    numnormals_Front++;
                } else {
                    normalsum_Back += trace.plane.normal;
                    numnormals_Back++;
                }

                normalsum += trace.plane.normal;
            }
        }

        if (angles[2] < 60 || angles[2] > 300) {
            if (numnormals_Front > 1) {
                m_vFrontNormal = normalsum_Front * (1.0 / numnormals_Front);

                temp              = m_vFrontNormal;
                m_vFrontAngles[0] = -temp.CrossProduct(temp, j).toPitch();
                // no yaw
                m_vFrontAngles[1] = 0;

                temp              = m_vFrontNormal;
                m_vFrontAngles[2] = temp.CrossProduct(temp, i).toPitch();
            }

            if (numnormals_Back > 1) {
                m_vBackNormal = normalsum_Back * (1.0 / numnormals_Back);

                temp             = m_vBackNormal;
                m_vBackAngles[0] = -temp.CrossProduct(temp, j).toPitch();
                // no yaw
                m_vBackAngles[1] = 0;

                temp             = m_vBackNormal;
                m_vBackAngles[2] = temp.CrossProduct(temp, i).toPitch();
            }

            if (numnormals_Front + numnormals_Back > 1) {
                m_vBaseNormal = (normalsum_Front + normalsum_Back) * (1.0 / numnormals_Front + numnormals_Back);

                temp             = m_vBaseNormal;
                m_vBaseAngles[0] = -temp.CrossProduct(temp, j).toPitch();
                // no yaw
                m_vBaseAngles[1] = 0;

                temp             = m_vBaseNormal;
                m_vBaseAngles[2] = temp.CrossProduct(temp, i).toPitch();
            }
        }

        n_angles    = m_vBaseAngles - angles;
        n_angles[0] = angledist(n_angles[0]);
        n_angles[1] = angledist(n_angles[1]);
        n_angles[2] = angledist(n_angles[2]);

        angles[0] += n_angles[0] * 4.5 * level.frametime;
        angles[2] += n_angles[2] * 4.5 * level.frametime;

        m_vPrevNormal = m_vBaseNormal;

        if (currentspeed < 0 && velocity * i > 0) {
            m_fFrontBrakingForce = 0.2f;
            m_fBackBrakingForce  = 0.1f;
        } else {
            m_fFrontBrakingForce = 0;
            m_fBackBrakingForce  = 0;
        }

        turn = turnangle * 0.005;

        Vector _i, _j, _k;
        Vector vNewVpn = angles;
        vNewVpn.y += turnangle;

        vNewVpn.AngleVectorsLeft(&_i, &_j, &_k);
        UpdateVariables(&acceleration, &i, &j, &k, &_i, &_j, &_k);

        m_vAngles = angles;

        if (currentspeed < 0 && velocity * i > 0) {
            currentspeed = 0;
        }

        normalsum = vec_zero;

        for (index = 0; index < 4; index++) {
            Vector start, end;
            Vector boxoffset;
            Vector t_mins(-8, -8, -8);
            Vector t_maxs(8, 8, 8);

            boxoffset = Corners[index];

            start = origin + i * boxoffset[0] + j * boxoffset[1] + k * boxoffset[2] + Vector(0, 0, 10);
            end   = start;
            end[2] -= maxtracedist;

            trace = G_Trace(start, mins, maxs, end, this, MASK_SOLID, qfalse, "Vehicle::PostThink Corners");

            if (trace.fraction != 1.0) {
                float fCoef;

                if (index > 2) {
                    fCoef = m_fWheelFrontLoad / (m_fWheelFrontDist / m_fWheelBase * m_fMass);
                    fCoef = Q_clamp_float(fCoef, 0, 1);

                    normalsum += _i * currentspeed * fCoef;
                    normalsum += i * currentspeed * (1.0 - fCoef);
                } else {
                    fCoef = m_fWheelBackLoad / (m_fWheelBackDist / m_fWheelBase * m_fMass);
                    fCoef = Q_clamp_float(fCoef, 0, 1);

                    normalsum += i * currentspeed * fCoef;
                    normalsum += _i * currentspeed * (1.0 - fCoef);
                }
            }
        }

        AnglesToAxis(angles, orientation);

        vmove_t vm;
        SetMoveInfo(&vm);
        m_sMoveGrid->SetMoveInfo(&vm);
        m_sMoveGrid->SetOrientation(orientation);

        if (m_sMoveGrid->CheckStuck()) {
            angles = primal_angles;
            AnglesToAxis(angles, orientation);
        }

        if (groundentity && (angles[2] < 60 || angles[2] > 300)) {
            float  dot;
            Vector newvel, flatvel;

            velocity[0] *= 0.925f;
            velocity[1] *= 0.925f;

            newvel = orientation[0];
            velocity += newvel * currentspeed;
            newvel[2] = 0;

            dot = velocity * newvel;
            dot = Q_clamp_float(dot, -speed, speed);

            if (dot > speed) {
                dot = speed;
            } else if (dot < -speed) {
                dot = -speed;
            } else if (fabs(dot) < 20) {
                dot = 0;
            }

            flatvel = newvel * dot;

            velocity[0] = flatvel[0];
            velocity[1] = flatvel[1];
            velocity[2] += jumpimpulse * dot;
            avelocity *= 0.05f;

            if (steerinplace) {
                if (dot < 350) {
                    dot = 350;
                }

                avelocity.y += turn * dot;
            } else {
                avelocity.y += turn * dot;
            }
        } else {
            if (angles[2] > 60) {
                angles[2]; // FIXME: ??
            }

            velocity += m_vAcceleration * level.frametime;
        }

        angles += avelocity * level.frametime;
        setAngles(angles);

        prev_origin = origin;
    }

    drivespeed = acceleration * orientation[0];

    if (drivable && driver.ent) {
        str sound_name;

        if (currentspeed > 0) {
            sound_name = "snd_forward";
        } else if (currentspeed == 0) {
            sound_name = "snd_idle";
        } else {
            sound_name = "snd_backward";
        }

        LoopSound(sound_name);
    }

    i = orientation[0];
    j = orientation[1];
    k = orientation[2];

    if (driver.ent) {
        driver.ent->setOrigin(origin + i * driveroffset[0] + j * driveroffset[1] + k * driveroffset[2]);

        if (drivable) {
            // clear the driver velocity
            driver.ent->velocity = vec_zero;
            // set the driver angles to the vehicle's angle
            driver.ent->setAngles(angles);
        }
    }

    vTmp = (angles - primal_angles) * level.frametime;
    if (vTmp.x > 180 || vTmp.x < -180) {
        vTmp.x = 0;
    }
    if (vTmp.y > 180 || vTmp.y < -180) {
        vTmp.y = 0;
    }
    if (vTmp.z > 180 || vTmp.z < -180) {
        vTmp.z = 0;
    }

    if (vTmp.x > -1 || vTmp.x < 1) {
        vTmp.x = 0;
    }
    if (vTmp.y > -1 || vTmp.y < 1) {
        vTmp.y = 0;
    }
    if (vTmp.z > -1 || vTmp.z < 1) {
        vTmp.z = 0;
    }

    avelocity = vTmp;

    for (last = this; last->vlink; last = v) {
        v = last->vlink;

        v->setOrigin(origin + i * v->offset.x + j * v->offset.y + k * v->offset.z);
        v->avelocity = avelocity;
        v->velocity  = velocity;

        v->angles[ROLL]  = angles[ROLL];
        v->angles[YAW]   = angles[YAW];
        v->angles[PITCH] = (int)(drivespeed / 4.f + v->angles[PITCH]) % 360;
        v->setAngles(v->angles);
    }

    CheckWater();
    WorldEffects();

    last_origin = origin;

    if (!driver.ent && !velocity.length() && groundentity && !(watertype & CONTENTS_LAVA)) {
        // make the vehicle stationary if it's completely immobile
        flags &= ~FL_POSTTHINK;
        if (drivable) {
            setMoveType(MOVETYPE_STATIONARY);
        }
    }
}
