import { applyProceduralPlacementRotation } from './procedural-placement.js';

const TAU = Math.PI * 2;
export const HEART_FIELD_LETTERS = Object.freeze(['T', 'P', 'O', 'L', 'M']);

function seededRandomFactory(initialSeed) {
  let seed = Math.max(1, Math.floor(initialSeed || 1)) % 2147483647;
  return () => {
    seed = seed * 16807 % 2147483647;
    return (seed - 1) / 2147483646;
  };
}

function createHeartGeometry(THREE, config) {
  const shape = new THREE.Shape();
  shape.moveTo(0, -0.92);
  shape.bezierCurveTo(-0.18, -0.68, -1.02, -0.08, -1.02, 0.48);
  shape.bezierCurveTo(-1.02, 1.02, -0.34, 1.18, 0, 0.62);
  shape.bezierCurveTo(0.34, 1.18, 1.02, 1.02, 1.02, 0.48);
  shape.bezierCurveTo(1.02, -0.08, 0.18, -0.68, 0, -0.92);

  const depth = config.depth ?? 0.34;
  const bevel = Math.min(config.bevel ?? 0.09, depth * 0.42);
  const geometry = new THREE.ExtrudeGeometry(shape, {
    depth,
    bevelEnabled: bevel > 0,
    bevelThickness: bevel,
    bevelSize: bevel,
    bevelOffset: 0,
    bevelSegments: Math.max(1, Math.min(5, config.bevelSegments ?? 3)),
    curveSegments: Math.max(6, Math.min(32, config.curveSegments ?? 16)),
    steps: 1,
  });
  geometry.computeBoundingBox();
  const center = new THREE.Vector3();
  geometry.boundingBox.getCenter(center);
  geometry.translate(-center.x, -center.y, -center.z);
  geometry.computeVertexNormals();
  return { geometry, frontZ: depth * 0.5 + bevel };
}

function createSymbolMaterial(THREE, color, opacity) {
  return new THREE.ShaderMaterial({
    uniforms: {
      uColor: { value: new THREE.Color(color) },
      uOpacity: { value: opacity },
    },
    vertexShader: `
      attribute float instanceSymbol;
      varying vec2 vUv;
      varying float vSymbol;

      void main() {
        vUv = uv;
        vSymbol = instanceSymbol;
        gl_Position = projectionMatrix * modelViewMatrix * instanceMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      uniform vec3 uColor;
      uniform float uOpacity;
      varying vec2 vUv;
      varying float vSymbol;

      float sdSegment(vec2 p, vec2 a, vec2 b) {
        vec2 pa = p - a;
        vec2 ba = b - a;
        float h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
        return length(pa - ba * h);
      }

      float symbolDistance(vec2 p, float symbol) {
        // T
        if (symbol < 0.5) {
          float d = sdSegment(p, vec2(-0.40, 0.50), vec2(0.40, 0.50));
          return min(d, sdSegment(p, vec2(0.0, 0.50), vec2(0.0, -0.52)));
        }

        // P
        if (symbol < 1.5) {
          float d = sdSegment(p, vec2(-0.28, -0.52), vec2(-0.28, 0.52));
          d = min(d, sdSegment(p, vec2(-0.28, 0.50), vec2(0.08, 0.50)));
          d = min(d, sdSegment(p, vec2(0.08, 0.50), vec2(0.30, 0.32)));
          d = min(d, sdSegment(p, vec2(0.30, 0.32), vec2(0.30, 0.12)));
          d = min(d, sdSegment(p, vec2(0.30, 0.12), vec2(0.08, -0.04)));
          return min(d, sdSegment(p, vec2(0.08, -0.04), vec2(-0.28, -0.04)));
        }

        // O
        if (symbol < 2.5) {
          return abs(length(p * vec2(1.0, 0.82)) - 0.43);
        }

        // L
        if (symbol < 3.5) {
          float d = sdSegment(p, vec2(-0.28, 0.52), vec2(-0.28, -0.50));
          return min(d, sdSegment(p, vec2(-0.28, -0.50), vec2(0.34, -0.50)));
        }

        // M
        float d = sdSegment(p, vec2(-0.40, -0.52), vec2(-0.40, 0.52));
        d = min(d, sdSegment(p, vec2(-0.40, 0.52), vec2(0.0, 0.04)));
        d = min(d, sdSegment(p, vec2(0.0, 0.04), vec2(0.40, 0.52)));
        return min(d, sdSegment(p, vec2(0.40, 0.52), vec2(0.40, -0.52)));
      }

      void main() {
        vec2 p = (vUv - 0.5) * 2.0;
        float d = symbolDistance(p, vSymbol);
        float alpha = 1.0 - smoothstep(0.075, 0.12, d);
        if (alpha < 0.01) discard;
        gl_FragColor = vec4(uColor, alpha * uOpacity);
      }
    `,
    transparent: true,
    depthWrite: false,
    depthTest: true,
    side: THREE.DoubleSide,
    toneMapped: false,
  });
}

export function createHeartField(THREE, config, scene) {
  const count = Math.max(1, Math.min(3000, Math.round(config.count ?? 600)));
  const { geometry, frontZ } = createHeartGeometry(THREE, config);
  const palette = config.colors || {};
  const materialConfig = config.material || {};
  const material = new THREE.MeshPhysicalMaterial({
    color: '#ffffff',
    vertexColors: true,
    metalness: materialConfig.metalness ?? 0.68,
    roughness: materialConfig.roughness ?? 0.18,
    clearcoat: materialConfig.clearcoat ?? 1,
    clearcoatRoughness: materialConfig.clearcoatRoughness ?? 0.08,
    reflectivity: materialConfig.reflectivity ?? 1,
    envMapIntensity: materialConfig.envMapIntensity ?? 1.8,
    emissive: new THREE.Color(palette.emissive || '#26003d'),
    emissiveIntensity: materialConfig.emissiveIntensity ?? 0.18,
  });

  if (materialConfig.envMap) {
    const texture = new THREE.TextureLoader().load('/' + materialConfig.envMap);
    texture.mapping = THREE.EquirectangularReflectionMapping;
    texture.colorSpace = THREE.SRGBColorSpace;
    material.envMap = texture;
  }

  const heartMesh = new THREE.InstancedMesh(geometry, material, count);
  heartMesh.name = 'Instanced Hearts';
  heartMesh.frustumCulled = false;
  heartMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);

  const symbolGeometry = new THREE.PlaneGeometry(0.94, 0.94);
  symbolGeometry.translate(0, 0.1, frontZ + 0.018);
  const symbols = new Float32Array(count);
  const symbolCount = Math.max(
    1,
    Math.min(HEART_FIELD_LETTERS.length, Math.round(config.symbolCount ?? HEART_FIELD_LETTERS.length)),
  );
  const symbolMaterial = createSymbolMaterial(
    THREE,
    palette.symbol || '#fff6ff',
    config.symbolOpacity ?? 0.92,
  );
  const symbolMesh = new THREE.InstancedMesh(symbolGeometry, symbolMaterial, count);
  symbolMesh.name = 'Instanced TPOLM Letters';
  symbolMesh.frustumCulled = false;
  symbolMesh.instanceMatrix = heartMesh.instanceMatrix;

  const group = new THREE.Group();
  group.name = 'Instanced Heart Field';
  applyProceduralPlacementRotation(config, group);
  group.add(heartMesh, symbolMesh);
  scene.add(group);

  const random = seededRandomFactory(config.seed ?? 2001);
  const seeds = [];
  const colors = [
    new THREE.Color(palette.primary || '#ff2bbd'),
    new THREE.Color(palette.secondary || '#7048ff'),
    new THREE.Color(palette.accent || '#36dcff'),
  ];
  const dummy = new THREE.Object3D();
  for (let i = 0; i < count; i++) {
    const colorT = random();
    const colorIndex = Math.min(2, Math.floor(colorT * 3));
    const color = colors[colorIndex].clone().lerp(colors[(colorIndex + 1) % 3], random() * 0.35);
    heartMesh.setColorAt(i, color);
    symbols[i] = Math.floor(random() * symbolCount);
    seeds.push({
      x: random() * 2 - 1,
      z: random() * 2 - 1,
      phase: random(),
      scale: 0.55 + random() * 0.9,
      driftPhase: random() * TAU,
      rotation: new THREE.Euler((random() - 0.5) * 0.5, (random() - 0.5) * 0.7, (random() - 0.5) * 0.3),
      spin: new THREE.Vector3(random() * 2 - 1, random() * 2 - 1, random() * 2 - 1),
    });
  }
  symbolGeometry.setAttribute('instanceSymbol', new THREE.InstancedBufferAttribute(symbols, 1));
  heartMesh.instanceColor.needsUpdate = true;

  group.userData = {
    ...group.userData,
    config,
    heartMesh,
    symbolMesh,
    seeds,
    dummy,
    currentTime: Number.NaN,
  };
  updateHeartField(THREE, group, 0, {});
  return group;
}

export function updateHeartField(THREE, group, time, audioData = {}) {
  const data = group?.userData;
  if (!data?.heartMesh) return;
  const config = data.config;
  const animation = config.animation || {};
  const width = Math.max(0.1, config.width ?? 17);
  const height = Math.max(0.1, config.height ?? 11);
  const depth = Math.max(0, config.fieldDepth ?? 7);
  const offsetX = config.offsetX ?? 0;
  const offsetY = config.offsetY ?? 0;
  const offsetZ = config.offsetZ ?? 0;
  const speed = animation.speed ?? 0.55;
  const direction = animation.direction ?? 1;
  const drift = animation.drift ?? 0.55;
  const driftSpeed = animation.driftSpeed ?? 0.7;
  const tumble = animation.tumble ?? 0.34;
  const size = config.size ?? 0.48;
  const sizeVariation = config.sizeVariation ?? 0.5;
  const audioPulse = animation.audioPulse ?? 0.25;
  const pulse = 1 + (audioData.bass || 0) * audioPulse;
  const { dummy, seeds, heartMesh } = data;

  for (let i = 0; i < seeds.length; i++) {
    const seed = seeds[i];
    const travel = seed.phase + time * speed * direction / height;
    const wrapped = travel - Math.floor(travel);
    const y = (wrapped - 0.5) * height + offsetY;
    const x = seed.x * width * 0.5 + offsetX
      + Math.sin(time * driftSpeed + seed.driftPhase) * drift;
    const z = seed.z * depth * 0.5 + offsetZ;
    const scaleVariation = 1 + (seed.scale - 1) * sizeVariation;
    const s = Math.max(0.01, size * scaleVariation * pulse);

    dummy.position.set(x, y, z);
    dummy.rotation.set(
      seed.rotation.x + time * seed.spin.x * tumble,
      seed.rotation.y + time * seed.spin.y * tumble,
      seed.rotation.z + time * seed.spin.z * tumble * 0.55,
    );
    dummy.scale.setScalar(s);
    dummy.updateMatrix();
    heartMesh.setMatrixAt(i, dummy.matrix);
  }
  heartMesh.instanceMatrix.needsUpdate = true;
  data.currentTime = time;
}

export function applyHeartFieldMaterial(config, group) {
  applyProceduralPlacementRotation(config, group);
  const material = group?.userData?.heartMesh?.material;
  const symbolMaterial = group?.userData?.symbolMesh?.material;
  if (!material) return;
  const colors = config.colors || {};
  const settings = config.material || {};
  material.metalness = settings.metalness ?? material.metalness;
  material.roughness = settings.roughness ?? material.roughness;
  material.clearcoat = settings.clearcoat ?? material.clearcoat;
  material.clearcoatRoughness = settings.clearcoatRoughness ?? material.clearcoatRoughness;
  material.envMapIntensity = settings.envMapIntensity ?? material.envMapIntensity;
  material.emissiveIntensity = settings.emissiveIntensity ?? material.emissiveIntensity;
  if (colors.emissive) material.emissive.set(colors.emissive);
  if (colors.symbol && symbolMaterial?.uniforms?.uColor) symbolMaterial.uniforms.uColor.value.set(colors.symbol);
  if (symbolMaterial?.uniforms?.uOpacity) symbolMaterial.uniforms.uOpacity.value = config.symbolOpacity ?? 0.92;
  material.needsUpdate = true;
}
