import type { Quaternion, Vector2, Vector3, Vector4 } from "three";

import type { Vec3 } from "./engine_types.js";

declare type Vector = Vector3 | Vector4 | Vector2 | Quaternion;

/**
 * Math utility class providing common mathematical operations.  
 * Access via the exported `Mathf` constant. 
 *
 * @example
 * ```ts
 * import { Mathf } from "@needle-tools/engine";
 *
 * // Random number between 0 and 10
 * const rand = Mathf.random(0, 10);
 *
 * // Clamp a value
 * const clamped = Mathf.clamp(value, 0, 100);
 *
 * // Smooth interpolation
 * const smoothed = Mathf.lerp(start, end, t);
 * ```
 */
class MathHelper {

    /**
     * Returns a random number or element.
     * @param arr Array to pick a random element from
     * @returns Random element from array, or null if array is empty
     * @example `Mathf.random([1, 2, 3])` - returns random element
     */
    random<T>(arr: Array<T>): T | null;
    /**
     * Returns a random number between min and max (inclusive).
     * @param min Minimum value (inclusive)
     * @param max Maximum value (inclusive)
     * @returns Random number in range, or 0-1 if no args provided
     * @example `Mathf.random(0, 10)` - returns 0 to 10
     */
    random(min?: number, max?: number): number;
    random<T>(arrayOrMin?: number | Array<T>, max?: number): number | T | null {
        if (Array.isArray(arrayOrMin)) {
            if(arrayOrMin.length <= 0) return null;
            return arrayOrMin[Math.floor(Math.random() * arrayOrMin.length)];
        }
        else {
            if (arrayOrMin !== undefined && max !== undefined) {
                return Math.random() * (max - arrayOrMin) + arrayOrMin;
            }
        }
        return Math.random();
    }

    /**
     * Fills a Vector3 with random values.
     * @param target Vector3 to fill with random values
     * @param min Minimum value for each component
     * @param max Maximum value for each component
     */
    randomVector3(target: Vector3, min: number = 0, max: number = 1) {
        target.x = this.random(min, max);
        target.y = this.random(min, max);
        target.z = this.random(min, max);
    }

    /**
     * Clamps a value between min and max.
     * @param value Value to clamp
     * @param min Minimum bound
     * @param max Maximum bound
     * @returns Clamped value
     */
    clamp(value: number, min: number, max: number) {

        if (value < min) {
            return min;
        }
        else if (value > max) {
            return max;
        }

        return value;
    }

    /**
     * Clamps a value between 0 and 1.
     * @param value Value to clamp
     * @returns Value clamped to [0, 1]
     */
    clamp01(value: number) {
        return this.clamp(value, 0, 1);
    }

    /**
     * Linearly interpolates between two values.
     * @param value1 Start value (returned when t=0)
     * @param value2 End value (returned when t=1)
     * @param t Interpolation factor, clamped to [0, 1]
     * @returns Interpolated value
     */
    lerp(value1: number, value2: number, t: number) {
        t = t < 0 ? 0 : t;
        t = t > 1 ? 1 : t;
        return value1 + (value2 - value1) * t;
    }

    /**
     * Calculates the linear interpolation parameter that produces the given value.
     * Inverse of lerp: if `lerp(a, b, t) = v`, then `inverseLerp(a, b, v) = t`
     * @param value1 Start value
     * @param value2 End value
     * @param t The value to find the parameter for
     * @returns The interpolation parameter (may be outside [0,1] if t is outside [value1, value2])
     */
    inverseLerp(value1: number, value2: number, t: number) {
        return (t - value1) / (value2 - value1);
    }

    /**
     * Remaps a value from one range to another.
     * @param value The value to remap.
     * @param min1 The minimum value of the current range.
     * @param max1 The maximum value of the current range.
     * @param min2 The minimum value of the target range.
     * @param max2 The maximum value of the target range.
     */
    remap(value: number, min1: number, max1: number, min2: number, max2: number) {
        return min2 + (max2 - min2) * (value - min1) / (max1 - min1);
    }

    /**
     * Moves a value towards a target by a maximum step amount.
     * Useful for smooth following or gradual value changes.
     * @param value1 Current value
     * @param value2 Target value
     * @param amount Maximum step to move (positive moves toward target)
     * @returns New value moved toward target, never overshooting
     */
    moveTowards(value1: number, value2: number, amount: number) {
        value1 += amount;
        if (amount < 0 && value1 < value2) value1 = value2;
        else if (amount > 0 && value1 > value2) value1 = value2;
        return value1;
    }



    readonly Rad2Deg = 180 / Math.PI;
    readonly Deg2Rad = Math.PI / 180;
    readonly Epsilon = 0.00001;
    /**
     * Converts radians to degrees
     */
    toDegrees(radians: number) {
        return radians * 180 / Math.PI;
    }
    /**
     * Converts degrees to radians
     */
    toRadians(degrees: number) {
        return degrees * Math.PI / 180;
    }

    tan(radians: number) {
        return Math.tan(radians);
    }

    gammaToLinear(gamma: number) {
        return Math.pow(gamma, 2.2);
    }

    linearToGamma(linear: number) {
        return Math.pow(linear, 1 / 2.2);
    }

    /**
     * Checks if two vectors are approximately equal within epsilon tolerance.
     * Works with Vector2, Vector3, Vector4, and Quaternion.
     * @param v1 First vector
     * @param v2 Second vector
     * @param epsilon Tolerance for comparison (default: Number.EPSILON)
     * @returns True if all components are within epsilon of each other
     */
    approximately(v1: Vector, v2: Vector, epsilon = Number.EPSILON) {
        for (const key of vectorKeys) {
            const a = v1[key];
            const b = v2[key];
            if (a === undefined || b === undefined) break;
            const diff = Math.abs(a - b);
            if (diff > epsilon) {
                return false;
            }
        }
        return true;
    }

    /**
     * Easing function: slow start, fast middle, slow end (cubic).
     * @param x Input value from 0 to 1
     * @returns Eased value from 0 to 1
     */
    easeInOutCubic(x: number) {
        return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
    }
};

const vectorKeys = ["x", "y", "z", "w"]

export const Mathf = new MathHelper();


class LowPassFilter {
    y: number | null;
    s: number | null;
    alpha = 0;

    constructor(alpha: number) {
        this.setAlpha(alpha);
        this.y = null;
        this.s = null;
    }

    setAlpha(alpha: number) {
        if (alpha <= 0 || alpha > 1.0) {
            throw new Error();
        }
        this.alpha = alpha;
    }

    filter(value: number, alpha: number) {
        if (alpha) {
            this.setAlpha(alpha);
        }
        let s: number;
        if (!this.y) {
            s = value;
        } else {
            s = this.alpha * value + (1.0 - this.alpha) * this.s!;
        }
        this.y = value;
        this.s = s;
        return s;
    }

    lastValue() {
        return this.y;
    }

    reset(value: number) {
        this.y = value;
        this.s = value;
    }
}

/**
 * [OneEuroFilter](https://engine.needle.tools/docs/api/OneEuroFilter) is a low-pass filter designed to reduce jitter in noisy signals while maintaining low latency.
 * It's particularly useful for smoothing tracking data from XR controllers, hand tracking, or other input devices where the signal contains noise but responsiveness is important.
 *
 * The filter automatically adapts its smoothing strength based on the signal's velocity:
 * - When the signal moves slowly, it applies strong smoothing to reduce jitter
 * - When the signal moves quickly, it reduces smoothing to maintain responsiveness
 *
 * Based on the research paper: [1€ Filter: A Simple Speed-based Low-pass Filter for Noisy Input](http://cristal.univ-lille.fr/~casiez/1euro/)
 *
 * @example Basic usage with timestamp
 * ```ts
 * const filter = new OneEuroFilter(120, 1.0, 0.0);
 *
 * // In your update loop:
 * const smoothedValue = filter.filter(noisyValue, this.context.time.time);
 * ```
 *
 * @example Without timestamps (using frequency estimate)
 * ```ts
 * // Assuming 60 FPS update rate
 * const filter = new OneEuroFilter(60, 1.0, 0.5);
 *
 * // Call without timestamp - uses the frequency estimate
 * const smoothedValue = filter.filter(noisyValue);
 * ```
 *
 * @example Smoothing 3D positions
 * ```ts
 * const posFilter = new OneEuroFilterXYZ(90, 0.5, 0.0);
 *
 * posFilter.filter(trackedPosition, smoothedPosition, this.context.time.time);
 * ```
 *
 * @see {@link OneEuroFilterXYZ} for filtering 3D vectors
 */
export class OneEuroFilter {
    /**
     * An estimate of the frequency in Hz of the signal (> 0), if timestamps are not available.
     */
    freq: number;
    /**
     * Min cutoff frequency in Hz (> 0). Lower values allow to remove more jitter.
     */
    minCutOff: number;
    /**
     * Parameter to reduce latency (> 0). Higher values make the filter react faster to changes.
     */
    beta: number;
    /**
     * Used to filter the derivates. 1 Hz by default. Change this parameter if you know what you are doing.
     */
    dCutOff: number;
    /**
     * The low-pass filter for the signal.
     */
    x: LowPassFilter;
    /**
     * The low-pass filter for the derivates.
     */
    dx: LowPassFilter;
    /**
     * The last time the filter was called.
     */
    lasttime: number | null;

    /** Create a new OneEuroFilter
     * @param freq - An estimate of the frequency in Hz of the signal (> 0), if timestamps are not available.
     * @param minCutOff - Min cutoff frequency in Hz (> 0). Lower values allow to remove more jitter.
     * @param beta - Parameter to reduce latency (> 0). Higher values make the filter react faster to changes.
     * @param dCutOff - Used to filter the derivates. 1 Hz by default. Change this parameter if you know what you are doing.
     */
    constructor(freq: number, minCutOff = 1.0, beta = 0.0, dCutOff = 1.0) {
        if (freq <= 0 || minCutOff <= 0 || dCutOff <= 0) {
            throw new Error();
        }
        this.freq = freq;
        this.minCutOff = minCutOff;
        this.beta = beta;
        this.dCutOff = dCutOff;
        this.x = new LowPassFilter(this.alpha(this.minCutOff));
        this.dx = new LowPassFilter(this.alpha(this.dCutOff));
        this.lasttime = null;
    }

    alpha(cutOff: number) {
        const te = 1.0 / this.freq;
        const tau = 1.0 / (2 * Math.PI * cutOff);
        return 1.0 / (1.0 + tau / te);
    }

    /** Filter your value: call with your value and the current timestamp (e.g. from this.context.time.time) */
    filter(x: number, time: number | null = null) {
        if (this.lasttime && time) {
            this.freq = 1.0 / (time - this.lasttime);
        }
        this.lasttime = time;
        const prevX = this.x.lastValue();
        const dx = !prevX ? 0.0 : (x - prevX) * this.freq;
        const edx = this.dx.filter(dx, this.alpha(this.dCutOff));
        const cutOff = this.minCutOff + this.beta * Math.abs(edx);
        return this.x.filter(x, this.alpha(cutOff));
    }

    reset(x?: number) {
        if (x != undefined) this.x.reset(x);
        this.x.alpha = this.alpha(this.minCutOff);
        this.dx.alpha = this.alpha(this.dCutOff);
        this.lasttime = null;
    }
}

export class OneEuroFilterXYZ {
    readonly x: OneEuroFilter;
    readonly y: OneEuroFilter;
    readonly z: OneEuroFilter;

    /** Create a new OneEuroFilter
     * @param freq - An estimate of the frequency in Hz of the signal (> 0), if timestamps are not available.
     * @param minCutOff - Min cutoff frequency in Hz (> 0). Lower values allow to remove more jitter.
     * @param beta - Parameter to reduce latency (> 0). Higher values make the filter react faster to changes.
     * @param dCutOff - Used to filter the derivates. 1 Hz by default. Change this parameter if you know what you are doing.
     */
    constructor(freq: number, minCutOff = 1.0, beta = 0.0, dCutOff = 1.0) {
        this.x = new OneEuroFilter(freq, minCutOff, beta, dCutOff);
        this.y = new OneEuroFilter(freq, minCutOff, beta, dCutOff);
        this.z = new OneEuroFilter(freq, minCutOff, beta, dCutOff);
    }

    filter(value: Vec3, target: Vec3, time: number | null = null) {
        target.x = this.x.filter(value.x, time);
        target.y = this.y.filter(value.y, time);
        target.z = this.z.filter(value.z, time);
    }
    reset(value?: Vec3) {
        this.x.reset(value?.x);
        this.y.reset(value?.y);
        this.z.reset(value?.z);
    }
}