/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

/**
 * Compute the viewer-space position where a model's IFC (0,0,0) point
 * currently sits — the "true" IFC origin, regardless of any federation
 * alignment that re-baked the vertices.
 *
 * Standalone load or this model is the federation anchor:
 *   viewer = -(originShift + wasmRtcOffset_Yup)
 *
 * Non-anchor federated model:
 *   1. IFC (0,0,0)  → model projected coords (E_m, N_m, H_m)  [the pivot,
 *      so MapConversion rotation/scale collapses to the offsets]
 *   2. model projected → anchor projected via proj4 (identity if same CRS)
 *   3. anchor projected → anchor IFC frame via inverse MapConversion
 *   4. anchor IFC (Z-up) → world Y-up via axis swap
 *   5. subtract anchor's shift + rtc to land in anchor viewer-local space
 *
 * This works for both same-CRS arithmetic alignment and cross-CRS proj4
 * alignment without depending on stored alignment transforms or vertex
 * snapshots — it derives everything from the MapConversion + ProjectedCRS
 * the IFC file declares.
 */

import proj4 from 'proj4';
import type { MapConversion, ProjectedCRS } from '@ifc-lite/parser';
import type { CoordinateInfo } from '@ifc-lite/geometry';
import { resolveProjection } from './reproject';
import { getEffectiveHorizontalScale, resolveMapUnitToMetreScale } from './geo-scale';

export interface IfcOriginPlacement {
  /** Viewer-local position (Y-up) where this model's IFC (0,0,0) currently sits. */
  viewer: { x: number; y: number; z: number };
  /**
   * How the position was computed:
   *   - `'self'`      — model is its own frame (standalone or anchor)
   *   - `'anchor'`    — projected from model georef → anchor georef
   *   - `'fallback'`  — anchor has no usable georef; used model's own
   *                     pre-alignment frame as a best-effort guess
   */
  source: 'self' | 'anchor' | 'fallback';
}

export interface ModelGeorefInput {
  coordinateInfo?: CoordinateInfo;
  mapConversion?: MapConversion;
  projectedCRS?: ProjectedCRS;
  lengthUnitScale?: number;
  preAlignmentCoordinateInfo?: CoordinateInfo;
}

function totalYupOffset(info?: CoordinateInfo): { x: number; y: number; z: number } {
  const shift = info?.originShift ?? { x: 0, y: 0, z: 0 };
  const rtc = info?.wasmRtcOffset;
  const rtcYup = rtc ? { x: rtc.x, y: rtc.z, z: -rtc.y } : { x: 0, y: 0, z: 0 };
  return { x: shift.x + rtcYup.x, y: shift.y + rtcYup.y, z: shift.z + rtcYup.z };
}

export async function computeIfcOriginViewerPosition(
  model: ModelGeorefInput,
  anchor?: ModelGeorefInput | null,
): Promise<IfcOriginPlacement | null> {
  // Standalone load, or model has no georef → its own frame.
  if (!anchor || !model.mapConversion || !model.projectedCRS) {
    const off = totalYupOffset(model.coordinateInfo);
    return { viewer: { x: -off.x, y: -off.y, z: -off.z }, source: 'self' };
  }
  if (model === anchor) {
    const off = totalYupOffset(anchor.coordinateInfo);
    return { viewer: { x: -off.x, y: -off.y, z: -off.z }, source: 'self' };
  }
  if (!anchor.mapConversion || !anchor.projectedCRS) {
    // Anchor has no usable georef. The model's geometry was either left in
    // its own frame or aligned via RTC-only — fall back to pre-alignment.
    const off = totalYupOffset(model.preAlignmentCoordinateInfo ?? model.coordinateInfo);
    return { viewer: { x: -off.x, y: -off.y, z: -off.z }, source: 'fallback' };
  }

  // Step 1: model IFC (0,0,0) → model projected. At the pivot, rotation+scale
  // multiply zero — only the eastings/northings/orthogonalHeight remain.
  const modelLengthScale = model.lengthUnitScale ?? 1;
  const modelMapUnitScale = resolveMapUnitToMetreScale(model.projectedCRS.mapUnitScale, modelLengthScale);
  const eM = model.mapConversion.eastings * modelMapUnitScale;
  const nM = model.mapConversion.northings * modelMapUnitScale;
  const hM = model.mapConversion.orthogonalHeight * modelMapUnitScale;

  // Step 2: same CRS → identity; different CRS → proj4 hop.
  let eA = eM;
  let nA = nM;
  const sameCrs = (model.projectedCRS.name ?? '').toUpperCase()
    === (anchor.projectedCRS.name ?? '').toUpperCase();
  if (!sameCrs) {
    const srcDef = await resolveProjection(model.projectedCRS);
    const refDef = await resolveProjection(anchor.projectedCRS);
    if (!srcDef || !refDef) return null;
    try {
      const result = proj4(srcDef, refDef, [eM, nM]);
      eA = result[0];
      nA = result[1];
    } catch (error) {
      console.warn(
        `[ifc-origin] proj4 reprojection failed (${model.projectedCRS.name} → ${anchor.projectedCRS.name}) for [${eM}, ${nM}]:`,
        error,
      );
      return null;
    }
  }
  const hA = hM; // No vertical datum transform in browser.

  // Step 3: anchor projected → anchor IFC (invert anchor's MapConversion).
  const anchorLengthScale = anchor.lengthUnitScale ?? 1;
  const anchorMapUnitScale = resolveMapUnitToMetreScale(anchor.projectedCRS.mapUnitScale, anchorLengthScale);
  const eAnchor = anchor.mapConversion.eastings * anchorMapUnitScale;
  const nAnchor = anchor.mapConversion.northings * anchorMapUnitScale;
  const hAnchor = anchor.mapConversion.orthogonalHeight * anchorMapUnitScale;
  const anchorAbsc = anchor.mapConversion.xAxisAbscissa ?? 1;
  const anchorOrd = anchor.mapConversion.xAxisOrdinate ?? 0;
  const anchorScale = getEffectiveHorizontalScale(
    anchor.mapConversion.scale,
    anchorMapUnitScale,
    anchorLengthScale,
  );
  const anchorDenom = anchorScale * Math.max(anchorAbsc * anchorAbsc + anchorOrd * anchorOrd, 1e-12);
  if (Math.abs(anchorDenom) < 1e-12) return null;
  const invDenom = 1 / anchorDenom;
  const dE = eA - eAnchor;
  const dN = nA - nAnchor;
  const ifcX = invDenom * (anchorAbsc * dE + anchorOrd * dN);
  const ifcY = invDenom * (-anchorOrd * dE + anchorAbsc * dN);
  const ifcZ = hA - hAnchor;

  // Step 4 + 5: IFC Z-up → world Y-up → anchor viewer-local
  const anchorOff = totalYupOffset(anchor.coordinateInfo);
  return {
    viewer: {
      x: ifcX - anchorOff.x,
      y: ifcZ - anchorOff.y,
      z: -ifcY - anchorOff.z,
    },
    source: 'anchor',
  };
}
