const DEFAULT_STEP_URL = '/models/EInk_click_v101.step';
const DEFAULT_LIN_DEFLECTION = 0.1;
const DEFAULT_ANG_DEFLECTION = 0.1;

let openCascadePromise;

self.postMessage({ kind: 'ready' });

self.onmessage = async (event) => {
  const msg = event.data || {};
  if (msg.command !== 'convert-step') {
    return;
  }

  const timings = [];
  const startedAt = performance.now();

  try {
    const stepUrl = msg.stepUrl || DEFAULT_STEP_URL;
    const linDeflection = Number(msg.linDeflection ?? DEFAULT_LIN_DEFLECTION);
    const angDeflection = Number(msg.angDeflection ?? DEFAULT_ANG_DEFLECTION);

    mark(timings, 'fetch:start');
    const stepBytes = new Uint8Array(await (await fetch(stepUrl)).arrayBuffer());
    mark(timings, 'fetch:end');

    mark(timings, 'init:start');
    const oc = await loadOpenCascade();
    validateRequiredBindings(oc);
    mark(timings, 'init:end');

    mark(timings, 'read:start');
    writeFile(oc, '/model.step', stepBytes);
    const doc = readStepIntoDocument(oc, '/model.step');
    mark(timings, 'read:end');

    mark(timings, 'tessellate:start');
    const shape = firstFreeShape(oc, doc);
    if (oc.BRepTools?.Clean && shape) {
      oc.BRepTools.Clean(shape, false);
    }
    if (shape) {
      new oc.BRepMesh_IncrementalMesh_2(shape, linDeflection, false, angDeflection, false);
    }
    mark(timings, 'tessellate:end');

    mark(timings, 'export:start');
    const glbBytes = exportDocumentToGlb(oc, doc, '/model.glb');
    mark(timings, 'export:end');

    self.postMessage({
      kind: 'converted',
      timings,
      totalMs: performance.now() - startedAt,
      stepBytes: stepBytes.byteLength,
      glbBytes: glbBytes.byteLength,
      buffer: glbBytes.buffer,
      crossOriginIsolated: self.crossOriginIsolated === true,
      hardwareConcurrency: navigator.hardwareConcurrency || null,
    }, [glbBytes.buffer]);
  } catch (error) {
    self.postMessage({
      kind: 'error',
      timings,
      totalMs: performance.now() - startedAt,
      message: error instanceof Error ? error.message : String(error),
      stack: error instanceof Error ? error.stack : undefined,
      crossOriginIsolated: self.crossOriginIsolated === true,
      hardwareConcurrency: navigator.hardwareConcurrency || null,
    });
  }
};

async function loadOpenCascade() {
  if (!openCascadePromise) {
    openCascadePromise = (async () => {
      const openCascadeUrl = new URL('/vendor/opencascade/opencascade.js', self.location.origin).href;
      const module = await import(openCascadeUrl);
      const init = module.default || module.initOpenCascade || module.init;
      if (typeof init !== 'function') {
        throw new Error('OpenCascade module did not export an init function');
      }
      const options = {
        locateFile(path) {
          if (path.endsWith('.wasm')) {
            return '/vendor/opencascade/opencascade.wasm';
          }
          return `/vendor/opencascade/${path}`;
        },
      };
      return init(options);
    })();
  }
  return openCascadePromise;
}

function writeFile(oc, path, bytes) {
  if (oc.FS.analyzePath(path).exists) {
    oc.FS.unlink(path);
  }
  oc.FS.writeFile(path, bytes);
}

function validateRequiredBindings(oc) {
  const required = [
    ['STEPCAFControl_Reader_1', 'STEPCAFControl_Reader'],
    ['TDocStd_Document'],
    ['Handle_TDocStd_Document_2'],
    ['TCollection_ExtendedString_1'],
    ['XCAFDoc_DocumentTool'],
    ['XCAFDoc_ShapeTool'],
    ['TDF_LabelSequence_1'],
    ['BRepMesh_IncrementalMesh_2', 'BRepMesh_IncrementalMesh'],
    ['RWGltf_CafWriter'],
    ['TCollection_AsciiString_2'],
    ['TColStd_IndexedDataMapOfStringString_1'],
  ];
  const missing = required
    .filter((alternatives) => !alternatives.some((name) => oc[name] !== undefined))
    .map((alternatives) => alternatives.join(' or '));
  if (missing.length > 0) {
    throw new Error(`OpenCascade.js build is missing required STEP→GLB bindings: ${missing.join(', ')}`);
  }
}

function readStepIntoDocument(oc, path) {
  const reader = constructFirst(oc, ['STEPCAFControl_Reader_1', 'STEPCAFControl_Reader']);
  const readStatus = reader.ReadFile(path);
  if (readStatus !== undefined && isFailureStatus(oc, readStatus)) {
    throw new Error(`STEP read failed with status ${readStatus}`);
  }

  const doc = constructDocument(oc);
  const handle = construct(oc, 'Handle_TDocStd_Document_2', doc);
  const progress = optionalConstruct(oc, 'Message_ProgressRange_1');
  const transferred = progress
    ? callFirst(reader, ['Transfer_1', 'Transfer'], [handle, progress])
    : callFirst(reader, ['Transfer_1', 'Transfer'], [handle]);
  if (transferred === false) {
    throw new Error('STEP transfer into XDE document failed');
  }
  return doc;
}

function constructDocument(oc) {
  const name = construct(oc, 'TCollection_ExtendedString_1');
  return construct(oc, 'TDocStd_Document', name);
}

function firstFreeShape(oc, doc) {
  if (!oc.XCAFDoc_DocumentTool?.ShapeTool) {
    return undefined;
  }
  const shapeToolHandle = oc.XCAFDoc_DocumentTool.ShapeTool(doc.Main());
  const shapeTool = shapeToolHandle?.get ? shapeToolHandle.get() : shapeToolHandle;
  const labels = construct(oc, 'TDF_LabelSequence_1');
  shapeTool.GetFreeShapes(labels);
  if (labels.Length && labels.Length() < 1) {
    return undefined;
  }
  const label = labels.First();
  return callFirst(oc.XCAFDoc_ShapeTool, ['GetShape_2', 'GetShape'], [label]);
}

function exportDocumentToGlb(oc, doc, outputPath) {
  if (!oc.RWGltf_CafWriter) {
    throw new Error('RWGltf_CafWriter is missing from this OpenCascade.js build');
  }
  if (oc.FS.analyzePath(outputPath).exists) {
    oc.FS.unlink(outputPath);
  }
  const writer = new oc.RWGltf_CafWriter(construct(oc, 'TCollection_AsciiString_2', outputPath), true);
  const handle = construct(oc, 'Handle_TDocStd_Document_2', doc);
  const metadata = construct(oc, 'TColStd_IndexedDataMapOfStringString_1');
  const progress = optionalConstruct(oc, 'Message_ProgressRange_1');
  const exported = progress
    ? callFirst(writer, ['Perform_2', 'Perform'], [handle, metadata, progress])
    : callFirst(writer, ['Perform_2', 'Perform'], [handle, metadata]);
  if (exported === false) {
    throw new Error('RWGltf_CafWriter failed to export GLB');
  }
  return oc.FS.readFile(outputPath, { encoding: 'binary' });
}

function construct(oc, name, ...args) {
  const ctor = oc[name];
  if (typeof ctor !== 'function') {
    throw new Error(`${name} is missing from this OpenCascade.js build`);
  }
  return new ctor(...args);
}

function optionalConstruct(oc, name, ...args) {
  return typeof oc[name] === 'function' ? new oc[name](...args) : undefined;
}

function constructFirst(oc, names, ...args) {
  for (const name of names) {
    if (typeof oc[name] === 'function') {
      return new oc[name](...args);
    }
  }
  throw new Error(`${names.join(' or ')} is missing from this OpenCascade.js build`);
}

function callFirst(target, names, args) {
  for (const name of names) {
    if (typeof target?.[name] === 'function') {
      return target[name](...args);
    }
  }
  throw new Error(`missing method ${names.join(' or ')}`);
}

function isFailureStatus(oc, status) {
  if (typeof oc.IFSelect_ReturnStatus?.IFSelect_RetDone === 'number') {
    return status !== oc.IFSelect_ReturnStatus.IFSelect_RetDone;
  }
  return false;
}

function mark(timings, label) {
  timings.push({ label, ms: performance.now() });
}
