import * as THREE from 'three';
import profile from '../public/client-prototype/origin/weapon-grip-profile.json';

export const approvedGripProfile = profile;

export type HandAdjustment = { rotation: [number, number, number]; offset: [number, number, number] };
export type HandAdjustments = Record<'right' | 'left', HandAdjustment>;
export const emptyAdjustments = (): HandAdjustments => ({
  right: { rotation: [0, 0, 0], offset: [0, 0, 0] },
  left: { rotation: [0, 0, 0], offset: [0, 0, 0] },
});
export const approvedAdjustments = (): HandAdjustments => structuredClone(profile.handAdjustments) as HandAdjustments;
export function readAdjustments(value: unknown): HandAdjustments {
  const result = approvedAdjustments();
  if (!value || typeof value !== 'object') return result;
  for (const side of ['right', 'left'] as const) {
    const row = (value as Record<string, unknown>)[side] as Partial<HandAdjustment> | undefined;
    if (!row) continue;
    for (const key of ['rotation', 'offset'] as const) {
      const values = row[key];
      if (!Array.isArray(values) || values.length !== 3 || !values.every(v => typeof v === 'number' && Number.isFinite(v))) continue;
      result[side][key] = values.map(v => key === 'rotation' ? THREE.MathUtils.clamp(v, -180, 180) : v) as [number, number, number];
    }
  }
  return result;
}

// The bounds constrain the model pivot near the hand. They do not claim that
// the entire weapon fits inside it, or that every handle is collision free.
export function handOffsetBounds(meshes: THREE.SkinnedMesh[], hand: THREE.Object3D, socket: THREE.Object3D): THREE.Box3 | null {
  const bones = new Set<THREE.Object3D>();
  hand.traverse(node => { if ((node as THREE.Bone).isBone) bones.add(node); });
  hand.updateWorldMatrix(true, true);
  const inverse = socket.matrixWorld.clone().invert();
  const bounds = new THREE.Box3();
  let count = 0;
  for (const mesh of meshes) {
    mesh.updateWorldMatrix(true, false);
    mesh.skeleton.update();
    const position = mesh.geometry.getAttribute('position');
    const weights = mesh.geometry.getAttribute('skinWeight');
    const indices = mesh.geometry.getAttribute('skinIndex');
    if (!weights || !indices) continue;
    for (let i = 0; i < position.count; i++) {
      let weight = 0;
      for (let j = 0; j < 4; j++) if (bones.has(mesh.skeleton.bones[indices.getComponent(i, j)])) weight += weights.getComponent(i, j);
      if (weight < 0.5) continue;
      const point = new THREE.Vector3().fromBufferAttribute(position, i);
      mesh.applyBoneTransform(i, point);
      point.applyMatrix4(mesh.matrixWorld).applyMatrix4(inverse);
      if (!point.toArray().every(Number.isFinite)) continue;
      bounds.expandByPoint(point); count++;
    }
  }
  if (!count || bounds.isEmpty()) return null;
  // Keep the untouched source pivot available for reset even at a hand edge.
  bounds.expandByPoint(new THREE.Vector3());
  return bounds;
}

export function clampAdjustment(adjustment: HandAdjustment, bounds: THREE.Box3) {
  const offset = new THREE.Vector3(...adjustment.offset).clamp(bounds.min, bounds.max);
  adjustment.offset = offset.toArray() as HandAdjustment['offset'];
}
export function calibrationQuaternion(adjustment: HandAdjustment, source: number[]) {
  const [x, y, z] = adjustment.rotation.map(THREE.MathUtils.degToRad);
  return new THREE.Quaternion().setFromEuler(new THREE.Euler(x, y, z, 'XYZ'))
    .multiply(new THREE.Quaternion().fromArray(source).normalize());
}
