import { AnimationClip, Object3D } from "three";
import type { Light, Material, PerspectiveCamera } from "three";

import { AnimatorConditionMode, AnimatorControllerParameterType } from "../engine/extensions/NEEDLE_animator_controller_model.js";
import type { AnimatorControllerModel, Condition, Parameter, State, Transition } from "../engine/extensions/NEEDLE_animator_controller_model.js";
import { InstantiateIdProvider } from "../engine/engine_networking_instantiate.js";
import { AnimatorController } from "./AnimatorController.js";
import { resolveClipSource, track as trackFn, type TrackDescriptor, type TrackOptions, type AnimationKeyframe, type Tween, type Vec3Value, type QuatValue, type EulerValue, type ColorValue } from "./AnimationBuilder.js";

/** Keyframe array or tween shorthand */
type KF<V> = AnimationKeyframe<V>[] | Tween<V>;

/** Extracts parameter names of a given type from the builder's tracked parameter map */
type ParamNamesOfType<TParams, PType extends string> = {
    [K in keyof TParams & string]: TParams[K] extends PType ? K : never
}[keyof TParams & string];


/**
 * Configuration for an animation state in the builder
 */
export declare type StateOptions = {
    /**
     * The animation clip for this state. Accepts:
     * - A pre-built `AnimationClip`
     * - A single {@link TrackDescriptor} from {@link track}
     * - An array of {@link TrackDescriptor}s (multiple tracks combined into one clip)
     *
     * When omitted, use {@link AnimatorControllerBuilder.track .track()} to define animation tracks inline.
     */
    clip?: AnimationClip | TrackDescriptor | TrackDescriptor[];
    /** Whether the animation should loop (default: false) */
    loop?: boolean;
    /** Base speed multiplier (default: 1) */
    speed?: number;
    /** Name of a float parameter to multiply with speed */
    speedParameter?: string;
    /** Normalized cycle offset 0-1 (default: 0) */
    cycleOffset?: number;
    /** Name of a float parameter to use as cycle offset */
    cycleOffsetParameter?: string;
}

/**
 * Configuration for a transition in the builder
 */
export declare type TransitionOptions = {
    /** Duration of the crossfade in seconds (default: 0) */
    duration?: number;
    /** Normalized exit time 0-1. When set, the transition waits until the source animation reaches this point before transitioning. */
    exitTime?: number;
    /** Normalized offset into the destination state's animation (default: 0) */
    offset?: number;
    /** Whether duration is in seconds (true) or normalized (false) (default: false) */
    hasFixedDuration?: boolean;
}

/** String condition modes for the builder, mapped to {@link AnimatorConditionMode} */
export type ConditionMode = "if" | "ifNot" | "greater" | "less" | "equals" | "notEqual";

function conditionModeToEnum(mode: ConditionMode): AnimatorConditionMode {
    switch (mode) {
        case "if": return AnimatorConditionMode.If;
        case "ifNot": return AnimatorConditionMode.IfNot;
        case "greater": return AnimatorConditionMode.Greater;
        case "less": return AnimatorConditionMode.Less;
        case "equals": return AnimatorConditionMode.Equals;
        case "notEqual": return AnimatorConditionMode.NotEqual;
    }
}

type BuilderTransition = {
    to: string;
    options: TransitionOptions;
    conditions: Array<{ parameter: string; mode: ConditionMode; threshold: number }>;
};

type BuilderState = {
    name: string;
    options: StateOptions;
    inlineTracks: TrackDescriptor[];
    transitions: BuilderTransition[];
};

/**
 * A fluent builder for creating {@link AnimatorController} instances from code.
 *
 * Use {@link AnimatorControllerBuilder.create} or {@link AnimatorController.build} to create a new builder.
 *
 * The builder tracks state names and parameter types through the fluent chain,
 * providing autocomplete for state names in `.transition()` and type-aware
 * `.condition()` calls (e.g., trigger parameters don't require a mode argument).
 *
 * @example With pre-built AnimationClips
 * ```ts
 * const controller = AnimatorControllerBuilder.create("CharacterController")
 *     .floatParameter("Speed", 0)
 *     .triggerParameter("Jump")
 *     .state("Idle", { clip: idleClip, loop: true })
 *     .state("Walk", { clip: walkClip, loop: true })
 *     .state("Jump", { clip: jumpClip })
 *     .transition("Idle", "Walk", { duration: 0.25 })
 *         .condition("Speed", "greater", 0.1)
 *     .transition("Walk", "Idle", { duration: 0.25 })
 *         .condition("Speed", "less", 0.1)
 *     .transition("*", "Jump", { duration: 0.1 })
 *         .condition("Jump")
 *     .transition("Jump", "Idle", { hasExitTime: true, exitTime: 0.9, duration: 0.25 })
 *     .build();
 * ```
 *
 * @example With inline tracks (no pre-built clips needed)
 * ```ts
 * const controller = AnimatorControllerBuilder.create("Door")
 *     .boolParameter("Open", false)
 *     .state("Closed", { loop: true })
 *         .track(door, "position", { from: [0, 0, 0], to: [0, 0, 0], duration: 1 })
 *     .state("Open", { loop: true })
 *         .track(door, "position", { from: [0, 0, 0], to: [2, 0, 0], duration: 1 })
 *         .track(light, "intensity", { from: 0, to: 5, duration: 1 })
 *     .transition("Closed", "Open", { duration: 0.25 })
 *         .condition("Open", "if")
 *     .transition("Open", "Closed", { duration: 0.25 })
 *         .condition("Open", "ifNot")
 *     .build(room);
 * ```
 *
 * @typeParam TStates - Union of state names added via `.state()`. Used for autocomplete and validation in `.transition()` and `.defaultState()`.
 * @typeParam TParams - Record mapping parameter names to their types (`"trigger"`, `"bool"`, `"float"`, `"int"`). Used for type-aware `.condition()` overloads.
 *
 * @category Animation and Sequencing
 * @group Utilities
 */
export class AnimatorControllerBuilder<
    TStates extends string = never,
    TParams extends Record<string, "trigger" | "bool" | "float" | "int"> = {},
> {
    private _name: string;
    private _parameters: Parameter[] = [];
    private _states: BuilderState[] = [];
    private _anyStateTransitions: BuilderTransition[] = [];
    private _defaultStateName: string | null = null;
    private _lastTransition: BuilderTransition | null = null;
    private _lastState: BuilderState | null = null;

    /**
     * Creates a new AnimatorControllerBuilder instance.
     * @param name - Optional name for the controller
     */
    static create(name?: string): AnimatorControllerBuilder {
        return new AnimatorControllerBuilder(name);
    }

    constructor(name?: string) {
        this._name = name ?? "AnimatorController";
    }

    /** Adds a float parameter */
    floatParameter<N extends string>(name: N, defaultValue: number = 0): AnimatorControllerBuilder<TStates, TParams & Record<N, "float">> {
        this._parameters.push({ name, hash: this._parameters.length, type: AnimatorControllerParameterType.Float, value: defaultValue });
        return this as any;
    }

    /** Adds an integer parameter */
    intParameter<N extends string>(name: N, defaultValue: number = 0): AnimatorControllerBuilder<TStates, TParams & Record<N, "int">> {
        this._parameters.push({ name, hash: this._parameters.length, type: AnimatorControllerParameterType.Int, value: defaultValue });
        return this as any;
    }

    /** Adds a boolean parameter */
    boolParameter<N extends string>(name: N, defaultValue: boolean = false): AnimatorControllerBuilder<TStates, TParams & Record<N, "bool">> {
        this._parameters.push({ name, hash: this._parameters.length, type: AnimatorControllerParameterType.Bool, value: defaultValue });
        return this as any;
    }

    /** Adds a trigger parameter */
    triggerParameter<N extends string>(name: N): AnimatorControllerBuilder<TStates, TParams & Record<N, "trigger">> {
        this._parameters.push({ name, hash: this._parameters.length, type: AnimatorControllerParameterType.Trigger, value: false });
        return this as any;
    }

    /**
     * Adds a state to the controller. The first state added becomes the default state.
     *
     * When `options.clip` is provided, the state uses that clip directly.
     * When omitted, chain `.track()` calls to define animation tracks inline:
     * ```ts
     * .state("Open", { loop: true })
     *     .track(door, "position", { from: [0,0,0], to: [2,0,0], duration: 1 })
     *     .track(light, "intensity", { from: 0, to: 5, duration: 1 })
     * ```
     *
     * @param name - Unique name for the state
     * @param options - State configuration including clip, loop, speed. When omitted, use `.track()` to add animation data.
     */
    state<N extends string>(name: N, options?: StateOptions): AnimatorControllerBuilder<TStates | N, TParams> {
        const state: BuilderState = { name, options: options ?? {}, inlineTracks: [], transitions: [] };
        this._states.push(state);
        this._lastState = state;
        return this as any;
    }

    /**
     * Adds a transition between two states.
     * Use `"*"` as the source to create a transition from any state.
     * Chain `.condition()` calls after this to add conditions.
     * @param from - Source state name, or `"*"` for any-state transition
     * @param to - Destination state name
     * @param options - Transition configuration
     */
    transition(from: TStates | "*", to: TStates, options?: TransitionOptions): AnimatorControllerBuilder<TStates, TParams> {
        this._lastState = null;
        const t: BuilderTransition = { to: to as string, options: options ?? {}, conditions: [] };
        if (from === "*") {
            this._anyStateTransitions.push(t);
        }
        else {
            const state = this._states.find(s => s.name === from);
            if (!state) throw new Error(`AnimatorControllerBuilder: source state "${from}" not found. Add it with .state() first.`);
            state.transitions.push(t);
        }
        this._lastTransition = t;
        return this as any;
    }

    /**
     * Adds a condition to the most recently added transition.
     * Multiple conditions on the same transition are AND-ed together.
     *
     * The required arguments depend on the parameter type:
     * - **Trigger**: `.condition("Jump")` — mode defaults to `"if"`, no threshold needed
     * - **Bool**: `.condition("Open", "if")` or `.condition("Open", "ifNot")`
     * - **Float/Int**: `.condition("Speed", "greater", 0.1)`
     *
     * @param parameter - Name of the parameter to evaluate
     */
    // Trigger parameters: mode is optional (defaults to "if")
    condition(parameter: ParamNamesOfType<TParams, "trigger">, mode?: "if" | "ifNot"): AnimatorControllerBuilder<TStates, TParams>;
    // Bool parameters: mode is required
    condition(parameter: ParamNamesOfType<TParams, "bool">, mode: "if" | "ifNot"): AnimatorControllerBuilder<TStates, TParams>;
    // Float/Int parameters: mode and optional threshold
    condition(parameter: ParamNamesOfType<TParams, "float" | "int">, mode: "greater" | "less" | "equals" | "notEqual", threshold?: number): AnimatorControllerBuilder<TStates, TParams>;
    condition(parameter: string, mode?: ConditionMode, threshold?: number): AnimatorControllerBuilder<TStates, TParams> {
        if (!this._lastTransition) throw new Error("AnimatorControllerBuilder: .condition() must be called after .transition()");
        this._lastTransition.conditions.push({ parameter, mode: mode ?? "if", threshold: threshold ?? 0 });
        return this as any;
    }

    // --- Object3D ---
    /** Adds an animation track for an Object3D's position or scale to the current state */
    track(target: Object3D, property: "position" | "scale", keyframes: KF<Vec3Value>, options?: TrackOptions): this;
    /** Adds an animation track for an Object3D's quaternion to the current state */
    track(target: Object3D, property: "quaternion", keyframes: KF<QuatValue>, options?: TrackOptions): this;
    /** Adds an animation track for an Object3D's rotation (Euler, converted to quaternion) to the current state */
    track(target: Object3D, property: "rotation", keyframes: KF<EulerValue>, options?: TrackOptions): this;
    /** Adds an animation track for an Object3D's visibility to the current state */
    track(target: Object3D, property: "visible", keyframes: KF<boolean>, options?: TrackOptions): this;
    // --- Material ---
    /** Adds an animation track for a material's numeric property to the current state */
    track(target: Material, property: "opacity" | "roughness" | "metalness" | "alphaTest" | "emissiveIntensity" | "envMapIntensity" | "bumpScale" | "displacementScale" | "displacementBias", keyframes: KF<number>, options?: TrackOptions): this;
    /** Adds an animation track for a material's color property to the current state */
    track(target: Material, property: "color" | "emissive", keyframes: KF<ColorValue>, options?: TrackOptions): this;
    // --- Light ---
    /** Adds an animation track for a light's numeric property to the current state */
    track(target: Light, property: "intensity" | "distance" | "angle" | "penumbra" | "decay", keyframes: KF<number>, options?: TrackOptions): this;
    /** Adds an animation track for a light's color to the current state */
    track(target: Light, property: "color", keyframes: KF<ColorValue>, options?: TrackOptions): this;
    // --- Camera ---
    /** Adds an animation track for a camera's numeric property to the current state */
    track(target: PerspectiveCamera, property: "fov" | "near" | "far" | "zoom", keyframes: KF<number>, options?: TrackOptions): this;
    /**
     * Adds an animation track to the most recently added state.
     * Must be called after `.state()`. The track has the same type-safe overloads
     * as the standalone {@link track} function.
     *
     * @param target - The object whose type determines valid properties and value types
     * @param property - The property to animate
     * @param keyframes - Keyframe array or {@link Tween} shorthand
     * @param options - Optional {@link TrackOptions} with a `root` for named targeting
     */
    track(target: object, property: string, keyframes: KF<any>, options?: TrackOptions): this {
        if (!this._lastState) throw new Error("AnimatorControllerBuilder: .track() must be called after .state()");
        if (this._lastState.options.clip) throw new Error(`AnimatorControllerBuilder: state "${this._lastState.name}" already has a clip. Use either .track() or { clip: ... }, not both.`);
        this._lastState.inlineTracks.push(trackFn(target as Object3D, property as "position", keyframes, options));
        return this;
    }

    /**
     * Sets which state is the default/entry state.
     * If not called, the first added state is used.
     * @param name - Name of the state
     */
    defaultState(name: TStates): AnimatorControllerBuilder<TStates, TParams> {
        this._defaultStateName = name as string;
        return this as any;
    }

    /**
     * Builds and returns the {@link AnimatorController}.
     * Resolves all state name references to indices.
     * @param root - Optional root Object3D for resolving {@link TrackDescriptor} track paths.
     *   When provided, tracks targeting a different object use `target.name` for named resolution.
     */
    build(root?: Object3D): AnimatorController {
        const stateIndexMap = new Map<string, number>();
        this._states.forEach((s, i) => stateIndexMap.set(s.name, i));

        let defaultStateIndex = 0;
        if (this._defaultStateName !== null) {
            const idx = stateIndexMap.get(this._defaultStateName);
            if (idx === undefined) throw new Error(`AnimatorControllerBuilder: default state "${this._defaultStateName}" not found`);
            defaultStateIndex = idx;
        }

        const resolveTransition = (t: BuilderTransition): Transition => {
            const destIndex = stateIndexMap.get(t.to);
            if (destIndex === undefined) throw new Error(`AnimatorControllerBuilder: transition target "${t.to}" not found`);
            return {
                exitTime: t.options.exitTime ?? 1,
                hasExitTime: t.options.exitTime !== undefined,
                duration: t.options.duration ?? 0,
                offset: t.options.offset ?? 0,
                hasFixedDuration: t.options.hasFixedDuration ?? false,
                destinationState: destIndex,
                conditions: t.conditions.map(c => ({
                    parameter: c.parameter,
                    mode: conditionModeToEnum(c.mode),
                    threshold: c.threshold,
                })),
            };
        };

        const states: State[] = this._states.map((s, index) => {
            const transitions: Transition[] = s.transitions.map(resolveTransition);

            // Replicate any-state transitions onto every state (except self-targeting)
            for (const anyT of this._anyStateTransitions) {
                const destIndex = stateIndexMap.get(anyT.to);
                if (destIndex === index) continue;
                transitions.push(resolveTransition(anyT));
            }

            // Resolve clip: from options.clip, inline .track() calls, or error
            let clip: AnimationClip;
            if (s.options.clip) {
                clip = resolveClipSource(s.options.clip, root);
            }
            else if (s.inlineTracks.length > 0) {
                clip = resolveClipSource(s.inlineTracks, root);
            }
            else {
                throw new Error(`AnimatorControllerBuilder: state "${s.name}" has no clip and no inline tracks. Provide { clip } or chain .track() calls.`);
            }

            return {
                name: s.name,
                hash: index,
                motion: {
                    name: clip.name,
                    clip: clip,
                    isLooping: s.options.loop ?? false,
                },
                transitions,
                behaviours: [],
                speed: s.options.speed,
                speedParameter: s.options.speedParameter,
                cycleOffset: s.options.cycleOffset,
                cycleOffsetParameter: s.options.cycleOffsetParameter,
            };
        });

        const model: AnimatorControllerModel = {
            name: this._name,
            guid: new InstantiateIdProvider(Date.now()).generateUUID(),
            parameters: this._parameters,
            layers: [{
                name: "Base Layer",
                stateMachine: {
                    defaultState: defaultStateIndex,
                    states,
                }
            }],
        };

        return new AnimatorController(model);
    }
}
