/**
 * Anime.js - adapters - CJS
 * @version v4.5.0
 * @license MIT
 * @copyright 2026 - Julian Garnier
 */

'use strict';

var three = require('three');
var number = require('../../utils/number.cjs');
var adapter = require('./adapter.cjs');
var helpers = require('./helpers.cjs');

const hasSkewOrigin = (t) => !!(t._skewX || t._skewY || t._skewZ || t._originX || t._originY || t._originZ);

/**
 * @import { Object3D, InstancedMesh } from 'three'
 *
 * @typedef {InstancedMesh | BatchedMesh} InstanceParent
 */

const DIRTY_POS = 1;
const DIRTY_ROT = 2;
const DIRTY_SCALE = 4;
const DIRTY_SKEW = 8;

/** @type {WeakMap<InstanceParent, InstanceBinding>} */
const bindings = new WeakMap();

/**
 * Per-mesh state for `InstancedMesh` / `BatchedMesh` animations. Holds the instance array, the dirty queue, and the chained `onBeforeRender` closure.
 */
class InstanceBinding {

  /** @param {InstanceParent} mesh */
  constructor(mesh) {
    this.mesh = mesh;
    this.hasInstanceMatrix = 'instanceMatrix' in mesh;
    /** @type {(Instance | null)[]} */
    this.instances = [];
    /** @type {Instance[]} */
    this.dirtyList = [];
    /** @type {Object3D['onBeforeRender'] | null} */
    this.userOnBeforeRender = mesh.onBeforeRender || null;
    this.chainedHandler = makeChainedHandler(this);
    Object.defineProperty(mesh, 'onBeforeRender', {
      configurable: true,
      get: () => this.chainedHandler,
      set: (fn) => { this.userOnBeforeRender = fn; },
    });
  }

  flush() {
    const list = this.dirtyList;
    if (list.length === 0) return;
    for (let i = 0, l = list.length; i < l; i++) list[i]._flush();
    if (this.hasInstanceMatrix) /** @type {InstancedMesh} */(this.mesh).instanceMatrix.needsUpdate = true;
    list.length = 0;
  }

}

/**
 * @param {InstanceBinding} binding
 * @return {Object3D['onBeforeRender']}
 */
function makeChainedHandler(binding) {
  return function (renderer, scene, camera, geometry, material, group) {
    binding.flush();
    const u = binding.userOnBeforeRender;
    if (u) u.call(binding.mesh, renderer, scene, camera, geometry, material, group);
  };
}

/**
 * Per-instance adapter for `InstancedMesh` or `BatchedMesh`. Returned by `getInstances(mesh)`. Exposes the same flat properties as the mesh adapter, applied to a single instance id. Writes are coalesced and flushed before each render via `onBeforeRender`; call `commitChanges(mesh)` if you need to read `mesh.instanceMatrix` between a tick and a render.
 *
 * Caveats:
 * - `opacity` writes the parent's shared material, every instance is affected.
 * - `visible` is backed by `BatchedMesh.setVisibleAt`. On `InstancedMesh` it is a no-op, use `scale = 0` to hide an instance.
 */
class Instance {

  /**
   * @param {InstanceBinding} binding
   * @param {number} id
   */
  constructor(binding, id) {
    this.isAnimejsInstanceProxy = true;
    this.parent = binding.mesh;
    this.id = id;
    this._position = new three.Vector3();
    this._rotation = new three.Euler(0, 0, 0, 'XYZ');
    this._scale = new three.Vector3(1, 1, 1);
    this._matrix = new three.Matrix4();
    this._quat = new three.Quaternion();
    // Pre-allocate _color so the hidden class stays stable, set color mutates it in place rather than re-binding the field.
    this._color = new three.Color();
    this._dirty = 0;
    // Skew in degrees and transform-origin in object-space.
    // applySkewOrigin computes Math.tan inline per non-zero axis, so we only carry the user-facing angle here.
    this._skewX = 0;
    this._skewY = 0;
    this._skewZ = 0;
    this._originX = 0;
    this._originY = 0;
    this._originZ = 0;
    // Skip the position-only fast path in _flush whenever any skew or origin is non-zero, since both modify the matrix beyond pure translation.
    this._hasSkewOrigin = false;
    // Cache the binding dirty queue once so per-frame setters skip a property hop.
    /** @type {Instance[]} */
    this._dirtyList = binding.dirtyList;
    // Cache parent capability flags once so per-frame setters skip prototype-chain in checks.
    const parent = binding.mesh;
    this._hasSetColor = 'setColorAt' in parent;
    this._hasSetVisible = 'setVisibleAt' in parent;
    this._hasGetVisible = 'getVisibleAt' in parent;

    parent.getMatrixAt(id, this._matrix);
    if (this._matrix.elements[15] === 0) this._matrix.identity();
    this._matrix.decompose(this._position, this._quat, this._scale);
    this._rotation.setFromQuaternion(this._quat, 'XYZ');

    if ('getColorAt' in parent && 'instanceColor' in parent && parent.instanceColor) {
      parent.getColorAt(id, this._color);
    }
  }

  /**
   * @param {number} flag
   */
  _markDirty(flag) {
    // Enqueue once per frame, later flags only OR into _dirty without re-pushing to the queue.
    if (this._dirty === 0) this._dirtyList.push(this);
    this._dirty |= flag;
  }

  _flush() {
    const d = this._dirty;
    if (!d) return;
    const m = this._matrix;
    const p = this._position;
    if (d === DIRTY_POS && !this._hasSkewOrigin) {
      const e = m.elements;
      e[12] = p.x; e[13] = p.y; e[14] = p.z;
    } else {
      const q = this._quat;
      if (d & DIRTY_ROT) q.setFromEuler(this._rotation);
      m.compose(p, q, this._scale);
      if (this._hasSkewOrigin) {
        helpers.applySkewOrigin(m.elements, this._skewX, this._skewY, this._skewZ, this._originX, this._originY, this._originZ);
      }
    }
    this.parent.setMatrixAt(this.id, m);
    this._dirty = 0;
  }

  get x() { return this._position.x; }
  set x(v) { this._position.x = v; this._markDirty(DIRTY_POS); }

  get y() { return this._position.y; }
  set y(v) { this._position.y = v; this._markDirty(DIRTY_POS); }

  get z() { return this._position.z; }
  set z(v) { this._position.z = v; this._markDirty(DIRTY_POS); }

  get rotateX() { return number.radToDeg(this._rotation.x); }
  set rotateX(v) { this._rotation.x = number.degToRad(v); this._markDirty(DIRTY_ROT); }

  get rotateY() { return number.radToDeg(this._rotation.y); }
  set rotateY(v) { this._rotation.y = number.degToRad(v); this._markDirty(DIRTY_ROT); }

  get rotateZ() { return number.radToDeg(this._rotation.z); }
  set rotateZ(v) { this._rotation.z = number.degToRad(v); this._markDirty(DIRTY_ROT); }

  get scaleX() { return this._scale.x; }
  set scaleX(v) { this._scale.x = v; this._markDirty(DIRTY_SCALE); }

  get scaleY() { return this._scale.y; }
  set scaleY(v) { this._scale.y = v; this._markDirty(DIRTY_SCALE); }

  get scaleZ() { return this._scale.z; }
  set scaleZ(v) { this._scale.z = v; this._markDirty(DIRTY_SCALE); }

  get scale() { return this._scale.x; }
  set scale(v) {
    const s = this._scale;
    s.x = v; s.y = v; s.z = v;
    this._markDirty(DIRTY_SCALE);
  }

  get skewX() { return this._skewX; }
  set skewX(v) {
    this._skewX = v;
    this._hasSkewOrigin = hasSkewOrigin(this);
    this._markDirty(DIRTY_SKEW);
  }

  get skewY() { return this._skewY; }
  set skewY(v) {
    this._skewY = v;
    this._hasSkewOrigin = hasSkewOrigin(this);
    this._markDirty(DIRTY_SKEW);
  }

  get skewZ() { return this._skewZ; }
  set skewZ(v) {
    this._skewZ = v;
    this._hasSkewOrigin = hasSkewOrigin(this);
    this._markDirty(DIRTY_SKEW);
  }

  get transformOriginX() { return this._originX; }
  set transformOriginX(v) {
    this._originX = v;
    this._hasSkewOrigin = hasSkewOrigin(this);
    this._markDirty(DIRTY_SKEW);
  }

  get transformOriginY() { return this._originY; }
  set transformOriginY(v) {
    this._originY = v;
    this._hasSkewOrigin = hasSkewOrigin(this);
    this._markDirty(DIRTY_SKEW);
  }

  get transformOriginZ() { return this._originZ; }
  set transformOriginZ(v) {
    this._originZ = v;
    this._hasSkewOrigin = hasSkewOrigin(this);
    this._markDirty(DIRTY_SKEW);
  }

  get opacity() {
    return helpers.readScalar(/** @type {any} */(this.parent).material, 'opacity', helpers.PATH_DIRECT, 1);
  }
  /** @param {number} v */
  set opacity(v) {
    helpers.writeScalar(/** @type {any} */(this.parent).material, 'opacity', v, helpers.PATH_DIRECT);
  }

  get visible() {
    return this._hasGetVisible ? /** @type {any} */(this.parent).getVisibleAt(this.id) : true;
  }
  /** @param {boolean} v */
  set visible(v) {
    if (this._hasSetVisible) /** @type {any} */(this.parent).setVisibleAt(this.id, v);
  }

}

/**
 * Flushes pending matrix writes for every dirty instance of `mesh`.
 * Called automatically before each render. Call it yourself if you read
 * `mesh.instanceMatrix` between an animation tick and the next render.
 *
 * @param {InstanceParent} mesh
 */
function commitChanges(mesh) {
  const binding = bindings.get(mesh);
  if (binding) binding.flush();
}

/**
 * Returns an array of per-instance adapters for `mesh`. Index by id, deleted slots on `BatchedMesh` are `null`. Pass the array (or a slice / a single element) to `animate()`.
 *
 * animate(getInstances(mesh), { x: 100, delay: stagger(5) });
 * animate(getInstances(mesh)[42], { scale: 2 });
 *
 * The same array reference is preserved across `mesh.count` / `addInstance` / `deleteInstance` calls. Entries are pushed, nulled, or truncated in place. Animations bound to an outdated reference keep tweening their original adapters.
 *
 * `mesh.onBeforeRender` is replaced with an accessor that flushes
 * pending instance writes before each render and forwards to your
 * handler. Assigning your own `mesh.onBeforeRender = fn` keeps the
 * auto-flush, but reading `mesh.onBeforeRender` afterwards returns the
 * chained dispatcher rather than `fn` itself, so identity checks
 * (`mesh.onBeforeRender === fn`) will not match.
 *
 * @param {InstanceParent} mesh
 * @return {(Instance | null)[]}
 */
function getInstances(mesh) {
  let binding = bindings.get(mesh);
  if (!binding) {
    binding = new InstanceBinding(mesh);
    bindings.set(mesh, binding);
  }
  if (mesh instanceof three.BatchedMesh) {
    return refreshBatched(binding);
  }
  return refreshInstanced(binding);
}

/**
 * @param {InstanceBinding} binding
 * @return {(Instance | null)[]}
 */
function refreshInstanced(binding) {
  const mesh = /** @type {InstancedMesh} */(binding.mesh);
  const count = mesh.count;
  const arr = binding.instances;
  if (arr.length === 0 && count > 0) {
    arr.length = count;
    for (let i = 0; i < count; i++) arr[i] = new Instance(binding, i);
    return arr;
  }
  if (arr.length < count) {
    for (let i = arr.length; i < count; i++) arr.push(new Instance(binding, i));
  } else if (arr.length > count) {
    arr.length = count;
  }
  return arr;
}

/**
 * @param {InstanceBinding} binding
 * @return {(Instance | null)[]}
 */
function refreshBatched(binding) {
  const mesh = /** @type {BatchedMesh} */(binding.mesh);
  const arr = binding.instances;
  // Read three internal _instanceInfo to skip slots marked inactive by deleteInstance.
  // Iterate the full slot count, not instanceCount, since deleted slots leave gaps but live ids past them must still be visited.
  const instanceInfo = /** @type {{ _instanceInfo?: { active: boolean }[] }} */(mesh)._instanceInfo ?? [];
  const len = instanceInfo.length;
  if (arr.length < len) arr.length = len;
  for (let i = 0; i < len; i++) {
    const info = instanceInfo[i];
    const inactive = info && info.active === false;
    if (inactive) {
      arr[i] = null;
    } else if (!arr[i]) {
      arr[i] = new Instance(binding, i);
    }
  }
  if (arr.length > len) arr.length = len;
  return arr;
}

const instanceAdapter = adapter.threeAdapter.registerTargetAdapter((t) => t instanceof Instance);
instanceAdapter.registerProperty('color',
  (t) => helpers.readColorHex(t._color),
  (t, _, tw) => {
    if (!t._hasSetColor) return;
    const ns = tw._numbers;
    t._color.setRGB(ns[0] * helpers.COLOR_NORM, ns[1] * helpers.COLOR_NORM, ns[2] * helpers.COLOR_NORM, three.SRGBColorSpace);
    const p = /** @type {any} */(t.parent);
    p.setColorAt(t.id, t._color);
    if (p.instanceColor) p.instanceColor.needsUpdate = true;
  },
);
// Shorthand 3-token string x y z routed via the engine COMPLEX path.
// The setter reads tween _numbers for the per-frame lerped triplet.
instanceAdapter.registerProperty('transformOrigin',
  (t) => `${t._originX} ${t._originY} ${t._originZ}`,
  (t, _, tw) => {
    const ns = tw._numbers;
    t._originX = ns[0];
    t._originY = ns[1];
    t._originZ = ns[2];
    t._hasSkewOrigin = hasSkewOrigin(t);
    t._markDirty(DIRTY_SKEW);
  },
);

exports.commitChanges = commitChanges;
exports.getInstances = getInstances;
