#include "patches.h"
#include "camera_patches.h"
#include "transform_ids.h"
#include "z64cutscene.h"
#include "overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope.h"
#include "overlays/gamestates/ovl_file_choose/z_file_select.h"

// #define PRINT_CAMERA_INFO

static bool camera_interpolation_forced = false;
static bool camera_skip_interpolation_forced = false;
static bool camera_ignore_tracking = false;
static bool in_kaleido = false;
static bool prev_in_kaleido = false;

static bool camera_skipped = false;

void set_camera_skipped(bool skipped) {
    camera_skipped = skipped;
}

void clear_camera_skipped() {
    camera_skipped = false;
}

bool camera_was_skipped() {
    return camera_skipped;
}

void camera_pre_play_update(PlayState* play) {
}

void camera_post_play_update(PlayState* play) {
    // Track whether the game is in kaleido.
    prev_in_kaleido = in_kaleido;

    if ((play->pauseCtx.state != 0) || (play->pauseCtx.debugEditor != DEBUG_EDITOR_NONE)) {
        in_kaleido = true;
    }
    else {
        in_kaleido = false;

        if (play->activeCamId >= 0 && play->activeCamId < NUM_CAMS) {
            Camera *active_cam = play->cameraPtrs[play->activeCamId];
#ifdef PRINT_CAMERA_INFO
            recomp_printf("active_cam->setting %d  active_cam->mode %d play->sceneId %d\n", active_cam->setting, active_cam->mode, play->sceneId);
#endif

            // Dedicated section for workarounds where the heuristic fails to detect the large amount of movements the camera does in these particular areas.
            bool force_interpolation = false;
            if (active_cam->setting == CAM_SET_BOSS_MAJORA) {
                // Majora's Mask final fight. All cameras should transition smoothly during this fight while not in a cutscene.
                force_interpolation = true;
            }
            else if (active_cam->setting == CAM_SET_NORMAL0 || active_cam->setting == CAM_SET_DUNGEON0) {
                force_interpolation =
                    // Pirates' Fortress Moat. Pushing the switch that unlocks the fortress will cause very large camera movement.
                    play->sceneId == SCENE_TORIDE ||
                    // Z-targetting an actor.
                    active_cam->mode == CAM_MODE_FOLLOWTARGET ||
                    // Z-targetting nothing.
                    active_cam->mode == CAM_MODE_TARGET ||
                    // Z-targetting in battle.
                    active_cam->mode == CAM_MODE_BATTLE;
            }
            // TODO: This setting claims "Smoothly and gradually return camera to Player after a cutscene "CONNECT0"". It might be worth it to enable this globally regardless of the scene.
            else if (active_cam->setting == CAM_SET_CONNECT0) {
                // Stone tower and inverted stone tower. The block puzzles will cause very large camera movement after they're activated.
                force_interpolation = play->sceneId == SCENE_F40 || play->sceneId == SCENE_F41;
            }
            else if (active_cam->setting == CAM_SET_FREE0) {
                // Cutscene after Majora fight. The camera zooms out while facing the moon with a very large movement.
                force_interpolation = play->sceneId == SCENE_00KEIKOKU && play->csCtx.scriptIndex == 0 && play->csCtx.curFrame <= 98 && gSaveContext.sceneLayer == 8;
            }

            if (force_interpolation) {
                force_camera_interpolation();
            }
            // Dedicated section for workarounds where the heuristic fails to detect small camera teleports.
            bool force_no_interpolation = false;

            // Music Box House. The camera gets teleported by a very small amount when Link gets the Gibdo mask.
            if (play->sceneId == SCENE_MUSICHOUSE && play->csCtx.scriptIndex == 2 && play->csCtx.curFrame == 525 && active_cam->setting == CAM_SET_FREE0) {
                force_no_interpolation = true;
            }

            if (force_no_interpolation) {
                force_camera_skip_interpolation();
            }
        }
    }
}

s32 View_ApplyPerspective(View* view);
s32 View_ApplyOrtho(View* view);

void force_camera_interpolation() {
    camera_interpolation_forced = true;
}

void force_camera_skip_interpolation() {
    camera_skip_interpolation_forced = true;
}

void force_camera_ignore_tracking() {
    camera_ignore_tracking = true;
}

RECOMP_PATCH void KaleidoScope_SetView(PauseContext* pauseCtx, f32 eyeX, f32 eyeY, f32 eyeZ) {
    Vec3f eye;
    Vec3f at;
    Vec3f up;

    eye.x = eyeX;
    eye.y = eyeY;
    eye.z = eyeZ;
    at.x = at.y = at.z = 0.0f;
    up.x = up.z = 0.0f;
    up.y = 1.0f;

    // @recomp Force interpolation for this view and skip tracking positions.
    force_camera_interpolation();
    force_camera_ignore_tracking();

    View_LookAt(&pauseCtx->view, &eye, &at, &up);
    View_Apply(&pauseCtx->view,
               VIEW_ALL | VIEW_FORCE_VIEWING | VIEW_FORCE_VIEWPORT | VIEW_FORCE_PROJECTION_PERSPECTIVE);
}


RECOMP_PATCH void FileSelect_SetView(FileSelectState* this, f32 eyeX, f32 eyeY, f32 eyeZ) {
    Vec3f eye;
    Vec3f lookAt;
    Vec3f up;

    eye.x = eyeX;
    eye.y = eyeY;
    eye.z = eyeZ;

    lookAt.x = lookAt.y = lookAt.z = 0.0f;

    up.x = up.z = 0.0f;
    up.y = 1.0f;
    
    // @recomp Force interpolation for this view and skip tracking positions.
    force_camera_interpolation();
    force_camera_ignore_tracking();

    View_LookAt(&this->view, &eye, &lookAt, &up);
    View_Apply(&this->view, VIEW_ALL | VIEW_FORCE_VIEWING | VIEW_FORCE_VIEWPORT | VIEW_FORCE_PROJECTION_PERSPECTIVE);
}

bool should_interpolate_perspective(Vec3f* eye, Vec3f* at) {
    static Vec3f prev_eye = {0,0,0};
    static Vec3f prev_at = {0,0,0};
    static Vec3f eye_velocity = {0,0,0};
    static Vec3f at_velocity = {0,0,0};

    Vec3f predicted_eye;
    Vec3f predicted_at;
    // Predict the new eye and at positions based on the previous velocity and positions.
    Math_Vec3f_Sum(&prev_eye, &eye_velocity, &predicted_eye);
    Math_Vec3f_Sum(&prev_at, &at_velocity, &predicted_at);

    // Calculate the current velocities from the previous and current positions.
    Math_Vec3f_Diff(eye, &prev_eye, &eye_velocity);
    Math_Vec3f_Diff(at, &prev_at, &at_velocity);

    // Compare the predicted positions to the real positions.
    float eye_dist = Math_Vec3f_DistXYZ(&predicted_eye, eye);
    float at_dist = Math_Vec3f_DistXYZ(&predicted_at, at);

    // Compare the velocities of the eye and at positions.
    float velocity_diff = Math_Vec3f_DistXYZ(&eye_velocity, &at_velocity);

    // Update the tracking for the previous positions with the new ones.
    prev_eye = *eye;
    prev_at = *at;

    // These numbers are all picked via testing.

    // If the velocity of both positions was the same, then they're moving together and should interpolate.
    if (velocity_diff <= 3.0f && eye_dist <= 100.0f && at_dist <= 100.0f) {
        return true;
    }

    // If the focus or position are basically the same across frames and the eye didn't move too far then it should probably be interpolated.
    if (at_dist <= 10.0f && eye_dist <= 300.0f) {
        return true;
    }
    if (eye_dist <= 10.0f && at_dist <= 300.0f) {
        return true;
    }

    if (velocity_diff > 50.0f || at_dist > 50.0f || eye_dist > 300.0f) {
        eye_velocity.x = 0.0f;
        eye_velocity.y = 0.0f;
        eye_velocity.z = 0.0f;
        at_velocity.x = 0.0f;
        at_velocity.y = 0.0f;
        at_velocity.z = 0.0f;
        return false;
    }

    return true;
}

/**
 * Apply view to POLY_OPA_DISP, POLY_XLU_DISP (and OVERLAY_DISP if ortho)
 */
RECOMP_PATCH void View_Apply(View* view, s32 mask) {
    mask = (view->flags & mask) | (mask >> 4);
    
    // @recomp Determine if the camera should be interpolated this frame.
    bool interpolate_camera = false;

    if (mask & VIEW_PROJECTION_ORTHO) {
        View_ApplyOrtho(view);
    } else {
        View_ApplyPerspective(view);

        // @recomp Determine if interpolation should occur based on the new eye and at positions.
        if (!camera_ignore_tracking) {
            interpolate_camera = should_interpolate_perspective(&view->eye, &view->at);
        }
    }
    camera_ignore_tracking = false;

    // Force skipping interpolation if the previous view was kaleido and this one isn't.
    if (prev_in_kaleido && !in_kaleido) {
        camera_skip_interpolation_forced = true;
    }

    // @recomp Apply camera interpolation overrides.
    if (camera_skip_interpolation_forced) {
        interpolate_camera = false;
    }
    else if (camera_interpolation_forced) {
        interpolate_camera = true;
    }

    // @recomp Tag the camera matrices
    GraphicsContext* gfxCtx = view->gfxCtx;
    OPEN_DISPS(gfxCtx);

    if (interpolate_camera) {
        // Simple interpolation works much better for cameras because they orbit around a focus.
        gEXMatrixGroupSimple(POLY_OPA_DISP++, CAMERA_TRANSFORM_ID, G_EX_NOPUSH, G_MTX_PROJECTION,
            G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_ORDER_LINEAR, G_EX_EDIT_NONE,
            G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE);
        gEXMatrixGroupSimple(POLY_XLU_DISP++, CAMERA_TRANSFORM_ID, G_EX_NOPUSH, G_MTX_PROJECTION,
            G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE, G_EX_ORDER_LINEAR, G_EX_EDIT_NONE,
            G_EX_COMPONENT_INTERPOLATE, G_EX_COMPONENT_INTERPOLATE);
    }
    else {
        // Skip interpolation for this frame.
        gEXMatrixGroupSimple(POLY_OPA_DISP++, CAMERA_TRANSFORM_ID, G_EX_NOPUSH, G_MTX_PROJECTION,
            G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP, G_EX_COMPONENT_INTERPOLATE, G_EX_ORDER_LINEAR, G_EX_EDIT_NONE,
            G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP);
        gEXMatrixGroupSimple(POLY_XLU_DISP++, CAMERA_TRANSFORM_ID, G_EX_NOPUSH, G_MTX_PROJECTION,
            G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP, G_EX_COMPONENT_INTERPOLATE, G_EX_ORDER_LINEAR, G_EX_EDIT_NONE,
            G_EX_COMPONENT_SKIP, G_EX_COMPONENT_SKIP);
    }

    // Record whether the camera was skipped for later use.
    set_camera_skipped(!interpolate_camera);

    camera_interpolation_forced = false;
    camera_skip_interpolation_forced = false;

    CLOSE_DISPS(gfxCtx);
}

typedef s32 (*CameraUpdateFunc)(Camera*);

typedef struct {
    /* 0x0 */ s16 val;
    /* 0x2 */ s16 param;
} CameraModeValue; // size = 0x4

typedef struct {
    /* 0x0 */ s16 funcId;
    /* 0x2 */ s16 numValues;
    /* 0x4 */ CameraModeValue* values;
} CameraMode; // size = 0x8

typedef struct {
    /* 0x0 */ u32 validModes;
    /* 0x4 */ u32 flags;
    /* 0x8 */ CameraMode* cameraModes;
} CameraSetting; // size = 0xC

extern CameraSetting sCameraSettings[];
extern s32 sCameraInterfaceFlags;

s32 Camera_BgCheck(Camera* camera, Vec3f* from, Vec3f* to);
Vec3s* Camera_GetBgCamOrActorCsCamFuncData(Camera* camera, u32 camDataId);
f32 Camera_GetFocalActorHeight(Camera* camera);
f32 Camera_ScaledStepToCeilF(f32 target, f32 cur, f32 stepScale, f32 minDiff);
s16 Camera_ScaledStepToCeilS(s16 target, s16 cur, f32 stepScale, s16 minDiff);
void Camera_ScaledStepToCeilVec3f(Vec3f* target, Vec3f* cur, f32 xzStepScale, f32 yStepScale, f32 minDiff);
void Camera_SetFocalActorAtOffset(Camera* camera, Vec3f* focalActorPos);
void Camera_SetUpdateRatesSlow(Camera* camera);
Vec3f Camera_Vec3sToVec3f(Vec3s* src);

/**
 * Used for many fixed-based camera settings i.e. camera is fixed in rotation, and often position (but not always)
 */
// @recomp Modified to not force interpolation while panning.
RECOMP_PATCH s32 Camera_Fixed1(Camera* camera) {
    s32 pad[2];
    s32 yawDiff;
    VecGeo eyeOffset;
    VecGeo eyeAtOffset;
    VecGeo sp7C;
    u32 negOne;
    Vec3f adjustedPos;
    BgCamFuncData* bgCamFuncData;
    Vec3f* eye = &camera->eye;
    Vec3f* at = &camera->at;
    PosRot* focalActorPosRot = &camera->focalActorPosRot;
    f32 focalActorHeight = Camera_GetFocalActorHeight(camera);
    CameraModeValue* values;
    PosRot* targetHome;
    PosRot* targetWorld;
    VecGeo sp44;
    Fixed1ReadOnlyData* roData = &camera->paramData.fixd1.roData;
    Fixed1ReadWriteData* rwData = &camera->paramData.fixd1.rwData;

    sp7C = OLib_Vec3fDiffToVecGeo(at, eye);

    if (!RELOAD_PARAMS(camera)) {
    } else {
        values = sCameraSettings[camera->setting].cameraModes[camera->mode].values;
        bgCamFuncData = (BgCamFuncData*)Camera_GetBgCamOrActorCsCamFuncData(camera, camera->bgCamIndex);
        rwData->eyePosRotTarget.pos = Camera_Vec3sToVec3f(&bgCamFuncData->pos);

        rwData->eyePosRotTarget.rot = bgCamFuncData->rot;
        rwData->fov = bgCamFuncData->fov;
        rwData->focalActor = camera->focalActor;

        roData->unk_00 = GET_NEXT_SCALED_RO_DATA(values) * focalActorHeight;
        roData->unk_04 = GET_NEXT_SCALED_RO_DATA(values);
        roData->fov = GET_NEXT_RO_DATA(values);
        roData->interfaceFlags = GET_NEXT_RO_DATA(values);

        if (roData->interfaceFlags & FIXED1_FLAG_4) {
            if (camera->target == NULL) {
                return false;
            }

            targetHome = &camera->target->home;
            targetWorld = &camera->target->world;

            sp44 = OLib_Vec3fDiffToVecGeo(&targetHome->pos, &rwData->eyePosRotTarget.pos);
            sp44.yaw = targetWorld->rot.y + (s16)(sp44.yaw - targetHome->rot.y);
            rwData->eyePosRotTarget.pos = OLib_AddVecGeoToVec3f(&targetWorld->pos, &sp44);
            yawDiff = (s16)(rwData->eyePosRotTarget.rot.y - targetHome->rot.y);
            rwData->eyePosRotTarget.rot.y = targetWorld->rot.y + yawDiff;
        }
    }

    negOne = -1;

    if (rwData->focalActor != camera->focalActor) {
        camera->animState = 20;
    }

    if (rwData->fov == (s32)negOne) {
        rwData->fov = roData->fov * 100;
    } else if (rwData->fov <= 360) {
        rwData->fov *= 100;
    }

    sCameraInterfaceFlags = roData->interfaceFlags;

    if (camera->animState == 0) {
        camera->animState++;
        Camera_SetUpdateRatesSlow(camera);
        if (rwData->fov != (s32)negOne) {
            roData->fov = CAM_RODATA_UNSCALE(rwData->fov);
        }

        if (bgCamFuncData->unk_0E != (s32)negOne) {
            roData->unk_04 = CAM_RODATA_UNSCALE(bgCamFuncData->unk_0E);
        }
    }

    // @recomp Camera interpolation should always apply on this mode unless something else modified it externally.
    // We check for the approach percentage as well to detect when it wants to force an interpolation to the next position.
    static Vec3f lastEye = {};
    static Vec3f lastAt = {};
    if (OLib_Vec3fDist(eye, &lastEye) < 1e-6f && OLib_Vec3fDist(at, &lastAt) < 1e-6f && (roData->unk_04 < 0.999f)) {
        force_camera_interpolation();
    }

    eyeAtOffset = OLib_Vec3fDiffToVecGeo(eye, at);
    Camera_ScaledStepToCeilVec3f(&rwData->eyePosRotTarget.pos, eye, roData->unk_04, roData->unk_04, 0.2f);
    adjustedPos = focalActorPosRot->pos;
    adjustedPos.y += focalActorHeight;
    camera->dist = OLib_Vec3fDist(&adjustedPos, eye);
    eyeOffset.r = camera->dist;
    eyeOffset.pitch =
        Camera_ScaledStepToCeilS(rwData->eyePosRotTarget.rot.x * -1, eyeAtOffset.pitch, roData->unk_04, 5);
    eyeOffset.yaw = Camera_ScaledStepToCeilS(rwData->eyePosRotTarget.rot.y, eyeAtOffset.yaw, roData->unk_04, 5);
    *at = OLib_AddVecGeoToVec3f(eye, &eyeOffset);
    camera->eyeNext = *eye;
    Camera_BgCheck(camera, eye, at);

    camera->fov = Camera_ScaledStepToCeilF(roData->fov, camera->fov, roData->unk_04, 0.1f);
    camera->roll = 0;
    camera->atLerpStepScale = 0.0f;
    Camera_SetFocalActorAtOffset(camera, &focalActorPosRot->pos);
    camera->roll = Camera_ScaledStepToCeilS(rwData->eyePosRotTarget.rot.z, camera->roll, roData->unk_04, 5);

    // @recomp
    lastEye = *eye;
    lastAt = *at;

    return 1;
}
