import { AnimationClip, BooleanKeyframeTrack, Color, ColorKeyframeTrack, Euler,
    InterpolateDiscrete, InterpolateLinear, InterpolateSmooth, KeyframeTrack,
    NumberKeyframeTrack, Object3D, Quaternion, QuaternionKeyframeTrack,
    Vector3, VectorKeyframeTrack } from "three";
import type { Camera, InterpolationModes, Light, Material, PerspectiveCamera } from "three";

import { isDevEnvironment } from "../engine/debug/index.js";


// ============================================================
// Value types (R3F-style array support)
// ============================================================

/** A Vector3 value, either as a Three.js Vector3 or as a `[x, y, z]` tuple */
export type Vec3Value = Vector3 | [number, number, number];
/** A Quaternion value, either as a Three.js Quaternion or as a `[x, y, z, w]` tuple */
export type QuatValue = Quaternion | [number, number, number, number];
/** A Color value, either as a Three.js Color or as an `[r, g, b]` tuple (0–1) */
export type ColorValue = Color | [number, number, number];
/** An Euler value, either as a Three.js Euler or as a `[x, y, z]` tuple (radians) */
export type EulerValue = Euler | [number, number, number];

// ============================================================
// Interpolation
// ============================================================

/** User-friendly interpolation mode names */
export type AnimationInterpolation = "linear" | "smooth" | "step";

// ============================================================
// Keyframe & Tween
// ============================================================

/** A single keyframe: a time and a value */
export type AnimationKeyframe<V> = {
    /** Time in seconds */
    time: number;
    /** The value at this time */
    value: V;
    /** Interpolation mode for this track (default: `"linear"`). Note: Three.js applies one mode per track; the first keyframe's mode is used. */
    interpolation?: AnimationInterpolation;
};

/** Shorthand for a simple two-keyframe animation (start → end) */
export type Tween<V> = {
    /** Start value (at time 0) */
    from: V;
    /** End value (at time = duration) */
    to: V;
    /** Duration in seconds (default: 1) */
    duration?: number;
    /** Interpolation mode (default: `"linear"`) */
    interpolation?: AnimationInterpolation;
};

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

// ============================================================
// TrackDescriptor
// ============================================================

/**
 * An opaque descriptor for a single animation track.
 * Created by {@link track} and resolved into a Three.js KeyframeTrack
 * when passed to {@link createAnimation}, or inline to
 * {@link AnimatorControllerBuilder.state} / {@link TimelineBuilder.clip}.
 *
 * @category Animation and Sequencing
 */
export type TrackDescriptor = {
    readonly __isTrackDescriptor: true;
    /** @internal */ readonly _target: object;
    /** @internal */ readonly _property: string;
    /** @internal */ readonly _keyframes: Array<{ time: number; value: any; interpolation?: AnimationInterpolation }>;
    /** @internal */ readonly _root?: Object3D;
};

// ============================================================
// TrackOptions / CreateAnimationOptions
// ============================================================

/** Options for a single track */
export type TrackOptions = {
    /**
     * Root object for resolving the track path.
     * - If `root === target` → self-targeting (`.property`)
     * - If `root !== target` → named targeting (`"targetName.property"` using `target.name`)
     * - If omitted → self-targeting by default
     */
    root?: Object3D;
};

/** Options for {@link createAnimation} */
export type CreateAnimationOptions = {
    /** Default root for all tracks that don't specify their own */
    root?: Object3D;
    /** Clip name (auto-generated if omitted) */
    name?: string;
};

// ============================================================
// track() — type-safe overloads
// ============================================================

// --- Object3D ---
/** Create an animation track for an Object3D's position or scale */
export function track(target: Object3D, property: "position" | "scale", keyframes: KF<Vec3Value>, options?: TrackOptions): TrackDescriptor;
/** Create an animation track for an Object3D's quaternion */
export function track(target: Object3D, property: "quaternion", keyframes: KF<QuatValue>, options?: TrackOptions): TrackDescriptor;
/** Create an animation track for an Object3D's rotation (Euler angles, converted to quaternion internally) */
export function track(target: Object3D, property: "rotation", keyframes: KF<EulerValue>, options?: TrackOptions): TrackDescriptor;
/** Create an animation track for an Object3D's visibility */
export function track(target: Object3D, property: "visible", keyframes: KF<boolean>, options?: TrackOptions): TrackDescriptor;

// --- Material ---
/** Create an animation track for a material's numeric property */
export function track(target: Material, property: "opacity" | "roughness" | "metalness" | "alphaTest" | "emissiveIntensity" | "envMapIntensity" | "bumpScale" | "displacementScale" | "displacementBias", keyframes: KF<number>, options?: TrackOptions): TrackDescriptor;
/** Create an animation track for a material's color property */
export function track(target: Material, property: "color" | "emissive", keyframes: KF<ColorValue>, options?: TrackOptions): TrackDescriptor;

// --- Light ---
/** Create an animation track for a light's numeric property */
export function track(target: Light, property: "intensity" | "distance" | "angle" | "penumbra" | "decay", keyframes: KF<number>, options?: TrackOptions): TrackDescriptor;
/** Create an animation track for a light's color */
export function track(target: Light, property: "color", keyframes: KF<ColorValue>, options?: TrackOptions): TrackDescriptor;

// --- Camera ---
/** Create an animation track for a camera's numeric property */
export function track(target: PerspectiveCamera, property: "fov" | "near" | "far" | "zoom", keyframes: KF<number>, options?: TrackOptions): TrackDescriptor;

// --- Implementation ---
/**
 * Creates an animation track descriptor targeting a property on the given object.
 *
 * The `target` is used for **TypeScript type inference** — it determines which property names
 * are offered and what value types the keyframes accept. By default, the resulting track
 * targets "self" (the mixer root). Pass `{ root }` to target a named child instead.
 *
 * @param target - The object whose type determines valid properties and value types
 * @param property - The property to animate (e.g. `"position"`, `"opacity"`, `"intensity"`)
 * @param keyframes - An array of {@link AnimationKeyframe} objects, or a {@link Tween} shorthand
 * @param options - Optional {@link TrackOptions} with a `root` for named targeting
 * @returns A {@link TrackDescriptor} that can be passed to {@link createAnimation}, or inline
 *   to `AnimatorControllerBuilder.state()` or `TimelineBuilder.clip()`
 *
 * @example Keyframe array
 * ```ts
 * track(door, "position", [
 *     { time: 0, value: [0, 0, 0] },
 *     { time: 1, value: [2, 0, 0] },
 * ])
 * ```
 *
 * @example Tween shorthand
 * ```ts
 * track(door, "position", { from: [0, 0, 0], to: [2, 0, 0], duration: 1 })
 * ```
 *
 * @example Named targeting (track targets a child of root)
 * ```ts
 * track(door, "position", keyframes, { root: room })
 * ```
 *
 * @category Animation and Sequencing
 * @group Utilities
 */
export function track(target: object, property: string, keyframes: KF<any>, options?: TrackOptions): TrackDescriptor {
    const kf = isTween(keyframes) ? tweenToKeyframes(keyframes) : keyframes;
    return {
        __isTrackDescriptor: true as const,
        _target: target,
        _property: property,
        _keyframes: kf.map(k => ({ ...k, value: snapshotValue(k.value) })),
        _root: options?.root,
    };
}


/** @internal alias so the AnimationBuilder class method can call the standalone function */
const trackFn = track;

// ============================================================
// AnimationBuilder
// ============================================================

/**
 * A fluent builder for creating `AnimationClip` instances from code.
 *
 * Use {@link AnimationBuilder.create} to start a new builder, chain `.track()` calls
 * to add animation tracks, and call `.build()` to produce the clip.
 *
 * @example Single track
 * ```ts
 * const clip = AnimationBuilder.create()
 *     .track(door, "position", { from: [0,0,0], to: [2,0,0], duration: 1 })
 *     .build();
 * ```
 *
 * @example Multiple tracks
 * ```ts
 * const clip = AnimationBuilder.create("DoorOpen")
 *     .track(door, "position", { from: [0,0,0], to: [2,0,0], duration: 1 })
 *     .track(light, "intensity", { from: 0, to: 5, duration: 1 })
 *     .build(room);
 * ```
 *
 * @category Animation and Sequencing
 * @group Utilities
 */
export class AnimationBuilder {
    private _name?: string;
    private _tracks: TrackDescriptor[] = [];

    /** Creates a new AnimationBuilder instance */
    static create(name?: string): AnimationBuilder {
        return new AnimationBuilder(name);
    }

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

    // --- Object3D ---
    /** Adds an animation track for an Object3D's position or scale */
    track(target: Object3D, property: "position" | "scale", keyframes: KF<Vec3Value>, options?: TrackOptions): this;
    /** Adds an animation track for an Object3D's quaternion */
    track(target: Object3D, property: "quaternion", keyframes: KF<QuatValue>, options?: TrackOptions): this;
    /** Adds an animation track for an Object3D's rotation (Euler, converted to quaternion) */
    track(target: Object3D, property: "rotation", keyframes: KF<EulerValue>, options?: TrackOptions): this;
    /** Adds an animation track for an Object3D's visibility */
    track(target: Object3D, property: "visible", keyframes: KF<boolean>, options?: TrackOptions): this;
    // --- Material ---
    /** Adds an animation track for a material's numeric property */
    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 */
    track(target: Material, property: "color" | "emissive", keyframes: KF<ColorValue>, options?: TrackOptions): this;
    // --- Light ---
    /** Adds an animation track for a light's numeric property */
    track(target: Light, property: "intensity" | "distance" | "angle" | "penumbra" | "decay", keyframes: KF<number>, options?: TrackOptions): this;
    /** Adds an animation track for a light's color */
    track(target: Light, property: "color", keyframes: KF<ColorValue>, options?: TrackOptions): this;
    // --- Camera ---
    /** Adds an animation track for a camera's numeric property */
    track(target: PerspectiveCamera, property: "fov" | "near" | "far" | "zoom", keyframes: KF<number>, options?: TrackOptions): this;
    track(target: object, property: string, keyframes: KF<any>, options?: TrackOptions): this {
        this._tracks.push(trackFn(target as Object3D, property as "position", keyframes, options));
        return this;
    }

    /**
     * Builds and returns the `AnimationClip`.
     * @param root - Optional root Object3D for resolving track paths.
     *   When provided, tracks targeting a different object use `target.name` for named resolution.
     */
    build(root?: Object3D): AnimationClip {
        return resolveToClip(this._tracks, root, this._name);
    }
}

// Keep createAnimation as internal alias for backwards compatibility
/** @internal @deprecated Use {@link AnimationBuilder.create} instead */
export function createAnimation(options: CreateAnimationOptions, ...tracks: TrackDescriptor[]): AnimationClip;
/** @internal @deprecated Use {@link AnimationBuilder.create} instead */
export function createAnimation(...tracks: TrackDescriptor[]): AnimationClip;
export function createAnimation(...args: (CreateAnimationOptions | TrackDescriptor)[]): AnimationClip {
    let options: CreateAnimationOptions | undefined;
    let descriptors: TrackDescriptor[];

    if (args.length > 0 && !isTrackDescriptor(args[0])) {
        options = args[0] as CreateAnimationOptions;
        descriptors = args.slice(1) as TrackDescriptor[];
    }
    else {
        descriptors = args as TrackDescriptor[];
    }

    return resolveToClip(descriptors, options?.root, options?.name);
}


// ============================================================
// Resolution helpers (exported for use by AnimatorControllerBuilder / TimelineBuilder)
// ============================================================

/**
 * Resolves a clip source (AnimationClip, TrackDescriptor, or TrackDescriptor[]) into an AnimationClip.
 * Used internally by AnimatorControllerBuilder and TimelineBuilder.
 * @internal
 */
export function resolveClipSource(clip: AnimationClip | TrackDescriptor | TrackDescriptor[], root?: Object3D): AnimationClip {
    if (clip instanceof AnimationClip) return clip;
    if (Array.isArray(clip)) return resolveToClip(clip, root);
    if (isTrackDescriptor(clip)) return resolveToClip([clip], root);
    return clip as AnimationClip; // should not reach
}

/** Type guard for {@link TrackDescriptor} */
export function isTrackDescriptor(obj: unknown): obj is TrackDescriptor {
    return obj != null && typeof obj === "object" && (obj as any).__isTrackDescriptor === true;
}

/** Resolves an array of TrackDescriptors into an AnimationClip. @internal */
export function resolveToClip(
    descriptors: TrackDescriptor[],
    buildRoot?: Object3D,
    name?: string,
): AnimationClip {
    const keyframeTracks: KeyframeTrack[] = [];
    for (const desc of descriptors) {
        keyframeTracks.push(buildKeyframeTrack(desc, buildRoot));
    }
    let duration = 0;
    for (const t of keyframeTracks) {
        const last = t.times[t.times.length - 1];
        if (last !== undefined && last > duration) duration = last;
    }
    return new AnimationClip(name ?? `clip_${_clipCounter++}`, duration, keyframeTracks);
}


// ============================================================
// Internal helpers
// ============================================================

let _clipCounter = 0;

function buildKeyframeTrack(desc: TrackDescriptor, buildRoot?: Object3D): KeyframeTrack {
    const property = resolvePropertyName(desc._property);
    const trackName = resolveTrackName(desc, buildRoot, property);

    const times: number[] = [];
    const values: number[] = [];
    for (const kf of desc._keyframes) {
        times.push(kf.time);
        const flat = flattenValue(kf.value, desc._property);
        for (let i = 0; i < flat.length; i++) values.push(flat[i]);
    }

    const interpolation = resolveInterpolation(desc._keyframes[0]?.interpolation);
    const TrackClass = resolveTrackClass(desc._property, desc._keyframes[0]?.value);
    return new TrackClass(trackName, times, values, interpolation);
}

function resolveTrackName(desc: TrackDescriptor, buildRoot?: Object3D, resolvedProperty?: string): string {
    const root = desc._root ?? buildRoot;
    const property = resolvedProperty ?? resolvePropertyName(desc._property);

    // Material target → always self-targeting with .material. prefix
    if (isMaterial(desc._target)) {
        return `.material.${property}`;
    }

    // No root → self-targeting
    if (!root) return `.${property}`;
    // Root === target → self-targeting
    if (root === desc._target) return `.${property}`;

    // Root !== target → named targeting
    const target = desc._target as Object3D;
    const nodeName = target.name;
    if (!nodeName) {
        if (isDevEnvironment()) {
            console.warn(`AnimationBuilder: target has no name, falling back to self-targeting. Set target.name for named targeting.`);
        }
        return `.${property}`;
    }

    // Dev mode: validate that target is actually a descendant of root
    if (isDevEnvironment() && root instanceof Object3D && target instanceof Object3D) {
        let found = false;
        root.traverse(child => {
            if (child === target) found = true;
        });
        if (!found) {
            console.warn(`AnimationBuilder: target "${nodeName}" is not a descendant of the provided root "${root.name}". The track may not resolve at play time.`);
        }
    }

    return `${nodeName}.${property}`;
}

function resolvePropertyName(property: string): string {
    if (property === "rotation") return "quaternion";
    return property;
}

function resolveTrackClass(property: string, sampleValue: any): new (name: string, times: ArrayLike<number>, values: ArrayLike<number>, interpolation?: InterpolationModes) => KeyframeTrack {
    // Check property name first (most reliable)
    if (property === "quaternion" || property === "rotation") return QuaternionKeyframeTrack;
    if (property === "visible") return BooleanKeyframeTrack;
    if (property === "position" || property === "scale") return VectorKeyframeTrack;
    if (property === "color" || property === "emissive") return ColorKeyframeTrack;

    // Check value type
    if (sampleValue instanceof Vector3) return VectorKeyframeTrack;
    if (sampleValue instanceof Quaternion) return QuaternionKeyframeTrack;
    if (sampleValue instanceof Color) return ColorKeyframeTrack;
    if (sampleValue instanceof Euler) return QuaternionKeyframeTrack;
    if (typeof sampleValue === "boolean") return BooleanKeyframeTrack;
    if (typeof sampleValue === "number") return NumberKeyframeTrack;

    // Array → infer from length + property context
    if (Array.isArray(sampleValue)) {
        if (sampleValue.length === 4) return QuaternionKeyframeTrack;
        if (sampleValue.length === 3) return VectorKeyframeTrack;
        if (sampleValue.length === 2) return VectorKeyframeTrack;
        return NumberKeyframeTrack;
    }

    return NumberKeyframeTrack;
}

function flattenValue(value: any, property: string): number[] {
    // Tuple arrays — already flat
    if (Array.isArray(value)) {
        // Special case: Euler array [x,y,z] for "rotation" → convert to quaternion
        if (property === "rotation" && value.length === 3) {
            const q = new Quaternion().setFromEuler(new Euler(value[0], value[1], value[2]));
            return [q.x, q.y, q.z, q.w];
        }
        return value;
    }

    if (typeof value === "number") return [value];
    if (typeof value === "boolean") return [value ? 1 : 0];
    if (value instanceof Vector3) return [value.x, value.y, value.z];
    if (value instanceof Quaternion) return [value.x, value.y, value.z, value.w];
    if (value instanceof Color) return [value.r, value.g, value.b];
    if (value instanceof Euler) {
        const q = new Quaternion().setFromEuler(value);
        return [q.x, q.y, q.z, q.w];
    }
    // duck-type Vector2/Vector3-like
    if (typeof value === "object" && value !== null && "x" in value && "y" in value) {
        if ("w" in value) return [value.x, value.y, value.z, value.w];
        if ("z" in value) return [value.x, value.y, value.z];
        return [value.x, value.y];
    }
    return [Number(value)];
}

function resolveInterpolation(mode?: AnimationInterpolation): InterpolationModes {
    switch (mode) {
        case "smooth": return InterpolateSmooth;
        case "step": return InterpolateDiscrete;
        default: return InterpolateLinear;
    }
}

function isTween<V>(kf: KF<V>): kf is Tween<V> {
    return kf != null && !Array.isArray(kf) && typeof kf === "object" && "from" in kf && "to" in kf;
}

function tweenToKeyframes<V>(tween: Tween<V>): AnimationKeyframe<V>[] {
    return [
        { time: 0, value: tween.from, interpolation: tween.interpolation },
        { time: tween.duration ?? 1, value: tween.to, interpolation: tween.interpolation },
    ];
}

/** Snapshot a keyframe value so live references (e.g. `obj.position`) are captured at definition time */
function snapshotValue(value: any): any {
    if (value == null || typeof value !== "object") return value; // primitives are fine
    if (typeof value.clone === "function") return value.clone(); // Vector3, Quaternion, Color, Euler, etc.
    if (Array.isArray(value)) return value.slice(); // tuple arrays
    return value;
}

function isMaterial(obj: any): obj is Material {
    return obj != null && typeof obj === "object" && obj.isMaterial === true;
}
