/**
 * Generic read/write helpers for the three.js bindings. Each operates on a target (or target array) and walks to the leaf via a path tag - direct (`target[name]`), uniform (`target.uniforms[name].value`), or TSL node (`target[name].value`). Adapter and resolver files pass the matching `PATH_*` constant; the JIT specializes each call site so the branch in `leafAt` / `writeLeafScalar` constant-folds.
 */

import { SRGBColorSpace } from 'three';
import { degToRad } from '../../utils/number.js';

export const COLOR_NORM = 1 / 255;

export const AXIS_MAP = /** @type {Record<string, 'x' | 'y' | 'z' | 'w'>} */({ X: 'x', Y: 'y', Z: 'z', W: 'w' });

export const PATH_DIRECT = 0;
const PATH_UNIFORM = 1;
const PATH_NODE = 2;

export const KIND_COLOR = 0;
export const KIND_SCALAR = 1;
export const KIND_VECTOR = 2;

// Reuse one mutable descriptor across calls, callers must consume immediately since the next call mutates it.
const desc = { kind: 0, path: 0, base: '', axis: /** @type {'x' | 'y' | 'z' | 'w'} */('x') };

/**
 * Patches a column-major `Matrix4.elements` array in place with CSS-style `skewX(α)` / `skewY(β)`, the 3D extension `skewZ(γ)` (shears z by x), and a `transform-origin` shift.
 *
 * @param {number[]} e
 * @param {number} skewX
 * @param {number} skewY
 * @param {number} skewZ
 * @param {number} ox
 * @param {number} oy
 * @param {number} oz
 */
export function applySkewOrigin(e, skewX, skewY, skewZ, ox, oy, oz) {
  if (skewX !== 0) {
    const t = Math.tan(degToRad(skewX));
    e[4] += e[0] * t;
    e[5] += e[1] * t;
    e[6] += e[2] * t;
  }
  if (skewY !== 0) {
    const t = Math.tan(degToRad(skewY));
    e[0] += e[4] * t;
    e[1] += e[5] * t;
    e[2] += e[6] * t;
  }
  if (skewZ !== 0) {
    const t = Math.tan(degToRad(skewZ));
    e[0] += e[8]  * t;
    e[1] += e[9]  * t;
    e[2] += e[10] * t;
  }
  if (ox !== 0 || oy !== 0 || oz !== 0) {
    e[12] += ox - (e[0] * ox + e[4] * oy + e[8] * oz);
    e[13] += oy - (e[1] * ox + e[5] * oy + e[9] * oz);
    e[14] += oz - (e[2] * ox + e[6] * oy + e[10] * oz);
  }
}

/**
 * @param {any} c
 * @return {String | null}
 */
export function readColorHex(c) {
  return c ? `#${c.getHexString(SRGBColorSpace)}` : null;
}

/**
 * Returns `true` when `v` is a `Vector2/3/4` instance whose dimension covers `axis`. `Quaternion` and plain `{x,y,z,w}`-shaped objects are intentionally rejected.
 *
 * @param {any} v
 * @param {'x' | 'y' | 'z' | 'w'} axis
 * @return {boolean}
 */
export function isVectorWith(v, axis) {
  if (!v) return false;
  if (axis === 'x' || axis === 'y') return !!(v.isVector2 || v.isVector3 || v.isVector4);
  if (axis === 'z') return !!(v.isVector3 || v.isVector4);
  return !!v.isVector4;
}

/**
 * Classifies `target[name]` and returns a descriptor `{ kind, path, base, axis }` (or `null`). Used by the resolver entries in `resolvers.js` and by the `Object3D` adapter to build the matching access pattern for any three.js target (Object3D, Material, Texture, Fog, UniformNode).
 *
 * @param {any} target
 * @param {string} name
 * @return {{ kind: number, path: number, base: string, axis: 'x' | 'y' | 'z' | 'w' } | null}
 */
export function classifyTargetProp(target, name) {
  const value = target[name];
  if (value !== undefined) {
    if (value && value.isColor) {
      desc.kind = KIND_COLOR; desc.path = PATH_DIRECT;
      return desc;
    }
    if (value && value.isUniformNode) {
      // Route TSL slot writes through node value, booleans share the scalar entry.
      const v = value.value;
      if (v && v.isColor) {
        desc.kind = KIND_COLOR; desc.path = PATH_NODE;
        return desc;
      }
      if (typeof v === 'number' || typeof v === 'boolean') {
        desc.kind = KIND_SCALAR; desc.path = PATH_NODE;
        return desc;
      }
      return null;
    }
    if (typeof value === 'number' || typeof value === 'boolean') {
      desc.kind = KIND_SCALAR; desc.path = PATH_DIRECT;
      return desc;
    }
    return null;
  }
  const uniforms = target.uniforms;
  const u = uniforms && uniforms[name];
  if (u) {
    const uv = u.value;
    if (uv && uv.isColor) {
      desc.kind = KIND_COLOR; desc.path = PATH_UNIFORM;
      return desc;
    }
    if (typeof uv === 'number') {
      desc.kind = KIND_SCALAR; desc.path = PATH_UNIFORM;
      return desc;
    }
  }
  // Decompose vector axis for names like posX, uOffsetY, uVec4W, colorNodeX.
  const axis = AXIS_MAP[name[name.length - 1]];
  if (axis) {
    const base = name.slice(0, -1);
    if (isVectorWith(target[base], axis)) {
      desc.kind = KIND_VECTOR; desc.path = PATH_DIRECT; desc.base = base; desc.axis = axis;
      return desc;
    }
    const baseSlot = target[base];
    if (baseSlot && baseSlot.isUniformNode && isVectorWith(baseSlot.value, axis)) {
      desc.kind = KIND_VECTOR; desc.path = PATH_NODE; desc.base = base; desc.axis = axis;
      return desc;
    }
    const ub = uniforms && uniforms[base];
    if (ub && isVectorWith(ub.value, axis)) {
      desc.kind = KIND_VECTOR; desc.path = PATH_UNIFORM; desc.base = base; desc.axis = axis;
      return desc;
    }
  }
  return null;
}

function leafAt(target, name, path) {
  if (path === PATH_DIRECT) return target[name];
  if (path === PATH_UNIFORM) {
    const u = target.uniforms;
    const e = u && u[name];
    return e ? e.value : null;
  }
  const n = target[name];
  return n ? n.value : null;
}

function writeLeafScalar(target, name, v, path) {
  if (path === PATH_DIRECT) { target[name] = v; return; }
  if (path === PATH_UNIFORM) {
    const u = target.uniforms;
    const e = u && u[name];
    if (e) e.value = v;
    return;
  }
  const n = target[name];
  if (n) n.value = v;
}

/**
 * @param {any} target
 * @param {string} name
 * @param {number} path
 * @param {number} [defaultValue]
 * @return {number}
 */
export function readScalar(target, name, path, defaultValue) {
  if (defaultValue === undefined) defaultValue = 0;
  if (!target) return defaultValue;
  const first = Array.isArray(target) ? target[0] : target;
  if (!first) return defaultValue;
  const v = leafAt(first, name, path);
  return v === undefined || v === null ? defaultValue : v;
}

/**
 * @param {any} target
 * @param {string} name
 * @param {number} v
 * @param {number} path
 */
export function writeScalar(target, name, v, path) {
  if (!target) return;
  if (Array.isArray(target)) {
    for (let i = 0, l = target.length; i < l; i++) writeLeafScalar(target[i], name, v, path);
  } else {
    writeLeafScalar(target, name, v, path);
  }
}

/**
 * @param {any} target
 * @param {string} name
 * @param {number} path
 * @return {String | null}
 */
export function readColorAt(target, name, path) {
  if (!target) return null;
  const first = Array.isArray(target) ? target[0] : target;
  return readColorHex(first ? leafAt(first, name, path) : null);
}

/**
 * @param {any} target
 * @param {string} name
 * @param {Array.<Number>} ns
 * @param {number} path
 */
export function writeColorAt(target, name, ns, path) {
  if (!target) return;
  const r = ns[0] * COLOR_NORM;
  const g = ns[1] * COLOR_NORM;
  const b = ns[2] * COLOR_NORM;
  if (Array.isArray(target)) {
    for (let i = 0, l = target.length; i < l; i++) {
      const c = leafAt(target[i], name, path);
      if (c) c.setRGB(r, g, b, SRGBColorSpace);
    }
  } else {
    const c = leafAt(target, name, path);
    if (c) c.setRGB(r, g, b, SRGBColorSpace);
  }
}

/**
 * @param {any} target
 * @param {string} base
 * @param {'x' | 'y' | 'z' | 'w'} axis
 * @param {number} path
 * @return {number}
 */
export function readVectorAt(target, base, axis, path) {
  if (!target) return 0;
  const first = Array.isArray(target) ? target[0] : target;
  const vec = first ? leafAt(first, base, path) : null;
  return vec ? vec[axis] : 0;
}

/**
 * @param {any} target
 * @param {string} base
 * @param {'x' | 'y' | 'z' | 'w'} axis
 * @param {number} v
 * @param {number} path
 */
export function writeVectorAt(target, base, axis, v, path) {
  if (!target) return;
  if (Array.isArray(target)) {
    for (let i = 0, l = target.length; i < l; i++) {
      const vec = leafAt(target[i], base, path);
      if (vec) vec[axis] = v;
    }
  } else {
    const vec = leafAt(target, base, path);
    if (vec) vec[axis] = v;
  }
}
