
import { BufferGeometry, CatmullRomCurve3, CubicBezierCurve3, Curve, Line, LineBasicMaterial,LineCurve3, Object3D, Quaternion, Vector3 } from "three";

import { Mathf } from "../../engine/engine_math.js";
import { serializeable } from "../../engine/engine_serialization.js";
import { getParam } from "../../engine/engine_utils.js";
import { Behaviour } from "../Component.js";
import type { SplineWalker } from "./index.js";

const debug = getParam("debugsplines");

/**
 * Represents a single knot (control point) in a spline curve.  
 *
 * Each knot defines a point along the spline with its position, rotation, and tangent handles  
 * that control the curve's shape entering and leaving the knot.  
 *
 * **Properties:**
 * - **position**: The 3D position of this knot in local space
 * - **rotation**: The orientation at this knot (useful for rotating objects along the spline)
 * - **tangentIn**: The incoming tangent handle controlling the curve shape before this knot
 * - **tangentOut**: The outgoing tangent handle controlling the curve shape after this knot
 *
 * @see {@link SplineContainer} for the container that holds and manages multiple knots
 */
export class SplineData {
    /**
     * The 3D position of this knot in local space relative to the SplineContainer.
     */
    @serializeable(Vector3)
    position: Vector3 = new Vector3();

    /**
     * The orientation at this knot. Can be used to rotate objects following the spline.
     */
    @serializeable(Quaternion)
    rotation: Quaternion = new Quaternion();

    /**
     * The incoming tangent handle controlling the curve shape as it approaches this knot.
     * The magnitude and direction affect the smoothness and curvature of the spline.
     */
    @serializeable(Vector3)
    tangentIn: Vector3 = new Vector3();

    /**
     * The outgoing tangent handle controlling the curve shape as it leaves this knot.
     * The magnitude and direction affect the smoothness and curvature of the spline.
     */
    @serializeable(Vector3)
    tangentOut: Vector3 = new Vector3();
}

// enum SplineTypeEnum {
//     CatmullRom = 0,
//     Bezier = 1,
//     Linear = 2
// }
// type SplineType = "CatmullRom" | "Bezier" | "Linear";


//@dont-generate-component
/**
 * [SplineContainer](https://engine.needle.tools/docs/api/SplineContainer) manages spline curves defined by a series of knots (control points).  
 * This component stores spline data and generates smooth curves that can be used for animation paths, camera paths, racing tracks, or any curved path in 3D space.
 *
 * ![](https://cloud.needle.tools/-/media/XIHaiNFsA1IbMZVJepp1aQ.gif)
 *
 * **How It Works:**
 * The spline is defined by an array of {@link SplineData} knots. Each knot contains:  
 * - **Position**: The location of the control point  
 * - **Rotation**: Orientation at that point (useful for banking/tilting objects along the path)  
 * - **Tangents**: Handles that control the curve's smoothness and shape  
 *
 * The component uses Catmull-Rom interpolation to create smooth curves between knots. The curve is automatically  
 * rebuilt when knots are added, removed, or marked dirty, and all sampling methods return positions in world space.
 *
 * **Key Features:**  
 * - Smooth Catmull-Rom curve interpolation  
 * - Support for open and closed curves  
 * - Dynamic knot management (add/remove at runtime)  
 * - World-space sampling with {@link getPointAt} and {@link getTangentAt}  
 * - Automatic curve regeneration when modified
 * - Built-in debug visualization
 * - Integrates seamlessly with {@link SplineWalker}
 *
 * **Common Use Cases:**
 * - Camera paths and cinematics
 * - Object movement along curved paths
 * - Racing game tracks and racing lines
 * - Character patrol routes
 * - Procedural road/path generation
 * - Animation curves for complex motion
 * - Cable/rope visualization
 *
 * @example Basic spline setup with knots
 * ```ts
 * const splineObj = new Object3D();
 * const spline = splineObj.addComponent(SplineContainer);
 *
 * // Add knots to define the path
 * spline.addKnot({ position: new Vector3(0, 0, 0) });
 * spline.addKnot({ position: new Vector3(2, 1, 0) });
 * spline.addKnot({ position: new Vector3(4, 0, 2) });
 * spline.addKnot({ position: new Vector3(6, -1, 1) });
 *
 * // Sample a point halfway along the spline
 * const midpoint = spline.getPointAt(0.5);
 * console.log("Midpoint:", midpoint);
 * ```
 *
 * @example Creating a closed loop spline
 * ```ts
 * const loopSpline = gameObject.addComponent(SplineContainer);
 * loopSpline.closed = true; // Makes the spline loop back to the start
 *
 * // Add circular path knots
 * for (let i = 0; i < 8; i++) {
 *   const angle = (i / 8) * Math.PI * 2;
 *   const pos = new Vector3(Math.cos(angle) * 5, 0, Math.sin(angle) * 5);
 *   loopSpline.addKnot({ position: pos });
 * }
 * ```
 *
 * @example Sampling points along a spline
 * ```ts
 * const spline = gameObject.getComponent(SplineContainer);
 *
 * // Sample 10 points along the spline
 * const points: Vector3[] = [];
 * for (let i = 0; i <= 10; i++) {
 *   const t = i / 10; // 0 to 1
 *   const point = spline.getPointAt(t);
 *   points.push(point);
 * }
 *
 * // Get tangent (direction) at 75% along the spline
 * const tangent = spline.getTangentAt(0.75);
 * console.log("Direction at 75%:", tangent);
 * ```
 *
 * @example Dynamic knot manipulation
 * ```ts
 * const spline = gameObject.getComponent(SplineContainer);
 *
 * // Add a new knot dynamically
 * const newKnot = new SplineData();
 * newKnot.position.set(10, 5, 0);
 * spline.addKnot(newKnot);
 *
 * // Remove the first knot
 * spline.removeKnot(0);
 *
 * // Modify existing knot
 * spline.spline[1].position.y += 2;
 * spline.markDirty(); // Tell the spline to rebuild
 * ```
 *
 * @example Using with SplineWalker for animation
 * ```ts
 * // Set up spline path
 * const spline = pathObject.addComponent(SplineContainer);
 * spline.addKnot({ position: new Vector3(0, 0, 0) });
 * spline.addKnot({ position: new Vector3(5, 2, 5) });
 * spline.addKnot({ position: new Vector3(10, 0, 0) });
 *
 * // Make object follow the spline
 * const walker = movingObject.addComponent(SplineWalker);
 * walker.spline = spline;
 * walker.speed = 2; // Units per second
 * walker.loop = true;
 * ```
 *
 * **Debug Visualization:**
 * Add `?debugsplines` to your URL to enable debug visualization, which draws the spline curve as a purple line.
 * You can also enable it programmatically:
 * ```ts
 * spline.debug = true; // Show debug visualization
 * ```
 *
 * @see {@link SplineWalker} - Component for moving objects along a spline path
 * @see {@link SplineData} - The knot data structure used to define spline points
 * @see {@link getPointAt} - Sample positions along the spline
 * @see {@link getTangentAt} - Get direction vectors along the spline
 * @see {@link addKnot} - Add control points to the spline
 * @see {@link removeKnot} - Remove control points from the spline
 *
 * @summary Manages smooth spline curves defined by control point knots
 * @category Splines
 * @group Components
 * @component
 */
// #region SplineContainer
export class SplineContainer extends Behaviour {

    /**
     * Adds a knot (control point) to the end of the spline.
     *
     * You can pass either a full {@link SplineData} object or a simple object with just a position.
     * When passing a simple object, default values are used for rotation and tangents.
     *
     * The spline curve is automatically marked dirty and will be rebuilt on the next update.
     *
     * @param knot - Either a SplineData object or an object with at least a `position` property
     * @returns This SplineContainer for method chaining
     *
     * @example Add knots with positions only
     * ```ts
     * spline.addKnot({ position: new Vector3(0, 0, 0) })
     *       .addKnot({ position: new Vector3(5, 0, 0) })
     *       .addKnot({ position: new Vector3(5, 0, 5) });
     * ```
     *
     * @example Add a full SplineData knot
     * ```ts
     * const knot = new SplineData();
     * knot.position.set(10, 2, 5);
     * knot.rotation.setFromEuler(new Euler(0, Math.PI / 4, 0));
     * spline.addKnot(knot);
     * ```
     */
    addKnot(knot: SplineData | { position: Vector3 }): SplineContainer {
        if (knot instanceof SplineData) {
            this.spline.push(knot);
            this._isDirty = true;
        }
        else {
            const k = new SplineData();
            k.position.copy(knot.position);
            this.spline.push(k);
            this._isDirty = true;
        }
        return this;
    }

    /**
     * Removes a knot (control point) from the spline.
     *
     * You can remove a knot either by its numeric index in the spline array or by passing
     * a reference to the SplineData object itself.
     *
     * The spline curve is automatically marked dirty and will be rebuilt on the next update.
     *
     * @param index - Either the numeric index of the knot to remove, or the SplineData object reference
     * @returns This SplineContainer for method chaining
     *
     * @example Remove knot by index
     * ```ts
     * spline.removeKnot(0); // Remove first knot
     * spline.removeKnot(spline.spline.length - 1); // Remove last knot
     * ```
     *
     * @example Remove knot by reference
     * ```ts
     * const knotToRemove = spline.spline[2];
     * spline.removeKnot(knotToRemove);
     * ```
     */
    removeKnot(index: number | SplineData): SplineContainer {
        if (typeof index === "number") {
            this.spline.splice(index, 1);
            this._isDirty = true;
        } else {
            const i = this.spline.indexOf(index);
            if (i !== -1) {
                this.spline.splice(i, 1);
                this._isDirty = true;
            }
        }
        return this;
    }

    /**
     * Samples a point on the spline at a given parametric position (in world space).
     *
     * The parameter `t` ranges from 0 to 1, where:
     * - `0` = start of the spline
     * - `0.5` = middle of the spline
     * - `1` = end of the spline
     *
     * The returned position is in world space, accounting for the SplineContainer's transform.
     * Values outside 0-1 are clamped to the valid range.
     *
     * @param to01 - Parametric position along the spline (0 to 1)
     * @param target - Optional Vector3 to store the result (avoids allocation)
     * @returns The world-space position at parameter `t`
     *
     * @example Sample multiple points along the spline
     * ```ts
     * // Sample 20 evenly-spaced points
     * const points: Vector3[] = [];
     * for (let i = 0; i <= 20; i++) {
     *   const t = i / 20;
     *   points.push(spline.getPointAt(t));
     * }
     * ```
     *
     * @example Using a target vector for efficiency
     * ```ts
     * const reusableVector = new Vector3();
     * for (let i = 0; i < 100; i++) {
     *   const point = spline.getPointAt(i / 100, reusableVector);
     *   // Use point...
     * }
     * ```
     *
     * @see {@link getTangentAt} to get the direction at a point
     */
    getPointAt(to01: number, target?: Vector3): Vector3 {
        if (!this.curve) return new Vector3();
        const pos = this.curve.getPointAt(Mathf.clamp01(to01), target);
        const worldMatrix = this.gameObject.matrixWorld ?? undefined;
        if (worldMatrix) {
            pos.applyMatrix4(worldMatrix);
        }
        return pos;
    }

    /**
     * Marks the spline as dirty, causing it to be rebuilt on the next update frame.
     *
     * Call this method whenever you manually modify the spline data (knot positions, rotations, or tangents)
     * to ensure the curve is regenerated. This is done automatically when using {@link addKnot} or {@link removeKnot}.
     *
     * @example Modifying knots and marking dirty
     * ```ts
     * // Modify existing knot positions
     * spline.spline[0].position.y += 2;
     * spline.spline[1].position.x -= 1;
     *
     * // Tell the spline to rebuild
     * spline.markDirty();
     * ```
     *
     * @example Animating knot positions
     * ```ts
     * update() {
     *   const time = this.context.time.time;
     *   // Animate knot positions
     *   for (let i = 0; i < spline.spline.length; i++) {
     *     spline.spline[i].position.y = Math.sin(time + i) * 2;
     *   }
     *   spline.markDirty(); // Rebuild curve each frame
     * }
     * ```
     */
    markDirty() {
        this._isDirty = true;
    }

    /**
     * Samples the tangent (direction) vector on the spline at a given parametric position (in world space).
     *
     * The tangent represents the forward direction of the curve at point `t`. This is useful for:
     * - Orienting objects along the spline (facing the direction of travel)
     * - Calculating banking/tilting for vehicles on the path
     * - Understanding the curve's direction at any point
     *
     * The parameter `t` ranges from 0 to 1 (same as {@link getPointAt}).
     * The returned vector is normalized and in world space, accounting for the SplineContainer's rotation.
     *
     * @param t - Parametric position along the spline (0 to 1)
     * @param target - Optional Vector3 to store the result (avoids allocation)
     * @returns The normalized tangent vector in world space at parameter `t`
     *
     * @example Orient an object along the spline
     * ```ts
     * const position = spline.getPointAt(0.5);
     * const tangent = spline.getTangentAt(0.5);
     *
     * object.position.copy(position);
     * object.lookAt(position.clone().add(tangent)); // Face along the spline
     * ```
     *
     * @example Calculate velocity direction for a moving object
     * ```ts
     * let t = 0;
     * update() {
     *   t += this.context.time.deltaTime * 0.2; // Speed
     *   if (t > 1) t = 0; // Loop
     *
     *   const pos = spline.getPointAt(t);
     *   const direction = spline.getTangentAt(t);
     *
     *   movingObject.position.copy(pos);
     *   movingObject.quaternion.setFromUnitVectors(
     *     new Vector3(0, 0, 1),
     *     direction
     *   );
     * }
     * ```
     *
     * @see {@link getPointAt} to get the position at a point
     */
    getTangentAt(t: number, target?: Vector3): Vector3 {
        if (!this.curve) return target ?? new Vector3();
        const wr = this.gameObject.worldQuaternion;
        return this.curve.getTangentAt(Mathf.clamp01(t), target).applyQuaternion(wr);
    }

    /**
     * Whether the spline forms a closed loop.
     *
     * **When `true`:**
     * - The spline connects the last knot back to the first knot, forming a continuous loop
     * - Perfect for racing tracks, patrol routes, or any circular path
     * - Parameter `t=1` will smoothly connect back to `t=0`
     *
     * **When `false` (default):**
     * - The spline is open, with distinct start and end points
     * - Suitable for one-way paths, camera movements, or linear progressions
     *
     * Changing this property marks the spline as dirty and triggers a rebuild.
     *
     * @example Create a circular patrol route
     * ```ts
     * const patrol = gameObject.addComponent(SplineContainer);
     * patrol.closed = true; // Loop back to start
     *
     * // Add points in a circle
     * for (let i = 0; i < 8; i++) {
     *   const angle = (i / 8) * Math.PI * 2;
     *   patrol.addKnot({
     *     position: new Vector3(Math.cos(angle) * 10, 0, Math.sin(angle) * 10)
     *   });
     * }
     * ```
     *
     * @default false
     */
    @serializeable()
    set closed(value: boolean) {
        this._closed = value;
        this._isDirty = true;
    }
    get closed() { return this._closed; }
    private _closed: boolean = false;


    /**
     * Array of knots (control points) that define the spline curve.
     *
     * Each element is a {@link SplineData} object containing position, rotation, and tangent information.
     * You can directly access and modify this array, but remember to call {@link markDirty} afterwards
     * to trigger a curve rebuild.
     *
     * **Best practices:**
     * - Use {@link addKnot} and {@link removeKnot} methods for automatic dirty marking
     * - If modifying knots directly, always call {@link markDirty} afterwards
     * - The order of knots determines the path direction
     *
     * @example Direct array access
     * ```ts
     * console.log(`Spline has ${spline.spline.length} knots`);
     *
     * // Access first knot
     * const firstKnot = spline.spline[0];
     * console.log("Start position:", firstKnot.position);
     *
     * // Modify and mark dirty
     * spline.spline[2].position.y += 5;
     * spline.markDirty();
     * ```
     *
     * @see {@link SplineData} for the knot data structure
     * @see {@link addKnot} for adding knots (auto marks dirty)
     * @see {@link removeKnot} for removing knots (auto marks dirty)
     * @see {@link markDirty} to trigger rebuild after manual modifications
     */
    @serializeable(SplineData)
    spline: SplineData[] = [];

    /**
     * Enables visual debug rendering of the spline curve.
     *
     * When enabled, the spline is rendered as a purple line in the scene, making it easy to
     * visualize the path during development. The debug line automatically updates when the spline is modified.
     *
     * **Debug visualization:**
     * - Purple line showing the complete curve path
     * - Automatically rebuilds when spline changes
     * - Line resolution based on number of knots (10 segments per knot)
     *
     * **Tip:** You can also enable debug visualization globally for all splines by adding `?debugsplines`
     * to your URL.
     *
     * @example Enable debug visualization
     * ```ts
     * const spline = gameObject.addComponent(SplineContainer);
     * spline.debug = true; // Show purple debug line
     *
     * // Add some knots to see the visualization
     * spline.addKnot({ position: new Vector3(0, 0, 0) });
     * spline.addKnot({ position: new Vector3(5, 2, 0) });
     * spline.addKnot({ position: new Vector3(10, 0, 5) });
     * ```
     */
    set debug(debug: boolean) {
        if (debug && !this._builtCurve) this.buildCurve();
        if (!this._debugLine) return;
        this._debugLine.visible = debug;
    }

    /**
     * The Three.js Curve object generated from the spline knots.
     *
     * This is the underlying curve implementation (typically a CatmullRomCurve3) that's used for
     * all position and tangent sampling. The curve is automatically regenerated when the spline
     * is marked dirty.
     *
     * **Note:** This curve is in local space relative to the SplineContainer. Use {@link getPointAt}
     * and {@link getTangentAt} methods to get world-space results.
     *
     * @returns The generated Three.js Curve, or null if not yet built
     */
    get curve(): Curve<Vector3> | null {
        return this._curve;
    }

    /**
     * Whether the spline needs to be rebuilt due to modifications.
     *
     * The spline is marked dirty when:
     * - Knots are added via {@link addKnot}
     * - Knots are removed via {@link removeKnot}
     * - {@link markDirty} is called manually
     * - The {@link closed} property is changed
     *
     * The curve is automatically rebuilt on the next update frame when dirty.
     *
     * @returns `true` if the spline needs rebuilding, `false` otherwise
     */
    get isDirty() { return this._isDirty; }

    private _isDirty: boolean = false;

    private _curve: Curve<Vector3> | null = null;
    private _builtCurve: boolean = false;
    private _debugLine: Object3D | null = null;

    /** @internal */
    awake() {
        if (debug) {
            console.log(`[Spline] ${this.name}`, this);
            this.buildCurve();
        }
    }

    /** @internal */
    update() {
        if (this._isDirty) {
            this.buildCurve(true);
        }
        if (this._debugLine && this._debugLine.parent !== this.gameObject) this.gameObject.add(this._debugLine);
    }

    private buildCurve(force: boolean = false) {
        if (this._builtCurve && !force) return;
        this._builtCurve = true;

        if (!this.spline) {
            console.error("[Spline] Can not build curve, no spline data", this.name);
            return;
        }
        this._isDirty = false;
        this._curve = createCatmullRomCurve(this.spline, this.closed);
        this.buildDebugCurve();
        // TODO: Unity supports spline interpolation type per knot which we don't support right now. Additionally EditType is deprecated. For simplicity we're just supporting CatmullRom for now.
        // switch (this.editType) {
        //     case SplineType.CatmullRom:
        //         this.createCatmullRomCurve();
        //         break;
        //     case SplineType.Bezier:
        //         console.warn("Bezier spline not implemented yet", this.name);
        //         this.createCatmullRomCurve();
        //         // this.createBezierCurve();
        //         break;
        //     case SplineType.Linear:
        //         this.createLinearCurve();
        //         break;
        // }
    }

    private buildDebugCurve() {
        if (debug && this.spline && this._curve) {
            this._debugLine?.removeFromParent();
            this._debugLine = null;

            const material = new LineBasicMaterial({
                color: 0x6600ff,
            });
            const res = this.spline.length * 10;
            const splinePoints = this._curve.getPoints(res);
            const geometry = new BufferGeometry().setFromPoints(splinePoints);
            this._debugLine = new Line(geometry, material);
            this.gameObject?.add(this._debugLine);
        }
    }
}


function createCatmullRomCurve(data: SplineData[], closed: boolean): CatmullRomCurve3 {
    const points = data.map(knot => new Vector3(-knot.position.x, knot.position.y, knot.position.z));
    if (points.length === 1) points.push(points[0]);
    const averageTension = data.reduce((acc, knot) => acc + Math.abs(knot.tangentOut.x) + Math.abs(knot.tangentOut.y) + Math.abs(knot.tangentOut.z), 0) / data.length;
    const tension = Mathf.clamp(Mathf.remap(averageTension, 0, 0.3, 0, .5), 0, 1);
    return new CatmullRomCurve3(points, closed, "catmullrom", tension);
}

function createLinearCurve(data: SplineData[], closed: boolean): LineCurve3 | null {
    if (!data || data.length < 2) return null;
    const points = data.map(knot => new Vector3(-knot.position.x, knot.position.y, knot.position.z));
    if (closed) points.push(points[0]);
    return new LineCurve3(points.at(0), points.at(1));
}


// function createBezierCurve(data: SplineData[], closed: boolean): CubicBezierCurve3 | null {
//     if (!data || data.length < 2) return null;

//     for (let k = 0; k < data.length; k++) {
//         const k0 = data[k];
//         let nextIndex = k + 1;
//         if (nextIndex >= data.length) {
//             if (!closed) break;
//             nextIndex = 0;
//         }
//         const k1 = data[nextIndex];
//         // points
//         const p0 = new Vector3(-k0.position.x, k0.position.y, k0.position.z);
//         const p1 = new Vector3(-k1.position.x, k1.position.y, k1.position.z);
//         // tangents
//         const t0 = new Vector3(-k0.tangentOut.x, k0.tangentOut.y, k0.tangentOut.z);
//         const t1 = new Vector3(-k1.tangentIn.x, k1.tangentIn.y, k1.tangentIn.z);
//         // rotations
//         // const q0 = k0.rotation;// new Quaternion(k0.rotation.value.x, k0.rotation.value.y, k0.rotation.value.z, k0.rotation.value.w);
//         // const q1 = k1.rotation;// new Quaternion(k1.rotation.value.x, k1.rotation.value.y, k1.rotation.value.z, k1.rotation.value.w);
//         // const a = new Vector3(0,1,0);
//         // const angle = Math.PI*.5;
//         // t0.sub(p0).applyQuaternion(q0).add(p0);
//         // t1.sub(p1).applyQuaternion(q1).add(p1);
//         t0.add(p0);
//         // t0.applyQuaternion(q0);
//         t1.add(p1);
//         const curve = new CubicBezierCurve3(p0, t0, t1, p1);
//         return curve;
//     }
//     return null;
// }

// class SplineCurve {

//     private spline: Spline;

//     constructor(spline: Spline) {
//         this.spline = spline;
//     }

//     getPoints(num: number): Vector3[] {
//         const points: Vector3[] = [];
//         const samplePerKnot = num / this.spline.length;
//         for (let k = 1; k < this.spline.length; k++) {
//             const cur = this.spline[k];
//             const prev = this.spline[k - 1];

//             for (let i = 0; i < samplePerKnot; i++) {
//                 const t = i / (samplePerKnot - 1);
//                 console.log(CurveUtils);
//                 const x = this.interpolate(-prev.Position.x, -cur.Position.x, -prev.tangentOut.x, -cur.TangentIn.x, t);
//                 const y = this.interpolate(prev.Position.y, cur.Position.y, prev.tangentOut.y, cur.TangentIn.y, t);
//                 const z = this.interpolate(prev.Position.z, cur.Position.z, prev.tangentOut.z, cur.TangentIn.z, t);
//                 points.push(new Vector3(x, y, z));
//             }
//         }

//         return points;
//     }

//     interpolate(p0, p1, p2, p3, t) {

//         var v0 = (p2 - p0) * 0.5;
//         var v1 = (p3 - p1) * 0.5;
//         var t2 = t * t;
//         var t3 = t * t2;
//         return (2 * p1 - 2 * p2 + v0 + v1) * t3 + (- 3 * p1 + 3 * p2 - 2 * v0 - v1) * t2 + v0 * t + p1;
//     }
// }
