import { BinaryBuffer } from "../../core/binary/BinaryBuffer.js";
import { ResourceAccessKind } from "../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../core/model/ResourceAccessSpecification.js";
import { System } from "../ecs/System.js";
import { InterpolationLog } from "./InterpolationLog.js";
import { Interpolated, INTERPOLATION_KEY_UNSET, INTERPOLATION_SOURCE_LOCAL } from "./Interpolated.js";

/**
 * Render-time consumer of the interpolation log. Runs in {@link update} (the
 * variable-rate, render-loop-driven cycle): for every {@link Interpolated}
 * entity it blends each interpoland between the last two completed fixed ticks
 * by the current fixed-step alpha and writes the result into the live target
 * component (e.g. {@link Transform}). Because it writes the live component, all
 * Transform-linked machinery (culling, particles, trails, attachments) renders
 * the smoothed pose.
 *
 * Producers (physics, network) record authoritative per-tick snapshots into
 * {@link log}; this system only consumes. Cost is O(entities × interpolands),
 * and producers record only for things that moved, so the log stays sparse.
 *
 * For a LOCAL entity (the default), tick + alpha come from the simulation hub:
 * `entityManager.fixedStepTick` is the latest completed fixed step and
 * `entityManager.getFixedStepAlpha()` is the sub-step fraction, blending
 * `(fixedStepTick - 1, fixedStepTick)` from this system's own log.
 *
 * An entity may instead carry a non-default {@link Interpolated#sourceId} naming
 * a timeline registered via {@link registerSource} (the log + window provider
 * live on the system, not the component) — e.g. a networked entity blended on
 * the network's render-delayed playout. The same render pass then drives both
 * local and remote entities, so a mixed scene is coherent; each source is
 * sampled once per frame.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class InterpolationSystem extends System {

    /**
     * @param {{ buffer_capacity_bytes?: number, records_capacity?: number }} [options]
     */
    constructor({ buffer_capacity_bytes = 65536, records_capacity = 4096 } = {}) {
        super();

        this.dependencies = [Interpolated];

        this.components_used = [
            ResourceAccessSpecification.from(Interpolated, ResourceAccessKind.Read | ResourceAccessKind.Write),
        ];

        /**
         * Shared per-tick snapshot store. Producers record into it; this system
         * blends from it.
         * @type {InterpolationLog}
         */
        this.log = new InterpolationLog({ buffer_capacity_bytes, records_capacity });

        /**
         * Linked entities → their {@link Interpolated} component. Iterated each
         * update.
         * @private
         * @type {Map<number, Interpolated>}
         */
        this.__entities = new Map();

        /**
         * Reusable decode buffer for blended snapshots.
         * @private
         * @type {BinaryBuffer}
         */
        this.__scratch = new BinaryBuffer();
        this.__scratch.fromArrayBuffer(new ArrayBuffer(256));

        /**
         * Non-local timeline registry, keyed by `sourceId`. Each entry is the
         * timeline's `{ log, sample() }` — its log to read and a per-frame window
         * provider. Owners (e.g. the network layer) register here; the
         * {@link Interpolated} component only carries the integer id.
         * @private
         * @type {Map<number, { log: InterpolationLog, sample: () => { tick_a: number, tick_b: number, t: number } }>}
         */
        this.__sources = new Map();

        /**
         * Per-update cache of `sourceId → window`, so a source's playout is
         * sampled once per frame, not once per entity. Cleared each update.
         * @private
         * @type {Map<number, { tick_a: number, tick_b: number, t: number }>}
         */
        this.__source_windows = new Map();
    }

    /**
     * Register a non-local interpolation timeline under `sourceId`. Entities
     * carrying that id on their {@link Interpolated} component are blended from
     * `log` on the window `sample()` returns (sampled once per render frame).
     * Used by timeline owners — e.g. the network layer registers its
     * render-delayed playout. `sourceId` must not be
     * {@link INTERPOLATION_SOURCE_LOCAL} (reserved for the local clock).
     *
     * @param {number} sourceId
     * @param {InterpolationLog} log
     * @param {() => { tick_a: number, tick_b: number, t: number }} sample
     */
    registerSource(sourceId, log, sample) {
        this.__sources.set(sourceId, { log, sample });
    }

    /**
     * @param {EntityManager} entityManager
     */
    async startup(entityManager) {
        this.entityManager = entityManager;
    }

    /**
     * @param {Interpolated} interpolated
     * @param {number} entity
     */
    link(interpolated, entity) {
        if (interpolated.key === INTERPOLATION_KEY_UNSET) {
            // Default identity: the entity id. A producer (e.g. the network
            // layer) may have set a network_id already, in which case we keep it.
            interpolated.key = entity;
        }
        this.__entities.set(entity, interpolated);
    }

    /**
     * @param {Interpolated} interpolated
     * @param {number} entity
     */
    unlink(interpolated, entity) {
        this.__entities.delete(entity);
    }

    update(dt) {
        const entities = this.__entities;
        if (entities.size === 0) {
            return;
        }

        const em = this.entityManager;
        const dataset = (em !== null && em !== undefined) ? em.dataset : null;
        if (dataset === null || dataset === undefined) {
            return;
        }

        const scratch = this.__scratch;

        // Default (local) timeline: blend the last two completed fixed steps at
        // the sub-step alpha, reading this system's own log.
        const local_log = this.log;
        const local_tick_b = em.fixedStepTick;
        const local_tick_a = local_tick_b - 1;
        const local_t = em.getFixedStepAlpha();

        // A non-local source's playout window is sampled once per frame.
        const source_windows = this.__source_windows;
        source_windows.clear();

        for (const [entity, interpolated] of entities) {
            const key = interpolated.key;
            if (key === INTERPOLATION_KEY_UNSET) {
                continue;
            }

            if (interpolated.snap) {
                // Teleport / wake: the live components already hold the
                // authoritative pose (set by setPose, or the body's own pose on
                // wake), which isn't in the log as the latest two ticks yet.
                // Leave them untouched this frame so we don't slide across the
                // jump; resume interpolating next frame.
                interpolated.snap = false;
                continue;
            }

            // Pick the timeline by id: the default local fixed-step clock, or a
            // registered non-local source (e.g. the network playout).
            let log, tick_a, tick_b, t;
            const source_id = interpolated.sourceId;
            if (source_id === INTERPOLATION_SOURCE_LOCAL) {
                log = local_log;
                tick_a = local_tick_a;
                tick_b = local_tick_b;
                t = local_t;
            } else {
                const src = this.__sources.get(source_id);
                if (src === undefined) {
                    continue; // unregistered source — nothing to read
                }
                log = src.log;
                let win = source_windows.get(source_id);
                if (win === undefined) {
                    win = src.sample();
                    source_windows.set(source_id, win);
                }
                tick_a = win.tick_a;
                tick_b = win.tick_b;
                t = win.t;
            }

            const interpolands = interpolated.interpolands;
            const n = interpolands.length;
            for (let i = 0; i < n; i++) {
                const interpoland = interpolands[i];

                scratch.position = 0;
                const ok = log.interpolate(
                    scratch, key, interpoland.type_id, tick_a, tick_b, t, interpoland.interpolation_adapter
                );
                if (!ok) {
                    // Neither tick carries this interpoland — leave the live
                    // component untouched.
                    continue;
                }

                const target = dataset.getComponent(entity, interpoland.component_class);
                if (target === undefined || target === null) {
                    continue;
                }

                scratch.position = 0;
                interpoland.serialization_adapter.deserialize(scratch, target);
            }
        }
    }
}

/**
 * @readonly
 * @type {boolean}
 */
InterpolationSystem.prototype.isInterpolationSystem = true;
