import * as THREE from "three";
import { Quaternion, Vector3 } from "three";
import { toBtQuaternion } from "../three-ammo/worker/utils";
import { ShapeConfig, ShapeFit, ShapeType } from "../three-ammo/lib/types";

export interface FinalizedShape extends Ammo.btCollisionShape {
  type: ShapeType;
  destroy(): void;
  localTransform: Ammo.btTransform;

  // Ammo objects destroyed when shape is destroyed
  resources?: any[];

  // adress of allocated memory
  heightfieldData?: number;

  // Children in a compound shape
  shapes?: FinalizedShape[];
}

export function createCollisionShapes(
  vertices,
  matrices,
  indexes,
  matrixWorld,
  options: ShapeConfig
): FinalizedShape | null {
  switch (options.type) {
    case ShapeType.BOX:
      return createBoxShape(vertices, matrices, matrixWorld, options);
    case ShapeType.CYLINDER:
      return createCylinderShape(vertices, matrices, matrixWorld, options);
    case ShapeType.CAPSULE:
      return createCapsuleShape(vertices, matrices, matrixWorld, options);
    case ShapeType.CONE:
      return createConeShape(vertices, matrices, matrixWorld, options);
    case ShapeType.SPHERE:
      return createSphereShape(vertices, matrices, matrixWorld, options);
    case ShapeType.HULL:
      return createHullShape(vertices, matrices, matrixWorld, options);
    case ShapeType.HACD:
      return createCompoundShape(
        createHACDShapes(vertices, matrices, indexes, matrixWorld, options),
        options
      );
    case ShapeType.VHACD:
      return createCompoundShape(
        createVHACDShapes(vertices, matrices, indexes, matrixWorld, options),
        options
      );
    case ShapeType.MESH:
      return createTriMeshShape(
        vertices,
        matrices,
        indexes,
        matrixWorld,
        options
      );
    case ShapeType.HEIGHTFIELD:
      return createHeightfieldTerrainShape(options);
    default:
      console.warn(options.type + " is not currently supported");
      return null;
  }
}

export function createCompoundShape(
  shapes: FinalizedShape[],
  options: ShapeConfig
): FinalizedShape {
  const compoundShape = new Ammo.btCompoundShape(true);

  for (const shape of shapes) {
    compoundShape.addChildShape(shape.localTransform, shape);
  }

  ((compoundShape as unknown) as FinalizedShape).shapes = shapes;

  return finishCollisionShape(compoundShape, options);
}

//TODO: support gimpact (dynamic trimesh) and heightmap

export function createBoxShape(
  vertices,
  matrices,
  matrixWorld,
  options: ShapeConfig
) {
  options.type = ShapeType.BOX;
  _setOptions(options);

  if (options.fit === ShapeFit.ALL) {
    options.halfExtents = _computeHalfExtents(
      _computeBounds(vertices, matrices),
      options.minHalfExtents,
      options.maxHalfExtents
    );
  }

  const btHalfExtents = new Ammo.btVector3(
    options.halfExtents!.x,
    options.halfExtents!.y,
    options.halfExtents!.z
  );
  const collisionShape = new Ammo.btBoxShape(btHalfExtents);
  Ammo.destroy(btHalfExtents);

  return finishCollisionShape(
    collisionShape,
    options,
    _computeScale(matrixWorld, options)
  );
}

export function createCylinderShape(
  vertices,
  matrices,
  matrixWorld,
  options: ShapeConfig
) {
  options.type = ShapeType.CYLINDER;
  _setOptions(options);

  if (options.fit === ShapeFit.ALL) {
    options.halfExtents = _computeHalfExtents(
      _computeBounds(vertices, matrices),
      options.minHalfExtents,
      options.maxHalfExtents
    );
  }

  const btHalfExtents = new Ammo.btVector3(
    options.halfExtents!.x,
    options.halfExtents!.y,
    options.halfExtents!.z
  );
  const collisionShape = (() => {
    switch (options.cylinderAxis) {
      case "x":
        return new Ammo.btCylinderShapeX(btHalfExtents);
      case "z":
        return new Ammo.btCylinderShapeZ(btHalfExtents);
      case "y":
      default:
        return new Ammo.btCylinderShape(btHalfExtents);
    }
  })();
  Ammo.destroy(btHalfExtents);

  return finishCollisionShape(
    collisionShape,
    options,
    _computeScale(matrixWorld, options)
  );
}

export function createCapsuleShape(
  vertices,
  matrices,
  matrixWorld,
  options: ShapeConfig
) {
  options.type = ShapeType.CAPSULE;
  _setOptions(options);

  if (options.fit === ShapeFit.ALL) {
    options.halfExtents = _computeHalfExtents(
      _computeBounds(vertices, matrices),
      options.minHalfExtents,
      options.maxHalfExtents
    );
  }

  const { x, y, z } = options.halfExtents!;
  const collisionShape = (() => {
    switch (options.cylinderAxis) {
      case "x":
        return new Ammo.btCapsuleShapeX(Math.max(y, z), x * 2);
      case "z":
        return new Ammo.btCapsuleShapeZ(Math.max(x, y), z * 2);
      case "y":
      default:
        return new Ammo.btCapsuleShape(Math.max(x, z), y * 2);
    }
  })();

  return finishCollisionShape(
    collisionShape,
    options,
    _computeScale(matrixWorld, options)
  );
}

export function createConeShape(
  vertices,
  matrices,
  matrixWorld,
  options: ShapeConfig
) {
  options.type = ShapeType.CONE;
  _setOptions(options);

  if (options.fit === ShapeFit.ALL) {
    options.halfExtents = _computeHalfExtents(
      _computeBounds(vertices, matrices),
      options.minHalfExtents,
      options.maxHalfExtents
    );
  }

  const { x, y, z } = options.halfExtents!;
  const collisionShape = (() => {
    switch (options.cylinderAxis) {
      case "x":
        return new Ammo.btConeShapeX(Math.max(y, z), x * 2);
      case "z":
        return new Ammo.btConeShapeZ(Math.max(x, y), z * 2);
      case "y":
      default:
        return new Ammo.btConeShape(Math.max(x, z), y * 2);
    }
  })();

  return finishCollisionShape(
    collisionShape,
    options,
    _computeScale(matrixWorld, options)
  );
}

export function createSphereShape(
  vertices,
  matrices,
  matrixWorld,
  options: ShapeConfig
) {
  options.type = ShapeType.SPHERE;
  _setOptions(options);

  let radius;
  if (options.fit === ShapeFit.MANUAL && !isNaN(options.sphereRadius!)) {
    radius = options.sphereRadius;
  } else {
    radius = _computeRadius(
      vertices,
      matrices,
      _computeBounds(vertices, matrices)
    );
  }

  const collisionShape = new Ammo.btSphereShape(radius);
  return finishCollisionShape(
    collisionShape,
    options,
    _computeScale(matrixWorld, options)
  );
}

export const createHullShape = (function () {
  const vertex = new THREE.Vector3();
  const center = new THREE.Vector3();
  const matrix = new THREE.Matrix4();
  return function (vertices, matrices, matrixWorld, options: ShapeConfig) {
    options.type = ShapeType.HULL;
    _setOptions(options);

    if (options.fit === ShapeFit.MANUAL) {
      console.warn("cannot use fit: manual with type: hull");
      return null;
    }

    const bounds = _computeBounds(vertices, matrices);

    const btVertex = new Ammo.btVector3();
    const originalHull = new Ammo.btConvexHullShape();
    originalHull.setMargin(options.margin ?? 0);
    center.addVectors(bounds.max, bounds.min).multiplyScalar(0.5);

    let vertexCount = 0;
    for (let i = 0; i < vertices.length; i++) {
      vertexCount += vertices[i].length / 3;
    }

    const maxVertices = options.hullMaxVertices || 100000;
    // todo: might want to implement this in a deterministic way that doesn't do O(verts) calls to Math.random
    if (vertexCount > maxVertices) {
      console.warn(
        `too many vertices for hull shape; sampling ~${maxVertices} from ~${vertexCount} vertices`
      );
    }
    const p = Math.min(1, maxVertices / vertexCount);

    for (let i = 0; i < vertices.length; i++) {
      const components = vertices[i];
      matrix.fromArray(matrices[i]);
      for (let j = 0; j < components.length; j += 3) {
        const isLastVertex =
          i === vertices.length - 1 && j === components.length - 3;
        if (Math.random() <= p || isLastVertex) {
          // always include the last vertex
          vertex
            .set(components[j], components[j + 1], components[j + 2])
            .applyMatrix4(matrix)
            .sub(center);
          btVertex.setValue(vertex.x, vertex.y, vertex.z);
          originalHull.addPoint(btVertex, isLastVertex); // recalc AABB only on last geometry
        }
      }
    }

    let collisionShape = originalHull;
    if (originalHull.getNumVertices() >= 100) {
      //Bullet documentation says don't use convexHulls with 100 verts or more
      const shapeHull = new Ammo.btShapeHull(originalHull);
      shapeHull.buildHull(options.margin ?? 0);
      Ammo.destroy(originalHull);
      collisionShape = new Ammo.btConvexHullShape(
        // @ts-ignore
        Ammo.getPointer(shapeHull.getVertexPointer()),
        shapeHull.numVertices()
      );
      Ammo.destroy(shapeHull); // btConvexHullShape makes a copy
    }

    Ammo.destroy(btVertex);

    return finishCollisionShape(
      collisionShape,
      options,
      _computeScale(matrixWorld, options)
    );
  };
})();

export const createHACDShapes = (function () {
  const vector = new THREE.Vector3();
  const center = new THREE.Vector3();
  const matrix = new THREE.Matrix4();
  return function (
    vertices,
    matrices,
    indexes,
    matrixWorld,
    options: ShapeConfig
  ) {
    options.type = ShapeType.HACD;
    _setOptions(options);

    if (options.fit === ShapeFit.MANUAL) {
      console.warn("cannot use fit: manual with type: hacd");
      return [];
    }

    if (!Ammo.hasOwnProperty("HACD")) {
      console.warn(
        "HACD unavailable in included build of Ammo.js. Visit https://github.com/mozillareality/ammo.js for the latest version."
      );
      return [];
    }

    const bounds = _computeBounds(vertices, matrices);
    const scale = _computeScale(matrixWorld, options);

    let vertexCount = 0;
    let triCount = 0;
    center.addVectors(bounds.max, bounds.min).multiplyScalar(0.5);

    for (let i = 0; i < vertices.length; i++) {
      vertexCount += vertices[i].length / 3;
      if (indexes && indexes[i]) {
        triCount += indexes[i].length / 3;
      } else {
        triCount += vertices[i].length / 9;
      }
    }

    // @ts-ignore
    const hacd = new Ammo.HACD();
    if (options.hasOwnProperty("compacityWeight"))
      hacd.SetCompacityWeight(options.compacityWeight);
    if (options.hasOwnProperty("volumeWeight"))
      hacd.SetVolumeWeight(options.volumeWeight);
    if (options.hasOwnProperty("nClusters"))
      hacd.SetNClusters(options.nClusters);
    if (options.hasOwnProperty("nVerticesPerCH"))
      hacd.SetNVerticesPerCH(options.nVerticesPerCH);
    if (options.hasOwnProperty("concavity"))
      hacd.SetConcavity(options.concavity);

    const points = Ammo._malloc(vertexCount * 3 * 8);
    const triangles = Ammo._malloc(triCount * 3 * 4);
    hacd.SetPoints(points);
    hacd.SetTriangles(triangles);
    hacd.SetNPoints(vertexCount);
    hacd.SetNTriangles(triCount);

    let pptr = points / 8,
      tptr = triangles / 4;

    for (let i = 0; i < vertices.length; i++) {
      const components = vertices[i];
      matrix.fromArray(matrices[i]);
      for (let j = 0; j < components.length; j += 3) {
        vector
          .set(components[j + 0], components[j + 1], components[j + 2])
          .applyMatrix4(matrix)
          .sub(center);
        Ammo.HEAPF64[pptr + 0] = vector.x;
        Ammo.HEAPF64[pptr + 1] = vector.y;
        Ammo.HEAPF64[pptr + 2] = vector.z;
        pptr += 3;
      }
      if (indexes[i]) {
        const indices = indexes[i];
        for (let j = 0; j < indices.length; j++) {
          Ammo.HEAP32[tptr] = indices[j];
          tptr++;
        }
      } else {
        for (let j = 0; j < components.length / 3; j++) {
          Ammo.HEAP32[tptr] = j;
          tptr++;
        }
      }
    }

    hacd.Compute();
    Ammo._free(points);
    Ammo._free(triangles);
    const nClusters = hacd.GetNClusters();

    const shapes: FinalizedShape[] = [];
    for (let i = 0; i < nClusters; i++) {
      const hull = new Ammo.btConvexHullShape();
      hull.setMargin(options.margin ?? 0);
      const nPoints = hacd.GetNPointsCH(i);
      const nTriangles = hacd.GetNTrianglesCH(i);
      const hullPoints = Ammo._malloc(nPoints * 3 * 8);
      const hullTriangles = Ammo._malloc(nTriangles * 3 * 4);
      hacd.GetCH(i, hullPoints, hullTriangles);

      const pptr = hullPoints / 8;
      for (let pi = 0; pi < nPoints; pi++) {
        const btVertex = new Ammo.btVector3();
        const px = Ammo.HEAPF64[pptr + pi * 3 + 0];
        const py = Ammo.HEAPF64[pptr + pi * 3 + 1];
        const pz = Ammo.HEAPF64[pptr + pi * 3 + 2];
        btVertex.setValue(px, py, pz);
        hull.addPoint(btVertex, pi === nPoints - 1);
        Ammo.destroy(btVertex);
      }

      shapes.push(finishCollisionShape(hull, options, scale));
    }

    return shapes;
  };
})();

export const createVHACDShapes = (function () {
  const vector = new THREE.Vector3();
  const center = new THREE.Vector3();
  const matrix = new THREE.Matrix4();
  return function (
    vertices,
    matrices,
    indexes,
    matrixWorld,
    options: ShapeConfig
  ) {
    options.type = ShapeType.VHACD;
    _setOptions(options);

    if (options.fit === ShapeFit.MANUAL) {
      console.warn("cannot use fit: manual with type: vhacd");
      return [];
    }

    if (!Ammo.hasOwnProperty("VHACD")) {
      console.warn(
        "VHACD unavailable in included build of Ammo.js. Visit https://github.com/mozillareality/ammo.js for the latest version."
      );
      return [];
    }

    const bounds = _computeBounds(vertices, matrices);
    const scale = _computeScale(matrixWorld, options);

    let vertexCount = 0;
    let triCount = 0;
    center.addVectors(bounds.max, bounds.min).multiplyScalar(0.5);

    for (let i = 0; i < vertices.length; i++) {
      vertexCount += vertices[i].length / 3;
      if (indexes && indexes[i]) {
        triCount += indexes[i].length / 3;
      } else {
        triCount += vertices[i].length / 9;
      }
    }

    // @ts-ignore
    const vhacd = new Ammo.VHACD();
    // @ts-ignore
    const params = new Ammo.Parameters();
    //https://kmamou.blogspot.com/2014/12/v-hacd-20-parameters-description.html
    if (options.hasOwnProperty("resolution"))
      params.set_m_resolution(options.resolution);
    if (options.hasOwnProperty("depth")) params.set_m_depth(options.depth);
    if (options.hasOwnProperty("concavity"))
      params.set_m_concavity(options.concavity);
    if (options.hasOwnProperty("planeDownsampling"))
      params.set_m_planeDownsampling(options.planeDownsampling);
    if (options.hasOwnProperty("convexhullDownsampling"))
      params.set_m_convexhullDownsampling(options.convexhullDownsampling);
    if (options.hasOwnProperty("alpha")) params.set_m_alpha(options.alpha);
    if (options.hasOwnProperty("beta")) params.set_m_beta(options.beta);
    if (options.hasOwnProperty("gamma")) params.set_m_gamma(options.gamma);
    if (options.hasOwnProperty("pca")) params.set_m_pca(options.pca);
    if (options.hasOwnProperty("mode")) params.set_m_mode(options.mode);
    if (options.hasOwnProperty("maxNumVerticesPerCH"))
      params.set_m_maxNumVerticesPerCH(options.maxNumVerticesPerCH);
    if (options.hasOwnProperty("minVolumePerCH"))
      params.set_m_minVolumePerCH(options.minVolumePerCH);
    if (options.hasOwnProperty("convexhullApproximation"))
      params.set_m_convexhullApproximation(options.convexhullApproximation);
    if (options.hasOwnProperty("oclAcceleration"))
      params.set_m_oclAcceleration(options.oclAcceleration);

    const points = Ammo._malloc(vertexCount * 3 * 8 + 3);
    const triangles = Ammo._malloc(triCount * 3 * 4);

    let pptr = points / 8,
      tptr = triangles / 4;

    for (let i = 0; i < vertices.length; i++) {
      const components = vertices[i];
      matrix.fromArray(matrices[i]);
      for (let j = 0; j < components.length; j += 3) {
        vector
          .set(components[j + 0], components[j + 1], components[j + 2])
          .applyMatrix4(matrix)
          .sub(center);
        Ammo.HEAPF64[pptr + 0] = vector.x;
        Ammo.HEAPF64[pptr + 1] = vector.y;
        Ammo.HEAPF64[pptr + 2] = vector.z;
        pptr += 3;
      }
      if (indexes[i]) {
        const indices = indexes[i];
        for (let j = 0; j < indices.length; j++) {
          Ammo.HEAP32[tptr] = indices[j];
          tptr++;
        }
      } else {
        for (let j = 0; j < components.length / 3; j++) {
          Ammo.HEAP32[tptr] = j;
          tptr++;
        }
      }
    }
    vhacd.Compute(points, 3, vertexCount, triangles, 3, triCount, params);
    Ammo._free(points);
    Ammo._free(triangles);
    const nHulls = vhacd.GetNConvexHulls();

    const shapes: FinalizedShape[] = [];
    // @ts-ignore
    const ch = new Ammo.ConvexHull();
    for (let i = 0; i < nHulls; i++) {
      vhacd.GetConvexHull(i, ch);
      const nPoints = ch.get_m_nPoints();
      const hullPoints = ch.get_m_points();

      const hull = new Ammo.btConvexHullShape();
      hull.setMargin(options.margin ?? 0);

      for (let pi = 0; pi < nPoints; pi++) {
        const btVertex = new Ammo.btVector3();
        const px = ch.get_m_points(pi * 3 + 0);
        const py = ch.get_m_points(pi * 3 + 1);
        const pz = ch.get_m_points(pi * 3 + 2);
        btVertex.setValue(px, py, pz);
        hull.addPoint(btVertex, pi === nPoints - 1);
        Ammo.destroy(btVertex);
      }

      shapes.push(finishCollisionShape(hull, options, scale));
    }
    Ammo.destroy(ch);
    Ammo.destroy(vhacd);

    return shapes;
  };
})();

export const createTriMeshShape = (function () {
  const va = new THREE.Vector3();
  const vb = new THREE.Vector3();
  const vc = new THREE.Vector3();
  const matrix = new THREE.Matrix4();
  return function (
    vertices,
    matrices,
    indexes,
    matrixWorld,
    options: ShapeConfig
  ) {
    options.type = ShapeType.MESH;
    _setOptions(options);

    if (options.fit === ShapeFit.MANUAL) {
      console.warn("cannot use fit: manual with type: mesh");
      return null;
    }

    const scale = _computeScale(matrixWorld, options);

    const bta = new Ammo.btVector3();
    const btb = new Ammo.btVector3();
    const btc = new Ammo.btVector3();
    const triMesh = new Ammo.btTriangleMesh(true, false);

    for (let i = 0; i < vertices.length; i++) {
      const components = vertices[i];
      const index = indexes[i] ? indexes[i] : null;
      matrix.fromArray(matrices[i]);
      if (index) {
        for (let j = 0; j < index.length; j += 3) {
          const ai = index[j] * 3;
          const bi = index[j + 1] * 3;
          const ci = index[j + 2] * 3;
          va.set(
            components[ai],
            components[ai + 1],
            components[ai + 2]
          ).applyMatrix4(matrix);
          vb.set(
            components[bi],
            components[bi + 1],
            components[bi + 2]
          ).applyMatrix4(matrix);
          vc.set(
            components[ci],
            components[ci + 1],
            components[ci + 2]
          ).applyMatrix4(matrix);
          bta.setValue(va.x, va.y, va.z);
          btb.setValue(vb.x, vb.y, vb.z);
          btc.setValue(vc.x, vc.y, vc.z);
          triMesh.addTriangle(bta, btb, btc, false);
        }
      } else {
        for (let j = 0; j < components.length; j += 9) {
          va.set(
            components[j + 0],
            components[j + 1],
            components[j + 2]
          ).applyMatrix4(matrix);
          vb.set(
            components[j + 3],
            components[j + 4],
            components[j + 5]
          ).applyMatrix4(matrix);
          vc.set(
            components[j + 6],
            components[j + 7],
            components[j + 8]
          ).applyMatrix4(matrix);
          bta.setValue(va.x, va.y, va.z);
          btb.setValue(vb.x, vb.y, vb.z);
          btc.setValue(vc.x, vc.y, vc.z);
          triMesh.addTriangle(bta, btb, btc, false);
        }
      }
    }

    const localScale = new Ammo.btVector3(scale.x, scale.y, scale.z);
    triMesh.setScaling(localScale);
    Ammo.destroy(localScale);

    const collisionShape = new Ammo.btBvhTriangleMeshShape(triMesh, true, true);

    const triangleInfoMap = new Ammo.btTriangleInfoMap();

    if (options.computeInternalEdgeInfo ?? true) {
      collisionShape.generateInternalEdgeInfo(triangleInfoMap);
    }

    ((collisionShape as unknown) as FinalizedShape).resources = [
      triMesh,
      triangleInfoMap,
    ];

    Ammo.destroy(bta);
    Ammo.destroy(btb);
    Ammo.destroy(btc);

    const finalizedShape = finishCollisionShape(collisionShape, options);
    finalizedShape.setMargin(0);
    return finalizedShape;
  };
})();

export function createHeightfieldTerrainShape(options: ShapeConfig) {
  _setOptions(options);

  if (options.fit === ShapeFit.ALL) {
    console.warn("cannot use fit: all with type: heightfield");
    return null;
  }
  const heightfieldDistance = options.heightfieldDistance || 1;
  const heightfieldData = options.heightfieldData || [];
  const heightScale = options.heightScale || 0;
  const upAxis = options.upAxis ?? 1; // x = 0; y = 1; z = 2
  const hdt = (() => {
    switch (options.heightDataType) {
      case "short":
        // @ts-ignore
        return Ammo.PHY_SHORT;
      case "float":
        // @ts-ignore
        return Ammo.PHY_FLOAT;
      default:
        // @ts-ignore
        return Ammo.PHY_FLOAT;
    }
  })();
  const flipQuadEdges = options.hasOwnProperty("flipQuadEdges")
    ? options.flipQuadEdges
    : true;

  const heightStickLength = heightfieldData.length;
  const heightStickWidth =
    heightStickLength > 0 ? heightfieldData[0].length : 0;

  const data = Ammo._malloc(heightStickLength * heightStickWidth * 4);
  const ptr = data / 4;

  let minHeight = Number.POSITIVE_INFINITY;
  let maxHeight = Number.NEGATIVE_INFINITY;
  let index = 0;
  for (let l = 0; l < heightStickLength; l++) {
    for (let w = 0; w < heightStickWidth; w++) {
      const height = heightfieldData[l][w];
      Ammo.HEAPF32[ptr + index] = height;
      index++;
      minHeight = Math.min(minHeight, height);
      maxHeight = Math.max(maxHeight, height);
    }
  }

  const collisionShape = new Ammo.btHeightfieldTerrainShape(
    heightStickWidth,
    heightStickLength,
    data,
    heightScale,
    minHeight,
    maxHeight,
    upAxis,
    hdt,
    flipQuadEdges ?? false
  );

  const scale = new Ammo.btVector3(heightfieldDistance, 1, heightfieldDistance);
  collisionShape.setLocalScaling(scale);
  Ammo.destroy(scale);
  // @ts-ignore
  collisionShape.heightfieldData = data;

  return finishCollisionShape(collisionShape, options);
}

function _setOptions(options: ShapeConfig) {
  options.fit = options.fit ?? ShapeFit.ALL;
  options.minHalfExtents = options.minHalfExtents ?? 0;
  options.maxHalfExtents = options.maxHalfExtents ?? Number.POSITIVE_INFINITY;
}

function finishCollisionShape(
  collisionShape: Ammo.btCollisionShape,
  options: ShapeConfig,
  scale?: Vector3
): FinalizedShape {
  collisionShape.setMargin(options.margin ?? 0);

  const localTransform = new Ammo.btTransform();
  const rotation = new Ammo.btQuaternion(0, 0, 0, 1);
  localTransform.setIdentity();

  if (options.offset) {
    localTransform
      .getOrigin()
      .setValue(-options.offset.x, -options.offset.y, -options.offset.z);
  }

  toBtQuaternion(rotation, options.orientation ?? new Quaternion());
  const invertedRotation = rotation.inverse();
  localTransform.setRotation(invertedRotation);

  Ammo.destroy(rotation);
  Ammo.destroy(invertedRotation);

  if (scale) {
    const localScale = new Ammo.btVector3(scale.x, scale.y, scale.z);
    collisionShape.setLocalScaling(localScale);
    Ammo.destroy(localScale);
  }

  return Object.assign(collisionShape, {
    type: options.type,
    localTransform,
    destroy() {
      const finalizedShape = collisionShape as FinalizedShape;

      for (let res of finalizedShape.resources || []) {
        Ammo.destroy(res);
      }

      if (finalizedShape.heightfieldData) {
        Ammo._free(finalizedShape.heightfieldData);
      }

      if (finalizedShape.shapes) {
        for (const shape of finalizedShape.shapes) {
          shape.destroy();
        }
      }

      Ammo.destroy(collisionShape);
    },
  });
}

export const iterateGeometries = (function () {
  const inverse = new THREE.Matrix4();
  return function (root, options, cb) {
    inverse.copy(root.matrixWorld).invert();
    const scale = new THREE.Vector3();
    scale.setFromMatrixScale(root.matrixWorld);
    root.traverse((mesh) => {
      const transform = new THREE.Matrix4();
      if (
        mesh.isMesh &&
        mesh.name !== "Sky" &&
        (options.includeInvisible ||
          (mesh.el && mesh.el.object3D.visible) ||
          mesh.visible)
      ) {
        if (mesh === root) {
          transform.identity();
        } else {
          mesh.updateWorldMatrix(true);
          transform.multiplyMatrices(inverse, mesh.matrixWorld);
        }
        // todo: might want to return null xform if this is the root so that callers can avoid multiplying
        // things by the identity matrix
        cb(
          mesh.geometry.isBufferGeometry
            ? mesh.geometry.attributes.position.array
            : mesh.geometry.vertices,
          transform.elements,
          mesh.geometry.index ? mesh.geometry.index.array : null
        );
      }
    });
  };
})();

const _computeScale = (function () {
  const matrix = new THREE.Matrix4();
  return function (matrixWorld, options: Pick<ShapeConfig, "fit"> = {}) {
    const scale = new THREE.Vector3(1, 1, 1);
    if (options.fit === ShapeFit.ALL) {
      matrix.fromArray(matrixWorld);
      scale.setFromMatrixScale(matrix);
    }
    return scale;
  };
})();

const _computeRadius = (function () {
  const center = new THREE.Vector3();
  return function (vertices, matrices, bounds) {
    let maxRadiusSq = 0;
    let { x: cx, y: cy, z: cz } = bounds.getCenter(center);

    _iterateVertices(vertices, matrices, (v) => {
      const dx = cx - v.x;
      const dy = cy - v.y;
      const dz = cz - v.z;
      maxRadiusSq = Math.max(maxRadiusSq, dx * dx + dy * dy + dz * dz);
    });
    return Math.sqrt(maxRadiusSq);
  };
})();

const _computeHalfExtents = function (bounds, minHalfExtent, maxHalfExtent) {
  const halfExtents = new THREE.Vector3();
  return halfExtents
    .subVectors(bounds.max, bounds.min)
    .multiplyScalar(0.5)
    .clampScalar(minHalfExtent, maxHalfExtent);
};

const _computeLocalOffset = function (matrix, bounds, target) {
  target
    .addVectors(bounds.max, bounds.min)
    .multiplyScalar(0.5)
    .applyMatrix4(matrix);
  return target;
};

// returns the bounding box for the geometries underneath `root`.
const _computeBounds = function (vertices, matrices) {
  const bounds = new THREE.Box3();
  let minX = +Infinity;
  let minY = +Infinity;
  let minZ = +Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;
  let maxZ = -Infinity;
  bounds.min.set(0, 0, 0);
  bounds.max.set(0, 0, 0);

  _iterateVertices(vertices, matrices, (v) => {
    if (v.x < minX) minX = v.x;
    if (v.y < minY) minY = v.y;
    if (v.z < minZ) minZ = v.z;
    if (v.x > maxX) maxX = v.x;
    if (v.y > maxY) maxY = v.y;
    if (v.z > maxZ) maxZ = v.z;
  });

  bounds.min.set(minX, minY, minZ);
  bounds.max.set(maxX, maxY, maxZ);
  return bounds;
};

const _iterateVertices = (function () {
  const vertex = new THREE.Vector3();
  const matrix = new THREE.Matrix4();
  return function (vertices, matrices, cb) {
    for (let i = 0; i < vertices.length; i++) {
      matrix.fromArray(matrices[i]);
      for (let j = 0; j < vertices[i].length; j += 3) {
        vertex
          .set(vertices[i][j], vertices[i][j + 1], vertices[i][j + 2])
          .applyMatrix4(matrix);
        cb(vertex);
      }
    }
  };
})();