import * as THREE from 'three';

/**
 * Shared bus between the main 3D viewer and the floating ViewCube widget.
 * - mainCameraRef / mainControlsRef: written by ModelViewer each frame
 * - viewAnim: smooth camera animation when a face is clicked
 * - calibration: orchestrates a 4-click (+ optional 2-click verify) procedure
 *   to align the active model's intrinsic axes to the world axes.
 */
export const mainCameraRef: { current: THREE.Camera | null } = { current: null };
export const mainControlsRef: { current: any | null } = { current: null };

// ── View animation ────────────────────────────────────────────────
export interface ViewAnim {
  active: boolean;
  startPos: THREE.Vector3;
  endPos: THREE.Vector3;
  startUp: THREE.Vector3;
  endUp: THREE.Vector3;
  t: number;
  duration: number;
}

export const viewAnim: ViewAnim = {
  active: false,
  startPos: new THREE.Vector3(),
  endPos: new THREE.Vector3(),
  startUp: new THREE.Vector3(0, 1, 0),
  endUp: new THREE.Vector3(0, 1, 0),
  t: 0,
  duration: 0.5,
};

export function requestView(dir: THREE.Vector3) {
  const cam = mainCameraRef.current;
  const controls = mainControlsRef.current;
  if (!cam || !controls) return;
  const target: THREE.Vector3 = controls.target ?? new THREE.Vector3();
  const distance = cam.position.distanceTo(target);
  const d = dir.clone().normalize();
  const endPos = target.clone().add(d.multiplyScalar(distance));
  const up = new THREE.Vector3(0, 1, 0);
  if (Math.abs(dir.y) > 0.99) {
    up.set(0, 0, dir.y > 0 ? -1 : 1);
  }
  viewAnim.startPos.copy(cam.position);
  viewAnim.endPos.copy(endPos);
  viewAnim.startUp.copy(cam.up);
  viewAnim.endUp.copy(up);
  viewAnim.t = 0;
  viewAnim.active = true;
}

// ── Calibration state machine ─────────────────────────────────────
export type CalibrationStep =
  | 'idle'
  | 'await-cube-1'
  | 'await-model-1'
  | 'await-cube-2'
  | 'await-model-2'
  | 'await-cube-3'   // optional verification
  | 'await-model-3'
  | 'done';

interface Pair { cube: THREE.Vector3; model: THREE.Vector3 }

interface CalState {
  step: CalibrationStep;
  modelId: string | null;
  pairs: Pair[];
  pendingCube: THREE.Vector3 | null;
  /** 0..1 progress for the button visual. */
  progress: number;
  /** Last verification angular error, in degrees. NaN until verify pair captured. */
  verifyErrorDeg: number;
  /** Listener notified on every state change so the UI re-renders. */
  listeners: Set<() => void>;
}

export const calibration: CalState = {
  step: 'idle',
  modelId: null,
  pairs: [],
  pendingCube: null,
  progress: 0,
  verifyErrorDeg: NaN,
  listeners: new Set(),
};

function notify() { calibration.listeners.forEach(fn => { try { fn(); } catch {} }); }

export function subscribeCalibration(fn: () => void): () => void {
  calibration.listeners.add(fn);
  return () => { calibration.listeners.delete(fn); };
}

export function startCalibration(modelId: string) {
  calibration.step = 'await-cube-1';
  calibration.modelId = modelId;
  calibration.pairs = [];
  calibration.pendingCube = null;
  calibration.progress = 0;
  calibration.verifyErrorDeg = NaN;
  notify();
}

export function cancelCalibration() {
  calibration.step = 'idle';
  calibration.modelId = null;
  calibration.pairs = [];
  calibration.pendingCube = null;
  calibration.progress = 0;
  calibration.verifyErrorDeg = NaN;
  notify();
}

/** Called when user clicks a cube face during calibration. */
export function pushCubeFace(dirWorld: THREE.Vector3) {
  if (calibration.step === 'await-cube-1') {
    calibration.pendingCube = dirWorld.clone().normalize();
    calibration.step = 'await-model-1';
  } else if (calibration.step === 'await-cube-2') {
    calibration.pendingCube = dirWorld.clone().normalize();
    calibration.step = 'await-model-2';
  } else if (calibration.step === 'await-cube-3') {
    calibration.pendingCube = dirWorld.clone().normalize();
    calibration.step = 'await-model-3';
  } else {
    return;
  }
  // Auto-rotate the main camera to look straight at the picked cube face
  // so the user sees the corresponding model face head-on (especially helpful
  // in orthographic mode where this guarantees a perpendicular click).
  try { requestView(dirWorld.clone()); } catch {}
  notify();
}

/** Snap a unit vector to the nearest principal axis (±X/±Y/±Z) when within
 *  `maxDeg` degrees of it. Returns the snapped vector or the input unchanged. */
function snapToPrincipalAxis(v: THREE.Vector3, maxDeg = 12): THREE.Vector3 {
  const cosT = Math.cos(THREE.MathUtils.degToRad(maxDeg));
  const axes = [
    new THREE.Vector3(1, 0, 0), new THREE.Vector3(-1, 0, 0),
    new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, -1, 0),
    new THREE.Vector3(0, 0, 1), new THREE.Vector3(0, 0,-1),
  ];
  let best = v;
  let bestDot = cosT;
  for (const a of axes) {
    const d = v.dot(a);
    if (d > bestDot) { bestDot = d; best = a.clone(); }
  }
  return best;
}

/**
 * Called when user clicks a model face during calibration.
 * @param normalWorld world-space face normal of the clicked face
 * @param calGroupWorldQuat current world quaternion of the calibration group
 *   (used to map both cube and model directions into the same local frame).
 */
export function pushModelFaceNormal(normalWorld: THREE.Vector3, calGroupWorldQuat: THREE.Quaternion) {
  if (!calibration.pendingCube) return;
  const cubeWorld = calibration.pendingCube;
  const inv = calGroupWorldQuat.clone().invert();
  const cubeLocal = cubeWorld.clone().applyQuaternion(inv).normalize();
  const modelLocalRaw = normalWorld.clone().applyQuaternion(inv).normalize();
  // Snap near-axial picks (chamfered or slightly off faces) to the closest
  // principal axis. This dramatically improves robustness when picking faces
  // from an orthographic head-on view.
  const modelLocal = snapToPrincipalAxis(modelLocalRaw, 12);
  calibration.pairs.push({ cube: cubeLocal, model: modelLocal });
  calibration.pendingCube = null;

  if (calibration.step === 'await-model-1') {
    calibration.step = 'await-cube-2';
    calibration.progress = 0.33;
  } else if (calibration.step === 'await-model-2') {
    calibration.progress = 0.75;
    calibration.step = 'await-cube-3';
  } else if (calibration.step === 'await-model-3') {
    calibration.progress = 1;
    calibration.verifyErrorDeg = computeVerifyError(calibration.pairs);
    calibration.step = 'done';
  }
  notify();
}

/** Computes a quaternion R such that R*pairs[i].model ≈ pairs[i].cube using
 *  Gram-Schmidt to ortho-normalize both bases. Returns null if degenerate. */
export function computeCalibrationQuaternion(pairs: Pair[]): THREE.Quaternion | null {
  if (pairs.length < 2) return null;
  const n1 = pairs[0].model.clone().normalize();
  const n2raw = pairs[1].model.clone();
  let n2 = n2raw.sub(n1.clone().multiplyScalar(n2raw.dot(n1)));
  if (n2.lengthSq() < 1e-6) return null;
  n2.normalize();
  const n3 = new THREE.Vector3().crossVectors(n1, n2);

  const c1 = pairs[0].cube.clone().normalize();
  const c2raw = pairs[1].cube.clone();
  let c2 = c2raw.sub(c1.clone().multiplyScalar(c2raw.dot(c1)));
  if (c2.lengthSq() < 1e-6) return null;
  c2.normalize();
  const c3 = new THREE.Vector3().crossVectors(c1, c2);

  // R = C * Nᵀ where columns of N are (n1,n2,n3) and C are (c1,c2,c3).
  // In three.js Matrix4: makeBasis sets columns.
  const N = new THREE.Matrix4().makeBasis(n1, n2, n3);
  const C = new THREE.Matrix4().makeBasis(c1, c2, c3);
  const Nt = N.clone().transpose();
  const R = new THREE.Matrix4().multiplyMatrices(C, Nt);
  const q = new THREE.Quaternion().setFromRotationMatrix(R);
  return q;
}

/** Predicted angular error of the verify pair, in degrees. */
export function computeVerifyError(pairs: Pair[]): number {
  if (pairs.length < 3) return NaN;
  const q = computeCalibrationQuaternion(pairs.slice(0, 2));
  if (!q) return NaN;
  const predicted = pairs[2].model.clone().applyQuaternion(q).normalize();
  const target = pairs[2].cube.clone().normalize();
  const cos = THREE.MathUtils.clamp(predicted.dot(target), -1, 1);
  return THREE.MathUtils.radToDeg(Math.acos(cos));
}
