export {};

const decoder = new TextDecoder();
const widths: Record<string, number> = { SCALAR: 1, VEC2: 2, VEC3: 3, VEC4: 4 };
const sizes: Record<number, number> = {
  5120: 1,
  5121: 1,
  5122: 2,
  5123: 2,
  5125: 4,
  5126: 4,
};
const arrays: Record<number, any> = {
  5120: Int8Array,
  5121: Uint8Array,
  5122: Int16Array,
  5123: Uint16Array,
  5125: Uint32Array,
  5126: Float32Array,
};

function component(view: DataView, offset: number, type: number) {
  if (type === 5120) return view.getInt8(offset);
  if (type === 5121) return view.getUint8(offset);
  if (type === 5122) return view.getInt16(offset, true);
  if (type === 5123) return view.getUint16(offset, true);
  if (type === 5125) return view.getUint32(offset, true);
  if (type === 5126) return view.getFloat32(offset, true);
  throw new Error("GLTF_COMPONENT");
}

function rotate(rotor: number[], value: number[]) {
  const [rx, ry, rz, rw] = rotor;
  const [x, y, z] = value;
  const tx = 2 * (ry * z - rz * y);
  const ty = 2 * (rz * x - rx * z);
  const tz = 2 * (rx * y - ry * x);
  return [
    x + rw * tx + ry * tz - rz * ty,
    y + rw * ty + rz * tx - rx * tz,
    z + rw * tz + rx * ty - ry * tx,
  ];
}

function multiply(a: number[], b: number[]) {
  return [
    a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
    a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
    a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
    a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
  ];
}

function decompose(matrix: number[]) {
  const scale = [
    Math.hypot(matrix[0], matrix[1], matrix[2]),
    Math.hypot(matrix[4], matrix[5], matrix[6]),
    Math.hypot(matrix[8], matrix[9], matrix[10]),
  ];
  const [sx, sy, sz] = scale;
  const [m00, m01, m02] = [matrix[0] / sx, matrix[4] / sy, matrix[8] / sz];
  const [m10, m11, m12] = [matrix[1] / sx, matrix[5] / sy, matrix[9] / sz];
  const [m20, m21, m22] = [matrix[2] / sx, matrix[6] / sy, matrix[10] / sz];
  let rotor: number[];
  if (m00 + m11 + m22 > 0) {
    const s = Math.sqrt(1 + m00 + m11 + m22) * 2;
    rotor = [(m21 - m12) / s, (m02 - m20) / s, (m10 - m01) / s, s / 4];
  } else if (m00 > m11 && m00 > m22) {
    const s = Math.sqrt(1 + m00 - m11 - m22) * 2;
    rotor = [s / 4, (m01 + m10) / s, (m02 + m20) / s, (m21 - m12) / s];
  } else if (m11 > m22) {
    const s = Math.sqrt(1 + m11 - m00 - m22) * 2;
    rotor = [(m01 + m10) / s, s / 4, (m12 + m21) / s, (m02 - m20) / s];
  } else {
    const s = Math.sqrt(1 + m22 - m00 - m11) * 2;
    rotor = [(m02 + m20) / s, (m12 + m21) / s, s / 4, (m10 - m01) / s];
  }
  return { position: matrix.slice(12, 15), rotor, scale };
}

function transform(node: any) {
  return node.matrix
    ? decompose(node.matrix)
    : {
        position: node.translation ?? [0, 0, 0],
        rotor: node.rotation ?? [0, 0, 0, 1],
        scale: node.scale ?? [1, 1, 1],
      };
}

function compose(parent: any, local: any) {
  const position = rotate(
    parent.rotor,
    local.position.map(
      (value: number, lane: number) => value * parent.scale[lane],
    ),
  );
  return {
    position: position.map((value, lane) => value + parent.position[lane]),
    rotor: multiply(parent.rotor, local.rotor),
    scale: local.scale.map(
      (value: number, lane: number) => value * parent.scale[lane],
    ),
  };
}

function parse(bytes: Uint8Array) {
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  if (view.getUint32(0, true) !== 0x46546c67)
    return {
      document: JSON.parse(decoder.decode(bytes)),
      binary: undefined as Uint8Array | undefined,
    };
  let offset = 12;
  let document: any;
  let binary: Uint8Array | undefined;
  while (offset < bytes.length) {
    const length = view.getUint32(offset, true);
    const type = view.getUint32(offset + 4, true);
    const chunk = bytes.subarray(offset + 8, offset + 8 + length);
    if (type === 0x4e4f534a)
      document = JSON.parse(decoder.decode(chunk).replace(/\0+$/u, ""));
    if (type === 0x004e4942) binary = chunk;
    offset += 8 + length;
  }
  if (!document) throw new Error("GLTF_JSON");
  return { document, binary };
}

async function load(url: string) {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`HTTP_${response.status}`);
  const { document, binary } = parse(
    new Uint8Array(await response.arrayBuffer()),
  );
  const buffers = await Promise.all(
    (document.buffers ?? []).map(async (buffer: any, index: number) => {
      if (buffer.uri === undefined) {
        if (index || !binary) throw new Error("GLTF_BUFFER");
        return binary;
      }
      const result = await fetch(new URL(buffer.uri, url));
      if (!result.ok) throw new Error(`HTTP_${result.status}`);
      return new Uint8Array(await result.arrayBuffer());
    }),
  );
  const imageBlobs = await Promise.all(
    (document.images ?? []).map(async (image: any) => {
      if (image.bufferView !== undefined) {
        const view = document.bufferViews[image.bufferView];
        const bytes = buffers[view.buffer];
        const start = view.byteOffset ?? 0;
        return new Blob([bytes.slice(start, start + view.byteLength)], {
          type: image.mimeType,
        });
      }
      const result = await fetch(new URL(image.uri, url));
      if (!result.ok) throw new Error(`HTTP_${result.status}`);
      return result.blob();
    }),
  );
  const textures = await Promise.all(
    (document.textures ?? []).map(async (texture: any) => ({
      image: await createImageBitmap(imageBlobs[texture.source]),
    })),
  );

  const accessor = (id: number, integer = false) => {
    const source = document.accessors[id];
    const width = widths[source.type];
    const size = sizes[source.componentType];
    const bufferView = document.bufferViews[source.bufferView];
    const bytes = buffers[bufferView.buffer];
    const start = (bufferView.byteOffset ?? 0) + (source.byteOffset ?? 0);
    const stride = bufferView.byteStride ?? width * size;
    const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
    const values = integer
      ? new Uint32Array(source.count * width)
      : new Float32Array(source.count * width);
    if (
      !source.normalized &&
      stride === width * size &&
      (bytes.byteOffset + start) % size === 0
    ) {
      const packed = new arrays[source.componentType](
        bytes.buffer,
        bytes.byteOffset + start,
        source.count * width,
      );
      values.set(packed);
      return values;
    }
    for (let item = 0; item < source.count; item++)
      for (let lane = 0; lane < width; lane++) {
        let value = component(
          view,
          start + item * stride + lane * size,
          source.componentType,
        );
        if (!integer && source.normalized) {
          const maximum =
            source.componentType === 5121
              ? 255
              : source.componentType === 5123
                ? 65535
                : 1;
          value /= maximum;
        }
        values[item * width + lane] = value;
      }
    return values;
  };

  const materials = (document.materials ?? []).map((material: any) => {
    const pbr = material.pbrMetallicRoughness ?? {};
    return {
      baseColor: pbr.baseColorFactor ?? [1, 1, 1, 1],
      metallic: pbr.metallicFactor ?? 0,
      roughness: pbr.roughnessFactor ?? 0.7,
      emissive: material.emissiveFactor ?? [0, 0, 0],
      alphaCutoff: material.alphaCutoff ?? 0.5,
      baseColorTexture: pbr.baseColorTexture?.index ?? -1,
      metallicRoughnessTexture: pbr.metallicRoughnessTexture?.index ?? -1,
      normalTexture: material.normalTexture?.index ?? -1,
      emissiveTexture: material.emissiveTexture?.index ?? -1,
    };
  });
  const primitives: any[] = [];
  const emitMesh = (meshId: number, transform: any) => {
    const mesh = document.meshes?.[meshId];
    for (const primitive of mesh?.primitives ?? []) {
      if (
        (primitive.mode ?? 4) !== 4 ||
        primitive.attributes.POSITION === undefined
      )
        continue;
      const positions = accessor(primitive.attributes.POSITION);
      const indices =
        primitive.indices === undefined
          ? Uint32Array.from(
              { length: positions.length / 3 },
              (_, index) => index,
            )
          : accessor(primitive.indices, true);
      primitives.push({
        positions,
        indices,
        ...(primitive.attributes.NORMAL === undefined
          ? {}
          : { normals: accessor(primitive.attributes.NORMAL) }),
        ...(primitive.attributes.TANGENT === undefined
          ? {}
          : { tangents: accessor(primitive.attributes.TANGENT) }),
        ...(primitive.attributes.TEXCOORD_0 === undefined
          ? {}
          : { uvs: accessor(primitive.attributes.TEXCOORD_0) }),
        ...(primitive.attributes.COLOR_0 === undefined
          ? {}
          : { colors: accessor(primitive.attributes.COLOR_0) }),
        material: primitive.material ?? -1,
        ...transform,
      });
    }
  };

  const nodes = document.nodes ?? [];
  const scene = document.scenes?.[document.scene ?? 0];
  const children = new Set(nodes.flatMap((node: any) => node.children ?? []));
  const roots =
    scene?.nodes ??
    nodes
      .map((_: any, id: number) => id)
      .filter((id: number) => !children.has(id));
  const visit = (
    id: number,
    parent = {
      position: [0, 0, 0],
      rotor: [0, 0, 0, 1],
      scale: [1, 1, 1],
    },
  ) => {
    const node = nodes[id] ?? {};
    const world = compose(parent, transform(node));
    if (node.mesh !== undefined) emitMesh(node.mesh, world);
    for (const child of node.children ?? []) visit(child, world);
  };
  for (const root of roots) visit(root);
  if (!nodes.length)
    for (let id = 0; id < (document.meshes ?? []).length; id++)
      emitMesh(id, {});
  return { materials, primitives, textures };
}

addEventListener("message", async ({ data }) => {
  try {
    const result = await load(data.url);
    const transfers = result.primitives.flatMap((primitive: any) =>
      ["positions", "indices", "normals", "tangents", "uvs", "colors"]
        .map((name) => primitive[name]?.buffer)
        .filter(Boolean),
    );
    transfers.push(...result.textures.map((texture: any) => texture.image));
    (postMessage as any)({ request: data.request, result }, transfers);
  } catch (error) {
    postMessage({
      request: data.request,
      error: error instanceof Error ? error.message : "GLTF_IMPORT",
    });
  }
});
