/* eslint-disable react/no-unknown-property */
"use client";

import { Canvas, extend, useFrame, type ThreeElement, type ThreeEvent } from "@react-three/fiber";
import {
  BallCollider,
  CuboidCollider,
  Physics,
  RigidBody,
  useRopeJoint,
  useSphericalJoint,
  type RapierRigidBody,
  type RigidBodyProps,
} from "@react-three/rapier";
import { MeshLineGeometry, MeshLineMaterial } from "meshline";
import { Suspense, useEffect, useRef, useState } from "react";
import * as THREE from "three";

extend({ MeshLineGeometry, MeshLineMaterial });

declare module "@react-three/fiber" {
  interface ThreeElements {
    meshLineGeometry: ThreeElement<typeof MeshLineGeometry>;
    meshLineMaterial: ThreeElement<typeof MeshLineMaterial>;
  }
}

type LanyardProps = {
  role: string;
};

type LanyardBody = RapierRigidBody & { lerped?: THREE.Vector3 };

function drawCover(
  context: CanvasRenderingContext2D,
  image: HTMLImageElement,
  x: number,
  y: number,
  width: number,
  height: number,
) {
  const scale = Math.max(width / image.width, height / image.height);
  const sourceWidth = width / scale;
  const sourceHeight = height / scale;
  const sourceX = (image.width - sourceWidth) / 2;
  const sourceY = (image.height - sourceHeight) / 2;
  context.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, x, y, width, height);
}

function useIdentityTexture(role: string) {
  const [texture, setTexture] = useState<THREE.CanvasTexture | null>(null);

  useEffect(() => {
    let disposed = false;
    const canvas = document.createElement("canvas");
    canvas.width = 800;
    canvas.height = 1120;
    const context = canvas.getContext("2d");
    if (!context) return;

    const render = (photo?: HTMLImageElement) => {
      const background = context.createLinearGradient(0, 0, 800, 1120);
      background.addColorStop(0, "#161c1e");
      background.addColorStop(0.58, "#090c0d");
      background.addColorStop(1, "#101514");
      context.fillStyle = background;
      context.fillRect(0, 0, 800, 1120);

      context.strokeStyle = "rgba(190, 242, 100, .16)";
      context.lineWidth = 2;
      for (let x = -420; x < 980; x += 72) {
        context.beginPath();
        context.moveTo(x, 0);
        context.lineTo(x + 600, 1120);
        context.stroke();
      }

      context.fillStyle = "#bff45d";
      context.fillRect(0, 0, 800, 18);
      context.fillStyle = "#d9ff8b";
      context.font = "700 25px Arial";
      context.letterSpacing = "6px";
      context.fillText("FR / DIGITAL MAKER", 56, 74);

      context.save();
      context.beginPath();
      context.roundRect(52, 118, 696, 600, 32);
      context.clip();
      context.fillStyle = "#1c2426";
      context.fillRect(52, 118, 696, 600);
      if (photo) drawCover(context, photo, 52, 118, 696, 600);
      const fade = context.createLinearGradient(0, 430, 0, 718);
      fade.addColorStop(0, "rgba(6, 9, 10, 0)");
      fade.addColorStop(1, "rgba(6, 9, 10, .78)");
      context.fillStyle = fade;
      context.fillRect(52, 420, 696, 298);
      context.restore();

      context.fillStyle = "#f3f6ed";
      context.font = "800 74px Arial";
      context.fillText("FATHOR", 52, 824);
      context.fillText("ROZI", 52, 900);

      context.fillStyle = "#bff45d";
      context.font = "700 27px Arial";
      context.letterSpacing = "2px";
      context.fillText(role.toUpperCase().slice(0, 34), 56, 958);

      context.fillStyle = "#899295";
      context.font = "600 22px Arial";
      context.letterSpacing = "4px";
      context.fillText("SUMENEP  •  INDONESIA", 56, 1031);

      context.fillStyle = "#d9ff8b";
      for (let index = 0; index < 34; index += 1) {
        const barWidth = index % 4 === 0 ? 7 : 3;
        context.fillRect(544 + index * 6, 998, barWidth, 46);
      }

      const nextTexture = new THREE.CanvasTexture(canvas);
      nextTexture.colorSpace = THREE.SRGBColorSpace;
      nextTexture.anisotropy = 8;
      nextTexture.needsUpdate = true;
      if (!disposed) setTexture(nextTexture);
      else nextTexture.dispose();
    };

    render();
    const image = new Image();
    image.src = "/assets/fathorrozi.jpg";
    image.onload = () => render(image);

    return () => {
      disposed = true;
      setTexture((current) => {
        current?.dispose();
        return null;
      });
    };
  }, [role]);

  return texture;
}

function Band({ role, mobile }: { role: string; mobile: boolean }) {
  const band = useRef<THREE.Mesh<
    InstanceType<typeof MeshLineGeometry>,
    InstanceType<typeof MeshLineMaterial>
  >>(null!);
  const fixed = useRef<RapierRigidBody>(null!);
  const joint1 = useRef<LanyardBody>(null!);
  const joint2 = useRef<LanyardBody>(null!);
  const joint3 = useRef<RapierRigidBody>(null!);
  const card = useRef<RapierRigidBody>(null!);
  const [dragged, setDragged] = useState<false | THREE.Vector3>(false);
  const [hovered, setHovered] = useState(false);
  const [curve] = useState(
    () => new THREE.CatmullRomCurve3([
      new THREE.Vector3(),
      new THREE.Vector3(),
      new THREE.Vector3(),
      new THREE.Vector3(),
    ]),
  );
  const texture = useIdentityTexture(role);
  const vector = useRef(new THREE.Vector3()).current;
  const direction = useRef(new THREE.Vector3()).current;
  const angular = useRef(new THREE.Vector3()).current;
  const rotation = useRef(new THREE.Vector3()).current;

  const segmentProps: RigidBodyProps = {
    type: "dynamic",
    canSleep: true,
    colliders: false,
    angularDamping: 4,
    linearDamping: 4,
  };

  useRopeJoint(fixed, joint1, [[0, 0, 0], [0, 0, 0], 1]);
  useRopeJoint(joint1, joint2, [[0, 0, 0], [0, 0, 0], 1]);
  useRopeJoint(joint2, joint3, [[0, 0, 0], [0, 0, 0], 1]);
  useSphericalJoint(joint3, card, [[0, 0, 0], [0, 1.82, 0]]);

  useEffect(() => {
    if (!hovered) return;
    document.body.style.cursor = dragged ? "grabbing" : "grab";
    return () => {
      document.body.style.cursor = "auto";
    };
  }, [dragged, hovered]);

  useFrame((state, delta) => {
    if (dragged) {
      vector.set(state.pointer.x, state.pointer.y, 0.5).unproject(state.camera);
      direction.copy(vector).sub(state.camera.position).normalize();
      vector.add(direction.multiplyScalar(state.camera.position.length()));
      [card, joint1, joint2, joint3, fixed].forEach((body) => body.current?.wakeUp());
      card.current?.setNextKinematicTranslation({
        x: vector.x - dragged.x,
        y: vector.y - dragged.y,
        z: vector.z - dragged.z,
      });
    }

    if (!fixed.current || !joint1.current || !joint2.current || !joint3.current || !card.current) return;
    [joint1.current, joint2.current].forEach((body) => {
      if (!body.lerped) body.lerped = new THREE.Vector3().copy(body.translation());
      const distance = Math.max(0.1, Math.min(1, body.lerped.distanceTo(body.translation())));
      body.lerped.lerp(body.translation(), delta * distance * 50);
    });

    curve.points[0].copy(joint3.current.translation());
    curve.points[1].copy(joint2.current.lerped!);
    curve.points[2].copy(joint1.current.lerped!);
    curve.points[3].copy(fixed.current.translation());
    band.current.geometry.setPoints(curve.getPoints(mobile ? 18 : 32));

    angular.copy(card.current.angvel());
    rotation.copy(card.current.rotation());
    card.current.setAngvel({ x: angular.x, y: angular.y - rotation.y * 0.25, z: angular.z }, true);
  });

  curve.curveType = "chordal";

  const release = (event: ThreeEvent<PointerEvent>) => {
    (event.target as Element).releasePointerCapture(event.pointerId);
    setDragged(false);
  };

  const grab = (event: ThreeEvent<PointerEvent>) => {
    event.stopPropagation();
    (event.target as Element).setPointerCapture(event.pointerId);
    setDragged(new THREE.Vector3().copy(event.point).sub(vector.copy(card.current.translation())));
  };

  return (
    <>
      <group position={[0, 4.25, 0]}>
        <RigidBody ref={fixed} {...segmentProps} type="fixed" />
        <RigidBody position={[0.55, 0, 0]} ref={joint1} {...segmentProps}>
          <BallCollider args={[0.1]} />
        </RigidBody>
        <RigidBody position={[1.1, 0, 0]} ref={joint2} {...segmentProps}>
          <BallCollider args={[0.1]} />
        </RigidBody>
        <RigidBody position={[1.65, 0, 0]} ref={joint3} {...segmentProps}>
          <BallCollider args={[0.1]} />
        </RigidBody>

        <RigidBody
          position={[2.15, 0, 0]}
          ref={card}
          {...segmentProps}
          type={dragged ? "kinematicPosition" : "dynamic"}
        >
          <CuboidCollider args={[1.18, 1.68, 0.08]} />
          <group
            position={[0, -1.76, 0]}
            onPointerOver={() => setHovered(true)}
            onPointerOut={() => setHovered(false)}
            onPointerDown={grab}
            onPointerUp={release}
          >
            <mesh castShadow>
              <boxGeometry args={[2.36, 3.36, 0.15]} />
              <meshPhysicalMaterial
                map={texture ?? undefined}
                color={texture ? "#ffffff" : "#121819"}
                clearcoat={mobile ? 0.25 : 0.8}
                clearcoatRoughness={0.2}
                roughness={0.48}
                metalness={0.08}
              />
            </mesh>
            <mesh position={[0, 1.82, 0]} castShadow>
              <boxGeometry args={[0.88, 0.36, 0.22]} />
              <meshStandardMaterial color="#aeb8b4" metalness={0.85} roughness={0.24} />
            </mesh>
            <mesh position={[0, 1.98, 0]} rotation={[Math.PI / 2, 0, 0]}>
              <torusGeometry args={[0.27, 0.08, 12, 28]} />
              <meshStandardMaterial color="#6e7775" metalness={0.92} roughness={0.2} />
            </mesh>
          </group>
        </RigidBody>
      </group>

      <mesh ref={band}>
        <meshLineGeometry />
        <meshLineMaterial
          color="#c7ff64"
          depthTest={false}
          resolution={mobile ? [720, 1280] : [1200, 1200]}
          lineWidth={0.82}
        />
      </mesh>
    </>
  );
}

export default function DraggableLanyard({ role }: LanyardProps) {
  const [mobile, setMobile] = useState(false);

  useEffect(() => {
    const update = () => setMobile(window.innerWidth < 768);
    update();
    window.addEventListener("resize", update);
    return () => window.removeEventListener("resize", update);
  }, []);

  return (
    <div className="lanyard-canvas" aria-label="Interactive draggable identity card">
      <Canvas
        camera={{ position: [0, 0, mobile ? 15 : 13], fov: mobile ? 30 : 27 }}
        dpr={[1, mobile ? 1.25 : 1.75]}
        gl={{ alpha: true, antialias: true }}
        onCreated={({ gl }) => gl.setClearColor(new THREE.Color(0x000000), 0)}
      >
        <ambientLight intensity={2.2} />
        <directionalLight position={[4, 8, 8]} intensity={3.4} color="#ffffff" />
        <pointLight position={[-5, -2, 5]} intensity={24} color="#c7ff64" />
        <Suspense fallback={null}>
          <Physics gravity={[0, -38, 0]} timeStep={mobile ? 1 / 30 : 1 / 60}>
            <Band role={role} mobile={mobile} />
          </Physics>
        </Suspense>
      </Canvas>
    </div>
  );
}
