#include "Library/Math/ParabolicPath.h"

#include "Library/Math/MathUtil.h"

namespace al {

ParabolicPath::ParabolicPath() = default;

void ParabolicPath::initFromUpVector(const sead::Vector3f& start, const sead::Vector3f& end,
                                     const sead::Vector3f& up) {
    f32 scalar;
    sead::Vector3f upDir;
    separateScalarAndDirection(&scalar, &upDir, up);
    initFromUpVector(start, end, upDir, scalar);
}

void ParabolicPath::initFromUpVector(const sead::Vector3f& start, const sead::Vector3f& end,
                                     const sead::Vector3f& up, f32 maxHeight) {
    mUp.set(up);
    sead::Vector3f diff = end - start;
    f32 verticalDistance = diff.dot(mUp);
    mHorizontalDirection = diff - (mUp * verticalDistance);
    separateScalarAndDirection(&mHorizontalDistance, &mHorizontalDirection, mHorizontalDirection);
    calcParabolicFunctionParam(&mGravity, &mInitialVelY, maxHeight, verticalDistance);
    mStart.set(start);
}

void ParabolicPath::initFromMaxHeight(const sead::Vector3f& start, const sead::Vector3f& end,
                                      const sead::Vector3f& projectedEnd) {
    sead::Vector3f up;
    f32 scalar;
    separateScalarAndDirection(&scalar, &up, projectedEnd - end);
    initFromUpVector(start, end, up, (projectedEnd - start).dot(up));
}

void ParabolicPath::initFromUpVectorAddHeight(const sead::Vector3f& start,
                                              const sead::Vector3f& end, const sead::Vector3f& up,
                                              f32 height) {
    f32 verticalDistance = sead::Mathf::clampMin((end - start).dot(up), 0.0f);
    initFromUpVector(start, end, up, verticalDistance + height);
}

f32 ParabolicPath::getLength(f32 start, f32 end, s32 iterations) const {
    s32 steps = sead::Mathi::clampMin(iterations, 1);
    f32 stepSize = (end - start) / steps;
    f32 squaredHStepSize = sead::Mathf::square(stepSize * mHorizontalDistance);
    f32 vDist = (mGravity * start + mInitialVelY) * start;
    f32 length = 0;

    for (s32 i = 0; i < steps;) {
        i++;
        f32 prevVDist = vDist;
        f32 curStep = start + i * stepSize;
        vDist = (mGravity * curStep + mInitialVelY) * curStep;
        length += sead::Mathf::sqrt(squaredHStepSize + sead::Mathf::square(vDist - prevVDist));
    }

    return length;
}

f32 ParabolicPath::getTotalLength(s32 iterations) const {
    return getLength(0.0f, 1.0f, iterations);
}

void ParabolicPath::calcPositionHV(sead::Vector3f* pos, f32 h, f32 v) const {
    f32 hDist = mHorizontalDistance * h;
    f32 vDist = (mGravity * v + mInitialVelY) * v;
    *pos = mStart + (vDist * mUp) + (hDist * mHorizontalDirection);
}

void ParabolicPath::calcPosition(sead::Vector3f* pos, f32 prog) const {
    calcPositionHV(pos, prog, prog);
}

void ParabolicPath::calcPositionEaseOutH(sead::Vector3f* pos, f32 prog) const {
    calcPositionHV(pos, easeOut(prog), prog);
}

void ParabolicPath::calcDirection(sead::Vector3f* dir, f32 prog, f32 stepSize) const {
    f32 prog1, prog2;
    if (prog < stepSize) {
        prog1 = 0.0f;
        prog2 = stepSize;
    } else if ((1.0f - stepSize) < prog) {
        prog2 = 1.0f;
        prog1 = 1.0f - stepSize;
    } else {
        prog2 = prog + stepSize;
        prog1 = prog;
    }

    sead::Vector3f pos1, pos2;
    calcPosition(&pos1, prog1);
    calcPosition(&pos2, prog2);
    *dir = pos2 - pos1;
    tryNormalizeOrZero(dir);
}

f32 ParabolicPath::calcPathSpeedFromGravityAccel(f32 frames) const {
    return sead::Mathf::abs(frames / mGravity);
}

f32 ParabolicPath::calcPathSpeedFromAverageSpeed(f32 frames) const {
    return frames / getTotalLength(10);
}

f32 ParabolicPath::calcPathSpeedFromHorizontalSpeed(f32 frames) const {
    return frames / mHorizontalDistance;
}

s32 ParabolicPath::calcPathTimeFromGravityAccel(f32 frames) const {
    return 1.0f / calcPathSpeedFromGravityAccel(frames);
}

s32 ParabolicPath::calcPathTimeFromAverageSpeed(f32 frames) const {
    return 1.0f / calcPathSpeedFromAverageSpeed(frames);
}

s32 ParabolicPath::calcPathTimeFromHorizontalSpeed(f32 frames) const {
    return 1.0f / calcPathSpeedFromHorizontalSpeed(frames);
}

}  // namespace al
