import { createPortal } from "@react-three/fiber";
import { useTexture } from "@react-three/drei";
import { useEffect, useMemo } from "react";
import * as THREE from "three";
import {
  CHARACTER_PART_CATALOG,
  characterAppearancePartIds,
  type CharacterAppearanceV1,
  type CharacterMaterialVariant,
  type CharacterPartId,
} from "../game/characterAppearance";
import type { MemberId } from "../game/types";
import { useGameGLTF } from "./GameAssetProvider";

interface BoundCharacterPart {
  group: THREE.Group;
  materials: THREE.Material[];
  skeletons: THREE.Skeleton[];
}

const CHARACTER_MATERIAL_VARIANT_URLS: Record<CharacterMaterialVariant, string> = {
  "priest-vestments": new URL("../assets/game/textures/claudecraft/chars/players/priest-vestments.webp", import.meta.url).href,
};

function rigBonesByName(rigScene: THREE.Object3D) {
  const bones = new Map<string, THREE.Bone>();
  rigScene.traverse((object) => {
    if (object instanceof THREE.Bone) bones.set(object.name, object);
  });
  return bones;
}

function createBoundCharacterPart(
  sourceScene: THREE.Object3D,
  rigScene: THREE.Object3D,
  partId: CharacterPartId,
  materialTexture?: THREE.Texture,
): BoundCharacterPart {
  const definition = CHARACTER_PART_CATALOG[partId];
  const rigBones = rigBonesByName(rigScene);
  const group = new THREE.Group();
  group.name = `character-part:${partId}`;
  const materialClones = new Map<THREE.Material, THREE.Material>();
  const skeletons: THREE.Skeleton[] = [];

  sourceScene.updateMatrixWorld(true);
  for (const nodeName of definition.nodeNames) {
    const sourceNode = sourceScene.getObjectByName(nodeName);
    if (!sourceNode) throw new Error(`Character part ${partId} is missing node ${nodeName}.`);

    const partNode = sourceNode.clone(true);
    partNode.matrix.copy(sourceNode.matrixWorld);
    partNode.matrix.decompose(partNode.position, partNode.quaternion, partNode.scale);
    partNode.traverse((object) => {
      if (!(object instanceof THREE.SkinnedMesh)) return;
      if (materialTexture) {
        const cloneMaterial = (source: THREE.Material) => {
          const existing = materialClones.get(source);
          if (existing) return existing;
          const clone = source.clone();
          if ("map" in clone) clone.map = materialTexture;
          clone.needsUpdate = true;
          materialClones.set(source, clone);
          return clone;
        };
        object.material = Array.isArray(object.material)
          ? object.material.map(cloneMaterial)
          : cloneMaterial(object.material);
      }
      const mappedBones = object.skeleton.bones.map((sourceBone) => {
        const rigBone = rigBones.get(sourceBone.name);
        if (!rigBone) throw new Error(`Character part ${partId} cannot resolve rig bone ${sourceBone.name}.`);
        return rigBone;
      });
      const skeleton = new THREE.Skeleton(
        mappedBones,
        object.skeleton.boneInverses.map((inverse) => inverse.clone()),
      );
      const bindMatrix = object.bindMatrix.clone();
      object.bind(skeleton, bindMatrix);
      object.castShadow = true;
      object.receiveShadow = true;
      object.frustumCulled = false;
      skeletons.push(skeleton);
    });
    group.add(partNode);
  }

  return { group, materials: [...materialClones.values()], skeletons };
}

function BoundCharacterPart({
  actorScene,
  materialTexture,
  modelUrls,
  partId,
}: {
  actorScene: THREE.Object3D;
  materialTexture?: THREE.Texture;
  modelUrls: Record<MemberId, string>;
  partId: CharacterPartId;
}) {
  const definition = CHARACTER_PART_CATALOG[partId];
  const gltf = useGameGLTF(modelUrls[definition.sourceMemberId]);
  const boundPart = useMemo(
    () => createBoundCharacterPart(gltf.scene, actorScene, partId, materialTexture),
    [actorScene, gltf.scene, materialTexture, partId],
  );

  useEffect(() => () => {
    for (const skeleton of boundPart.skeletons) skeleton.dispose();
    for (const material of boundPart.materials) material.dispose();
  }, [boundPart]);

  const rigRoot = actorScene.getObjectByName("Rig_Medium") ?? actorScene;
  return createPortal(<primitive object={boundPart.group} />, rigRoot);
}

function MaterialVariantCharacterPart({
  actorScene,
  materialVariant,
  modelUrls,
  partId,
}: {
  actorScene: THREE.Object3D;
  materialVariant: CharacterMaterialVariant;
  modelUrls: Record<MemberId, string>;
  partId: CharacterPartId;
}) {
  const sourceTexture = useTexture(CHARACTER_MATERIAL_VARIANT_URLS[materialVariant]);
  const materialTexture = useMemo(() => {
    sourceTexture.flipY = false;
    sourceTexture.colorSpace = THREE.SRGBColorSpace;
    sourceTexture.magFilter = THREE.NearestFilter;
    sourceTexture.needsUpdate = true;
    return sourceTexture;
  }, [sourceTexture]);
  return <BoundCharacterPart actorScene={actorScene} materialTexture={materialTexture} modelUrls={modelUrls} partId={partId} />;
}

function ModularCharacterPart({
  actorScene,
  modelUrls,
  partId,
}: {
  actorScene: THREE.Object3D;
  modelUrls: Record<MemberId, string>;
  partId: CharacterPartId;
}) {
  const definition = CHARACTER_PART_CATALOG[partId];
  const materialVariant = "materialVariant" in definition ? definition.materialVariant : undefined;
  return materialVariant
    ? <MaterialVariantCharacterPart actorScene={actorScene} materialVariant={materialVariant} modelUrls={modelUrls} partId={partId} />
    : <BoundCharacterPart actorScene={actorScene} modelUrls={modelUrls} partId={partId} />;
}

export function ModularCharacterBody({
  actorScene,
  appearance,
  modelUrls,
}: {
  actorScene: THREE.Object3D;
  appearance: CharacterAppearanceV1;
  modelUrls: Record<MemberId, string>;
}) {
  return (
    <>
      {characterAppearancePartIds(appearance).map((partId) => (
        <ModularCharacterPart
          key={`${partId}:${appearance.version}`}
          actorScene={actorScene}
          modelUrls={modelUrls}
          partId={partId}
        />
      ))}
    </>
  );
}
