/**
 * USD/USDA/USDZ format support.
 * - USDA: Custom text parser (works in Node.js and browser)
 * - USDZ: Three.js USDZLoader (browser only, uses fflate for ZIP)
 * - Export: USDA text export + Three.js USDZExporter
 */

import type { MeshLoadResult } from '../wasm/meshlab-core';
import { initMeshLabCore } from '../wasm/meshlab-core';

// ============ USDA Parser (standalone, no Three.js dependency) ============

export interface USDMesh {
  name: string;
  points: number[];           // [x,y,z, ...]
  faceVertexCounts: number[]; // per-face vertex counts
  faceVertexIndices: number[];
  normals?: number[];
  uvs?: number[];
}

/**
 * Parse USDA (ASCII USD) text format.
 * Extracts Mesh prims with points, faceVertexCounts, faceVertexIndices, normals, UVs.
 */
export function parseUSDA(text: string): USDMesh[] {
  const meshes: USDMesh[] = [];

  // Remove comments
  const lines = text.split('\n').map(l => {
    const ci = l.indexOf('#');
    // Don't strip # inside strings or at start (header)
    if (ci > 0 && !l.substring(0, ci).includes('"')) return l.substring(0, ci);
    if (ci === 0 && l.startsWith('#usda')) return ''; // skip header
    if (ci === 0) return '';
    return l;
  });
  const clean = lines.join('\n');

  // Find all `def Mesh "Name" { ... }` blocks
  // Use a brace-matching approach
  const meshDefRegex = /def\s+Mesh\s+"([^"]+)"\s*\{/g;
  let match;

  while ((match = meshDefRegex.exec(clean)) !== null) {
    const name = match[1];
    const startIdx = match.index + match[0].length;

    // Find matching closing brace
    let depth = 1;
    let endIdx = startIdx;
    for (let i = startIdx; i < clean.length && depth > 0; i++) {
      if (clean[i] === '{') depth++;
      else if (clean[i] === '}') depth--;
      endIdx = i;
    }

    const block = clean.substring(startIdx, endIdx);

    // Extract arrays from block
    const extractArray = (pattern: RegExp): number[] | null => {
      const m = pattern.exec(block);
      if (!m) return null;
      const arrayStr = m[1];
      // Parse numbers from array string, handling (x,y,z) tuples
      const nums: number[] = [];
      const numRegex = /-?[\d.]+(?:e[+-]?\d+)?/gi;
      let nm;
      while ((nm = numRegex.exec(arrayStr)) !== null) {
        nums.push(parseFloat(nm[0]));
      }
      return nums;
    };

    const points = extractArray(/point3f\[\]\s+points\s*=\s*\[([\s\S]*?)\]/);
    const fvc = extractArray(/int\[\]\s+faceVertexCounts\s*=\s*\[([\s\S]*?)\]/);
    const fvi = extractArray(/int\[\]\s+faceVertexIndices\s*=\s*\[([\s\S]*?)\]/);

    if (!points || !fvi) continue;

    const normals = extractArray(/normal3f\[\]\s+normals\s*=\s*\[([\s\S]*?)\]/);
    const uvs = extractArray(/texCoord2f\[\]\s+(?:primvars:st|st)\s*=\s*\[([\s\S]*?)\]/);

    // Triangulate: if faceVertexCounts has values > 3, fan-triangulate
    let triIndices: number[];
    if (fvc && fvc.length > 0) {
      triIndices = [];
      let offset = 0;
      for (const count of fvc) {
        for (let i = 1; i < count - 1; i++) {
          triIndices.push(fvi[offset], fvi[offset + i], fvi[offset + i + 1]);
        }
        offset += count;
      }
    } else {
      // Assume all triangles if no faceVertexCounts
      triIndices = [...fvi];
    }

    meshes.push({
      name,
      points,
      faceVertexCounts: fvc || Array(triIndices.length / 3).fill(3),
      faceVertexIndices: triIndices,
      normals: normals || undefined,
      uvs: uvs || undefined,
    });
  }

  return meshes;
}

/**
 * Convert parsed USD meshes to Wasm MeshLoadResults.
 */
async function usdMeshesToWasm(meshes: USDMesh[]): Promise<MeshLoadResult[]> {
  const core = await initMeshLabCore();
  const results: MeshLoadResult[] = [];

  for (const m of meshes) {
    // Build OBJ string
    let obj = `# USD mesh: ${m.name}\n`;
    for (let i = 0; i < m.points.length; i += 3) {
      obj += `v ${m.points[i]} ${m.points[i + 1]} ${m.points[i + 2]}\n`;
    }
    for (let i = 0; i < m.faceVertexIndices.length; i += 3) {
      obj += `f ${m.faceVertexIndices[i] + 1} ${m.faceVertexIndices[i + 1] + 1} ${m.faceVertexIndices[i + 2] + 1}\n`;
    }

    const handle = core.loadMeshFromOBJ(obj);
    if (handle < 0) continue;

    const vertexCount = core.getVertexCount(handle);
    const faceCount = core.getFaceCount(handle);
    const boundingBox = core.getBoundingBox(handle);
    const vertices = new Float32Array(core.getVertexBuffer(handle));
    const indices = new Uint32Array(core.getIndexBuffer(handle));
    const normals = new Float32Array(core.getNormalBuffer(handle));

    results.push({
      handle, vertexCount, faceCount, boundingBox,
      buffers: { vertices, indices, normals },
    });
  }

  return results;
}

/**
 * Load USDA text file.
 */
export async function loadUSDA(text: string): Promise<MeshLoadResult[]> {
  const meshes = parseUSDA(text);
  if (meshes.length === 0) throw new Error('No mesh found in USDA');
  return usdMeshesToWasm(meshes);
}

/** Helper: extract meshes from a Three.js scene graph into Wasm MeshLoadResults */
async function extractMeshesFromThreeScene(scene: import('three').Object3D): Promise<MeshLoadResult[]> {
  const core = await initMeshLabCore();
  const results: MeshLoadResult[] = [];

  scene.traverse((child: import('three').Object3D) => {
    // Duck typing: check for geometry with position attribute (handles cross-module Three.js instances)
    const anyChild = child as unknown as Record<string, unknown>;
    if (!anyChild.geometry) return;
    const geom = anyChild.geometry as import('three').BufferGeometry;
    if (!geom.attributes?.position) return;
    const pos = geom.attributes.position;
    if (!pos || pos.count === 0) return;

    // Apply world transform to vertices
    const mesh = child as import('three').Mesh;
    mesh.updateWorldMatrix(true, false);
    const matrix = mesh.matrixWorld;

    let obj = '';
    for (let i = 0; i < pos.count; i++) {
      let x = pos.getX(i), y = pos.getY(i), z = pos.getZ(i);
      // Apply transform
      const e = matrix.elements;
      const tx = e[0]*x + e[4]*y + e[8]*z + e[12];
      const ty = e[1]*x + e[5]*y + e[9]*z + e[13];
      const tz = e[2]*x + e[6]*y + e[10]*z + e[14];
      obj += `v ${tx} ${ty} ${tz}\n`;
    }
    const idx = geom.index;
    if (idx) {
      for (let i = 0; i < idx.count; i += 3) {
        obj += `f ${idx.getX(i) + 1} ${idx.getX(i + 1) + 1} ${idx.getX(i + 2) + 1}\n`;
      }
    } else {
      for (let i = 0; i < pos.count; i += 3) {
        obj += `f ${i + 1} ${i + 2} ${i + 3}\n`;
      }
    }

    const handle = core.loadMeshFromOBJ(obj);
    if (handle < 0) return;
    results.push({
      handle,
      vertexCount: core.getVertexCount(handle),
      faceCount: core.getFaceCount(handle),
      boundingBox: core.getBoundingBox(handle),
      buffers: {
        vertices: new Float32Array(core.getVertexBuffer(handle)),
        indices: new Uint32Array(core.getIndexBuffer(handle)),
        normals: new Float32Array(core.getNormalBuffer(handle)),
      },
    });
  });

  return results;
}

/**
 * Load USDZ file (ZIP containing USDA or USDC + optional textures).
 * Strategy:
 * 1. Try three-usdz-loader (Pixar USD Wasm, handles USDC binary — Apple official USDZ files)
 * 2. Try Three.js USDZLoader (handles USDA-only USDZ)
 * 3. Try usdz-to-glb conversion → GLTFLoader
 * 4. Fallback: manual ZIP extraction for USDA text
 */
export async function loadUSDZ(data: ArrayBuffer): Promise<MeshLoadResult[]> {
  const errors: string[] = [];

  // Method 1: three-usdz-loader (Pixar USD Wasm — handles some USDC files)
  try {
    const { USDZLoader: PixarUSDZLoader } = await import('three-usdz-loader');
    const THREE = await import('three');
    const targetGroup = new THREE.Group();
    const loader = new PixarUSDZLoader();
    const module = await loader.waitForModuleLoadingCompleted();
    if (module) {
      const file = new File([data], 'model.usdz', { type: 'model/vnd.usdz+zip' });
      await loader.loadFile(file, targetGroup);
      const results = await extractMeshesFromThreeScene(targetGroup);
      if (results.length > 0) return results;
      errors.push('three-usdz-loader: WASM loaded but no mesh geometry produced');
    } else {
      errors.push('three-usdz-loader: WASM module init failed');
    }
  } catch (e) {
    errors.push(`three-usdz-loader: ${e instanceof Error ? e.message : String(e)}`);
  }

  // Method 2: Three.js built-in USDZLoader (USDA-only USDZ)
  try {
    const { USDZLoader } = await import('three/examples/jsm/loaders/USDZLoader.js');
    const loader = new USDZLoader();
    const group = loader.parse(data);
    const results = await extractMeshesFromThreeScene(group);
    if (results.length > 0) return results;
    errors.push('Three.js USDZLoader: no meshes extracted');
  } catch (e) {
    errors.push(`Three.js USDZLoader: ${e instanceof Error ? e.message : String(e)}`);
  }

  // Method 3: usdz-to-glb conversion → GLTFLoader
  try {
    const { convertUsdzToGlb } = await import('usdz-to-glb');
    const glbBytes = convertUsdzToGlb(new Uint8Array(data));
    if (glbBytes.length > 500) { // Skip if essentially empty GLB (Apple USDC produces ~192 byte stubs)
      const { GLTFLoader } = await import('three/examples/jsm/loaders/GLTFLoader.js');
      const loader = new GLTFLoader();
      const glbBuffer = glbBytes.buffer.slice(glbBytes.byteOffset, glbBytes.byteOffset + glbBytes.byteLength) as ArrayBuffer;
      const gltf = await new Promise<import('three/examples/jsm/loaders/GLTFLoader.js').GLTF>((resolve, reject) => {
        loader.parse(glbBuffer, '', resolve, reject);
      });
      const results = await extractMeshesFromThreeScene(gltf.scene);
      if (results.length > 0) return results;
    }
    errors.push('usdz-to-glb: conversion produced empty mesh');
  } catch (e) {
    errors.push(`usdz-to-glb: ${e instanceof Error ? e.message : String(e)}`);
  }

  // Method 4: Manual USDA extraction from ZIP
  try {
    const view = new DataView(data);
    if (view.getUint8(0) === 0x50 && view.getUint8(1) === 0x4B) {
      let offset = 0;
      while (offset < data.byteLength - 30) {
        const sig = view.getUint32(offset, true);
        if (sig !== 0x04034B50) break;
        const comprMethod = view.getUint16(offset + 8, true);
        const compSize = view.getUint32(offset + 18, true);
        const fnameLen = view.getUint16(offset + 26, true);
        const extraLen = view.getUint16(offset + 28, true);
        const fname = new TextDecoder().decode(new Uint8Array(data, offset + 30, fnameLen));
        const dataStart = offset + 30 + fnameLen + extraLen;
        if (comprMethod === 0 && (fname.endsWith('.usda') || fname.endsWith('.usd'))) {
          const text = new TextDecoder().decode(new Uint8Array(data, dataStart, compSize));
          if (text.includes('def ') || text.includes('point3f')) {
            return loadUSDA(text);
          }
        }
        offset = dataStart + compSize;
        // USDZ 64-byte alignment
        while (offset % 64 !== 0 && offset < data.byteLength) offset++;
      }
    }
    errors.push('Manual extraction: no USDA text found');
  } catch (e) {
    errors.push(`Manual extraction: ${e instanceof Error ? e.message : String(e)}`);
  }

  // All methods failed — show helpful error
  throw new Error(
    'Failed to load USDZ file. This file uses USDC binary (Crate) format.\n\n' +
    'To open this file, try one of:\n' +
    '1. Convert to GLB using Blender (File > Import > USD, then File > Export > GLB)\n' +
    '2. Use Apple Reality Converter to export as USDA text format\n' +
    '3. Use an online USDZ to GLB converter\n\n' +
    'Technical details:\n' + errors.join('\n')
  );
}

// ============ USDA Exporter ============

/**
 * Export mesh to USDA text format.
 */
export async function exportUSDA(handle: number): Promise<string> {
  const core = await initMeshLabCore();
  const verts = new Float32Array(core.getVertexBuffer(handle));
  const indices = new Uint32Array(core.getIndexBuffer(handle));
  const normals = new Float32Array(core.getNormalBuffer(handle));
  const hasUVs = core.meshHasUVs(handle);
  const uvs = hasUVs ? new Float32Array(core.getUVBuffer(handle)) : null;

  let usda = '#usda 1.0\n';
  usda += '(\n    defaultPrim = "Root"\n    upAxis = "Y"\n)\n\n';
  usda += 'def Xform "Root"\n{\n';
  usda += '    def Mesh "Mesh"\n    {\n';

  // faceVertexCounts (all triangles)
  const faceCount = indices.length / 3;
  usda += '        int[] faceVertexCounts = [' + Array(faceCount).fill(3).join(', ') + ']\n';

  // faceVertexIndices
  usda += '        int[] faceVertexIndices = [' + Array.from(indices).join(', ') + ']\n';

  // points
  usda += '        point3f[] points = [';
  const pts: string[] = [];
  for (let i = 0; i < verts.length; i += 3) {
    pts.push(`(${verts[i]}, ${verts[i + 1]}, ${verts[i + 2]})`);
  }
  usda += pts.join(', ');
  usda += ']\n';

  // normals
  if (normals.length > 0) {
    usda += '        normal3f[] normals = [';
    const ns: string[] = [];
    for (let i = 0; i < normals.length; i += 3) {
      ns.push(`(${normals[i]}, ${normals[i + 1]}, ${normals[i + 2]})`);
    }
    usda += ns.join(', ');
    usda += ']\n';
  }

  // UVs
  if (uvs && uvs.length > 0) {
    usda += '        texCoord2f[] primvars:st = [';
    const us: string[] = [];
    for (let i = 0; i < uvs.length; i += 2) {
      us.push(`(${uvs[i]}, ${uvs[i + 1]})`);
    }
    usda += us.join(', ');
    usda += ']\n';
  }

  usda += '    }\n}\n';
  return usda;
}
