import {
  BoxGeometry,
  BufferGeometry,
  Float32BufferAttribute,
  Group,
  Mesh,
  MeshBasicMaterial,
  Vector3,
  type Material
} from "three";
import { ConvexGeometry } from "three/examples/jsm/geometries/ConvexGeometry.js";

import type {
  GeneratedModelBoxCollider,
  GeneratedModelCollider,
  GeneratedModelCompoundCollider,
  GeneratedModelHeightfieldCollider,
  GeneratedModelTriMeshCollider
} from "./model-instance-collider-generation";

const DEBUG_COLLIDER_COLORS = {
  simple: 0x87d2ff,
  terrain: 0x7be7b4,
  static: 0xffc66d,
  "static-simple": 0xa3ff9c,
  dynamic: 0xff8b7a
} as const;

function createWireframeMaterial(color: number): MeshBasicMaterial {
  return new MeshBasicMaterial({
    color,
    wireframe: true,
    transparent: true,
    opacity: 0.85,
    depthWrite: false,
    toneMapped: false
  });
}

function markDebugMesh(mesh: Mesh) {
  mesh.userData.shadowIgnored = true;
  mesh.userData.nonPickable = true;
  mesh.renderOrder = 3_500;
}

function createBoxColliderDebugMesh(collider: GeneratedModelBoxCollider): Mesh {
  const mesh = new Mesh(new BoxGeometry(collider.size.x, collider.size.y, collider.size.z), createWireframeMaterial(DEBUG_COLLIDER_COLORS.simple));
  mesh.position.set(collider.center.x, collider.center.y, collider.center.z);
  markDebugMesh(mesh);
  return mesh;
}

function createTriMeshColliderDebugMesh(collider: GeneratedModelTriMeshCollider): Mesh {
  const geometry = new BufferGeometry();

  geometry.setAttribute("position", new Float32BufferAttribute(collider.vertices, 3));
  geometry.setIndex(Array.from(collider.indices));

  const mesh = new Mesh(geometry, createWireframeMaterial(DEBUG_COLLIDER_COLORS.static));
  markDebugMesh(mesh);
  return mesh;
}

function createHeightfieldColliderDebugMesh(collider: GeneratedModelHeightfieldCollider): Mesh {
  const vertices: number[] = [];
  const indices: number[] = [];
  const width = collider.maxX - collider.minX;
  const depth = collider.maxZ - collider.minZ;

  for (let zIndex = 0; zIndex < collider.cols; zIndex += 1) {
    const zLerp = collider.cols === 1 ? 0 : zIndex / (collider.cols - 1);
    const z = collider.minZ + depth * zLerp;

    for (let xIndex = 0; xIndex < collider.rows; xIndex += 1) {
      const xLerp = collider.rows === 1 ? 0 : xIndex / (collider.rows - 1);
      const x = collider.minX + width * xLerp;
      const y = collider.heights[xIndex + zIndex * collider.rows];

      vertices.push(x, y, z);
    }
  }

  for (let zIndex = 0; zIndex < collider.cols - 1; zIndex += 1) {
    for (let xIndex = 0; xIndex < collider.rows - 1; xIndex += 1) {
      const topLeft = xIndex + zIndex * collider.rows;
      const topRight = topLeft + 1;
      const bottomLeft = topLeft + collider.rows;
      const bottomRight = bottomLeft + 1;

      indices.push(topLeft, bottomLeft, bottomRight, topLeft, bottomRight, topRight);
    }
  }

  const geometry = new BufferGeometry();

  geometry.setAttribute("position", new Float32BufferAttribute(vertices, 3));
  geometry.setIndex(indices);

  const mesh = new Mesh(geometry, createWireframeMaterial(DEBUG_COLLIDER_COLORS.terrain));
  markDebugMesh(mesh);
  return mesh;
}

function createCompoundColliderDebugGroup(collider: GeneratedModelCompoundCollider): Group {
  const group = new Group();
  const color = collider.mode === "static-simple" ? DEBUG_COLLIDER_COLORS["static-simple"] : DEBUG_COLLIDER_COLORS.dynamic;

  for (const piece of collider.pieces) {
    if (piece.kind === "box") {
      const mesh = new Mesh(new BoxGeometry(piece.size.x, piece.size.y, piece.size.z), createWireframeMaterial(color));
      mesh.position.set(piece.center.x, piece.center.y, piece.center.z);
      markDebugMesh(mesh);
      group.add(mesh);
      continue;
    }

    const points: Vector3[] = [];

    for (let index = 0; index < piece.points.length; index += 3) {
      points.push(new Vector3(piece.points[index], piece.points[index + 1], piece.points[index + 2]));
    }

    const mesh = new Mesh(new ConvexGeometry(points), createWireframeMaterial(color));
    markDebugMesh(mesh);
    group.add(mesh);
  }

  return group;
}

export function createModelColliderDebugGroup(collider: GeneratedModelCollider): Group {
  const group = new Group();

  switch (collider.kind) {
    case "box":
      group.add(createBoxColliderDebugMesh(collider));
      break;
    case "trimesh":
      group.add(createTriMeshColliderDebugMesh(collider));
      break;
    case "heightfield":
      group.add(createHeightfieldColliderDebugMesh(collider));
      break;
    case "compound":
      group.add(createCompoundColliderDebugGroup(collider));
      break;
  }

  group.userData.nonPickable = true;
  return group;
}

function disposeMaterial(material: Material | Material[]) {
  if (Array.isArray(material)) {
    for (const item of material) {
      item.dispose();
    }
    return;
  }

  material.dispose();
}

export function disposeModelColliderDebugGroup(group: Group) {
  group.traverse((object) => {
    const maybeMesh = object as Mesh & { isMesh?: boolean };

    if (maybeMesh.isMesh !== true) {
      return;
    }

    maybeMesh.geometry.dispose();
    disposeMaterial(maybeMesh.material);
  });
}
