import { Matrix4 } from "three";
import { assert } from "../../../../../core/assert.js";
import { eulerAnglesFromMatrix } from "../../../../../core/geom/3d/mat4/eulerAnglesFromMatrix.js";
import { normalize_angle_rad } from "../../../../../core/geom/normalize_angle_rad.js";
import Quaternion from "../../../../../core/geom/Quaternion.js";
import Vector3 from '../../../../../core/geom/Vector3.js';
import { lerp } from "../../../../../core/math/lerp.js";
import { max2 } from "../../../../../core/math/max2.js";
import { sign_not_zero } from "../../../../../core/math/sign_not_zero.js";

const m4 = new Matrix4();
const angles = [];

const panOffset = new Vector3();


/**
 *
 * @param {number} displacement_x
 * @param {number} displacement_y
 * @param {TopDownCameraController} input source thing being rotated
 * @param {TopDownCameraController} output target thing where the result is written to
 */
export function rotate_from_view(displacement_x, displacement_y, input, output) {
    // crop yaw and pitch to safe range of -PI to PI
    const i_pitch = normalize_angle_rad(input.pitch);
    const i_yaw = normalize_angle_rad(input.yaw);
    const i_roll = normalize_angle_rad(input.roll);

    const quaternion = Quaternion.fromEulerAngles(i_pitch, i_yaw, i_roll);
    const forward = Vector3.up.clone();
    forward.applyQuaternion(quaternion);

    // flip rotation axis when pitch is over 90deg in either direction, that is UP vector is pointing down
    const d_yaw = sign_not_zero(forward.y) * displacement_x;

    output.yaw = i_yaw + d_yaw;
    output.pitch = i_pitch - displacement_y;
}

/**
 * pass in distance in world space to move left
 * @param {number} distance
 * @param {Object3D} object
 * @param {Vector3} result
 * @param {Vector3} multiplier
 */
function panLeft(distance, object, result, multiplier) {

    const te = object.matrix.elements;
    // get X column of matrix
    panOffset.set(te[0], te[1], te[2]);
    panOffset.multiplyScalar(-distance);

    //result.add(panOffset);
    result._sub(panOffset.x * multiplier.x, panOffset.y * multiplier.y, panOffset.z * multiplier.z);

}

/**
 * pass in distance in world space to move up
 * @param {number} distance
 * @param {Object3D} object
 * @param {Vector3} result
 * @param {Vector3} multiplier
 */
function panUp(distance, object, result, multiplier) {

    const te = object.matrix.elements;
    // get Y column of matrix
    panOffset.set(te[4], te[5], te[6]);
    panOffset.multiplyScalar(distance);

    //result.add(panOffset);
    result._sub(panOffset.x * multiplier.x, panOffset.y * multiplier.y, panOffset.z * multiplier.z);
}

const deg2_in_rad = Math.PI / 360;

/**
 * main entry point; pass in Vector2 of change desired in pixel space, right and down are positive
 * @param {Vector2} delta
 * @param {Object3D} object
 * @param {Element} element
 * @param {number} targetDistance
 * @param {number} fov
 * @param {Vector3} result
 * @param {Vector3} multiplier
 */
function pan(delta, object, element, targetDistance, fov, result, multiplier = Vector3.one) {
    assert.isNumber(fov, 'fov');

    // half of the fov is center to top of screen
    const d = targetDistance * Math.tan(fov * deg2_in_rad);

    // NOTE: we actually don't use screenWidth, since perspective camera is fixed to screen height
    const m = 2 * d / element.clientHeight;

    panLeft(delta.x * m, object, result, multiplier);
    panUp(delta.y * m, object, result, multiplier);
}


class TopDownCameraController {
    constructor(options) {
        this.target = new Vector3();

        this.pitch = 0;
        this.yaw = 0;
        this.roll = 0;

        this.distance = 0;
        this.distanceMin = 1;
        this.distanceMax = 200;

        //
        if (options !== void 0) {
            this.fromJSON(options);
        }
    }

    /**
     * Set target, rotation and distance from 2 vectors
     * @param {Vector3} eye
     * @param {Vector3} target
     * @param {Vector3} [up]
     */
    setFromEyeAndTarget(eye, target, up = Vector3.up) {

        /*
        NOTE: I'm not sure why, but three.js camera points in the opposite direction to normal objects
        See: https://github.com/mrdoob/three.js/blob/412b99a7f26e117ea97f40eb53d010ab81aa3279/src/core/Object3D.js#L282
         */
        m4.lookAt(target, eye, up);

        eulerAnglesFromMatrix(angles, m4.elements, 2, 1, 0)

        this.pitch = angles[2];
        this.yaw = angles[1];
        this.roll = angles[0];

        this.distance = eye.distanceTo(target);

        // ensure we can maintain this distance
        this.distanceMax = max2(this.distance, this.distanceMax);

        this.target.copy(target);
    }

    /**
     *
     * @param {TopDownCameraController} other
     * @returns {boolean}
     */
    equals(other) {
        return this.target.equals(other.target)
            && this.pitch === other.pitch
            && this.yaw === other.yaw
            && this.roll === other.roll
            && this.distance === other.distance
            && this.distanceMin === other.distanceMin
            && this.distanceMax === other.distanceMax
            ;
    }

    /**
     *
     * @param {TopDownCameraController} other
     */
    copy(other) {
        this.target.copy(other.target);

        this.pitch = other.pitch;
        this.yaw = other.yaw;
        this.roll = other.roll;

        this.distance = other.distance;
        this.distanceMax = other.distanceMax;
        this.distanceMin = other.distanceMin;
    }

    /**
     *
     * @returns {TopDownCameraController}
     */
    clone() {
        const r = new TopDownCameraController();

        r.copy(this);

        return r;
    }

    /**
     *
     * @param {TopDownCameraController} a
     * @param {TopDownCameraController} b
     * @param {number} t
     */
    lerp(a, b, t) {
        this.target.lerpVectors(a.target, b.target, t);

        this.pitch = lerp(a.pitch, b.pitch, t);
        this.yaw = lerp(a.yaw, b.yaw, t);
        this.roll = lerp(a.roll, b.roll, t);

        this.distance = lerp(a.distance, b.distance, t);

        this.distanceMin = lerp(a.distanceMin, b.distanceMin, t);
        this.distanceMax = lerp(a.distanceMax, b.distanceMax, t);
    }

    toJSON() {
        return {
            target: this.target.toJSON(),

            pitch: this.pitch,
            yaw: this.yaw,
            roll: this.roll,

            distance: this.distance,
            distanceMin: this.distanceMin,
            distanceMax: this.distanceMax
        };
    }

    fromJSON(
        {
            distance = 0,
            distanceMin = 0,
            distanceMax = 0,
            pitch = 0,
            yaw = 0,
            roll = 0,
            target = Vector3.zero,
        }
    ) {

        this.distance = distance;
        this.distanceMin = distanceMin;
        this.distanceMax = distanceMax;

        this.pitch = pitch;
        this.roll = roll;
        this.yaw = yaw;

        this.target.fromJSON(target);

    }

    static fromJSON(j) {
        const r = new TopDownCameraController();

        r.fromJSON(j);

        return r;
    }
}

TopDownCameraController.typeName = "TopDownCameraController";

TopDownCameraController.pan = pan;
TopDownCameraController.rotate = rotate_from_view;

export default TopDownCameraController;


