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

function createUniforms(THREE, config) {
  const terrain = config.terrain || {};
  const colors = config.colors || {};
  const material = config.material || {};
  const animation = config.animation || {};
  const tunnel = config.tunnel || {};
  const fog = config.fog || {};
  const envMapPath = material.envMap || 'images/sphere-stars.jpg';
  const envMap = new THREE.TextureLoader().load(envMapPath);
  envMap.mapping = THREE.EquirectangularReflectionMapping;

  return {
    uTime: { value: 0 },
    uBass: { value: 0 },
    uHeight: { value: terrain.height ?? 3.8 },
    uFrequency: { value: terrain.frequency ?? 0.22 },
    uRidge: { value: terrain.ridge ?? 0.45 },
    uWarp: { value: terrain.warp ?? 0.75 },
    uTerrace: { value: terrain.terrace ?? 0 },
    uSpeed: { value: animation.speed ?? 0.12 },
    uAudioPulse: { value: animation.audioPulse ?? 0.3 },
    uTunnelRadius: { value: tunnel.radius ?? 7 },
    uTunnelTwist: { value: tunnel.twist ?? 0 },
    uTunnelRotateSpeed: { value: tunnel.rotateSpeed ?? 0 },
    uLowColor: { value: new THREE.Color(colors.low || '#18052b') },
    uHighColor: { value: new THREE.Color(colors.high || '#ff45bd') },
    uAccentColor: { value: new THREE.Color(colors.accent || '#67e8ff') },
    uEmissiveColor: { value: new THREE.Color(colors.emissive || '#25003d') },
    uEmissiveIntensity: { value: material.emissiveIntensity ?? 0.25 },
    uMetalness: { value: material.metalness ?? 0.72 },
    uRoughness: { value: material.roughness ?? 0.16 },
    uEnvIntensity: { value: material.envMapIntensity ?? 1.8 },
    uContourDensity: { value: material.contourDensity ?? 12 },
    uContourStrength: { value: material.contourStrength ?? 0.3 },
    uNormalSmoothing: { value: material.normalSmoothing ?? 0.18 },
    uLightDir: { value: new THREE.Vector3(
      config.light?.direction?.x ?? -0.45,
      config.light?.direction?.y ?? 0.85,
      config.light?.direction?.z ?? 0.55,
    ).normalize() },
    uLightColor: { value: new THREE.Color(config.light?.color || '#fff4ff') },
    uLightIntensity: { value: config.light?.intensity ?? 1.5 },
    uAmbientColor: { value: new THREE.Color(config.light?.ambientColor || '#251335') },
    uAmbientIntensity: { value: config.light?.ambientIntensity ?? 0.75 },
    uEnvMap: { value: envMap },
    uOffset: { value: new THREE.Vector3(
      config.offsetX ?? 0,
      config.offsetY ?? 0,
      config.offsetZ ?? 0,
    ) },
    uFogColor: { value: new THREE.Color(fog.color || '#08030f') },
    uFogNear: { value: fog.near ?? 12 },
    uFogFar: { value: fog.far ?? 34 },
  };
}

const noiseGlsl = `
  float hash21(vec2 p) {
    p = fract(p * vec2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
  }

  float valueNoise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    f = f * f * (3.0 - 2.0 * f);
    return mix(
      mix(hash21(i), hash21(i + vec2(1.0, 0.0)), f.x),
      mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0)), f.x),
      f.y
    );
  }

  float fbm(vec2 p) {
    float value = 0.0;
    float amplitude = 0.55;
    mat2 rotation = mat2(0.80, -0.60, 0.60, 0.80);
    for (int i = 0; i < 5; i++) {
      value += valueNoise(p) * amplitude;
      p = rotation * p * 2.03 + 17.13;
      amplitude *= 0.5;
    }
    return value;
  }

  float terrainHeight(vec2 p) {
    vec2 q = p * uFrequency;
    float t = uTime * uSpeed;
    vec2 distortion = vec2(
      fbm(q * 0.72 + vec2(t, 11.7)),
      fbm(q * 0.72 + vec2(8.3, -t))
    ) - 0.5;
    q += distortion * uWarp;
    float broad = fbm(q + vec2(t * 0.28, 0.0));
    float detail = fbm(q * 2.15 - vec2(0.0, t * 0.35));
    float ridged = 1.0 - abs(broad * 2.0 - 1.0);
    ridged *= ridged;
    float shape = mix(broad, ridged, uRidge);
    shape = mix(shape, shape * 0.72 + detail * 0.28, 0.42);
    shape += sin(p.x * uFrequency * 2.7 + t) * 0.055;
    float steps = max(2.0, uTerrace);
    float terraced = floor(shape * steps) / steps;
    shape = mix(shape, terraced, step(1.5, uTerrace) * 0.82);
    return (shape - 0.34) * uHeight * (1.0 + uBass * uAudioPulse);
  }
`;

function createSurfaceMaterial(THREE, uniforms, wireframe) {
  return new THREE.ShaderMaterial({
    uniforms,
    extensions: { derivatives: true },
    vertexShader: `
      uniform float uTime;
      uniform float uBass;
      uniform float uHeight;
      uniform float uFrequency;
      uniform float uRidge;
      uniform float uWarp;
      uniform float uTerrace;
      uniform float uSpeed;
      uniform float uAudioPulse;
      uniform float uNormalSmoothing;
      uniform vec3 uOffset;
      varying vec3 vWorldPos;
      varying vec3 vNormal;
      varying float vHeight;
      ${noiseGlsl}

      void main() {
        vec2 terrainPoint = vec2(position.x, position.z);
        float height = terrainHeight(terrainPoint);
        float epsilon = max(0.01, uNormalSmoothing);
        float heightX = terrainHeight(terrainPoint + vec2(epsilon, 0.0));
        float heightZ = terrainHeight(terrainPoint + vec2(0.0, epsilon));
        vec3 terrainNormal = normalize(vec3(height - heightX, epsilon, height - heightZ));
        vec3 displaced = vec3(position.x, height, position.z) + uOffset;
        vec4 world = modelMatrix * vec4(displaced, 1.0);
        vWorldPos = world.xyz;
        vNormal = normalize(mat3(modelMatrix) * terrainNormal);
        vHeight = clamp(height / max(uHeight, 0.001) + 0.35, 0.0, 1.0);
        gl_Position = projectionMatrix * viewMatrix * world;
      }
    `,
    fragmentShader: createFragmentShader(),
    wireframe,
    side: THREE.DoubleSide,
  });
}

function createVoxelMaterial(THREE, uniforms) {
  return new THREE.ShaderMaterial({
    uniforms,
    extensions: { derivatives: true },
    vertexShader: `
      attribute vec2 instanceXZ;
      uniform float uTime;
      uniform float uBass;
      uniform float uHeight;
      uniform float uFrequency;
      uniform float uRidge;
      uniform float uWarp;
      uniform float uTerrace;
      uniform float uSpeed;
      uniform float uAudioPulse;
      uniform vec3 uOffset;
      varying vec3 vWorldPos;
      varying vec3 vNormal;
      varying float vHeight;
      ${noiseGlsl}

      void main() {
        float top = terrainHeight(instanceXZ);
        float base = -uHeight * 0.46;
        float columnHeight = max(0.06, top - base);
        vec3 voxelPosition = position;
        voxelPosition.y = position.y * columnHeight + base + columnHeight * 0.5;
        voxelPosition.x += instanceXZ.x;
        voxelPosition.z += instanceXZ.y;
        voxelPosition += uOffset;
        vec4 world = modelMatrix * instanceMatrix * vec4(voxelPosition, 1.0);
        vWorldPos = world.xyz;
        vNormal = normalize(mat3(modelMatrix) * mat3(instanceMatrix) * normal);
        vHeight = clamp(top / max(uHeight, 0.001) + 0.35, 0.0, 1.0);
        gl_Position = projectionMatrix * viewMatrix * world;
      }
    `,
    fragmentShader: createFragmentShader(),
  });
}

function createTunnelMaterial(THREE, uniforms, wireframe) {
  return new THREE.ShaderMaterial({
    uniforms,
    extensions: { derivatives: true },
    vertexShader: `
      uniform float uTime;
      uniform float uBass;
      uniform float uHeight;
      uniform float uFrequency;
      uniform float uRidge;
      uniform float uWarp;
      uniform float uTerrace;
      uniform float uSpeed;
      uniform float uAudioPulse;
      uniform float uTunnelRadius;
      uniform float uTunnelTwist;
      uniform float uTunnelRotateSpeed;
      uniform vec3 uOffset;
      varying vec3 vWorldPos;
      varying vec3 vNormal;
      varying float vHeight;
      ${noiseGlsl}

      void main() {
        float baseAngle = atan(position.y, position.x);
        float twistAngle = position.z * uTunnelTwist * 0.08 + uTime * uTunnelRotateSpeed;
        float c = cos(twistAngle);
        float s = sin(twistAngle);
        vec2 twistedXY = mat2(c, -s, s, c) * position.xy;
        vec3 radial = normalize(vec3(twistedXY, 0.0));
        float tunnelHeight = terrainHeight(vec2(baseAngle * uTunnelRadius, position.z));
        vec3 displaced = vec3(twistedXY, position.z) - radial * tunnelHeight + uOffset;
        vec4 world = modelMatrix * vec4(displaced, 1.0);
        vWorldPos = world.xyz;
        vNormal = normalize(mat3(modelMatrix) * -radial);
        vHeight = clamp(tunnelHeight / max(uHeight, 0.001) + 0.35, 0.0, 1.0);
        gl_Position = projectionMatrix * viewMatrix * world;
      }
    `,
    fragmentShader: createFragmentShader(),
    wireframe,
    side: THREE.BackSide,
  });
}

function createFragmentShader() {
  return `
    uniform vec3 uLowColor;
    uniform vec3 uHighColor;
    uniform vec3 uAccentColor;
    uniform vec3 uEmissiveColor;
    uniform float uEmissiveIntensity;
    uniform float uMetalness;
    uniform float uRoughness;
    uniform float uEnvIntensity;
    uniform float uContourDensity;
    uniform float uContourStrength;
    uniform vec3 uLightDir;
    uniform vec3 uLightColor;
    uniform float uLightIntensity;
    uniform vec3 uAmbientColor;
    uniform float uAmbientIntensity;
    uniform sampler2D uEnvMap;
    uniform vec3 uFogColor;
    uniform float uFogNear;
    uniform float uFogFar;
    varying vec3 vWorldPos;
    varying vec3 vNormal;
    varying float vHeight;

    const float PI = 3.14159265359;

    vec3 sampleEnvironment(vec3 direction) {
      float phi = atan(direction.z, direction.x);
      float theta = acos(clamp(direction.y, -1.0, 1.0));
      return texture2D(uEnvMap, vec2(phi / (2.0 * PI) + 0.5, theta / PI)).rgb;
    }

    void main() {
      vec3 normal = normalize(vNormal);
      vec3 viewDirection = normalize(cameraPosition - vWorldPos);
      vec3 baseColor = mix(uLowColor, uHighColor, smoothstep(0.08, 0.92, vHeight));
      float diffuseLight = max(dot(normal, normalize(uLightDir)), 0.0);
      vec3 diffuse = baseColor * (uAmbientColor * uAmbientIntensity + uLightColor * diffuseLight * uLightIntensity);
      vec3 reflected = sampleEnvironment(reflect(-viewDirection, normal));
      float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 3.0);
      vec3 halfVector = normalize(viewDirection + normalize(uLightDir));
      float specularPower = mix(18.0, 150.0, 1.0 - uRoughness);
      float normalVariation = max(length(dFdx(normal)), length(dFdy(normal)));
      float filteredSpecularPower = mix(
        specularPower,
        18.0,
        clamp(normalVariation * 4.0, 0.0, 1.0)
      );
      float specular = pow(max(dot(normal, halfVector), 0.0), filteredSpecularPower);
      float contourPhase = abs(fract(vHeight * uContourDensity) - 0.5);
      float contourAA = max(fwidth(vHeight * uContourDensity), 0.002);
      float contour = 1.0 - smoothstep(
        max(0.0, 0.025 - contourAA),
        0.09 + contourAA,
        contourPhase
      );
      vec3 color = diffuse * (1.0 - uMetalness * 0.72);
      color += reflected * uEnvIntensity * mix(0.08, 0.92, uMetalness) * (0.22 + fresnel);
      color += uLightColor * specular * (1.0 - uRoughness);
      color += uAccentColor * contour * uContourStrength;
      color += uAccentColor * fresnel * 0.24;
      color += uEmissiveColor * uEmissiveIntensity;
      float distanceToCamera = length(cameraPosition - vWorldPos);
      float fog = smoothstep(uFogNear, uFogFar, distanceToCamera);
      gl_FragColor = vec4(mix(color, uFogColor, fog), 1.0);
    }
  `;
}

export function createLandscapeField(THREE, config, scene) {
  const mode = ['voxel', 'tunnel'].includes(config.mode) ? config.mode : 'surface';
  const width = Math.max(1, config.width ?? 18);
  const depth = Math.max(1, config.depth ?? 14);
  const resolutionX = Math.max(4, Math.min(480, Math.round(config.resolutionX ?? 150)));
  const resolutionZ = Math.max(4, Math.min(640, Math.round(config.resolutionZ ?? 110)));
  const uniforms = createUniforms(THREE, config);
  const group = new THREE.Group();
  applyProceduralPlacementRotation(config, group);
  group.name = mode === 'voxel'
    ? 'Instanced Voxel Landscape'
    : mode === 'tunnel' ? 'Wrapped Tunnel Landscape' : 'Displaced Landscape';
  let terrainMesh;

  if (mode === 'voxel') {
    const gap = Math.min(0.9, Math.max(0, config.voxelGap ?? 0.12));
    const cellWidth = width / resolutionX;
    const cellDepth = depth / resolutionZ;
    const geometry = new THREE.BoxGeometry(cellWidth * (1 - gap), 1, cellDepth * (1 - gap));
    const instanceXZ = new Float32Array(resolutionX * resolutionZ * 2);
    let cursor = 0;
    for (let z = 0; z < resolutionZ; z++) {
      for (let x = 0; x < resolutionX; x++) {
        instanceXZ[cursor++] = (x / (resolutionX - 1) - 0.5) * width;
        instanceXZ[cursor++] = (z / (resolutionZ - 1) - 0.5) * depth;
      }
    }
    geometry.setAttribute('instanceXZ', new THREE.InstancedBufferAttribute(instanceXZ, 2));
    terrainMesh = new THREE.InstancedMesh(
      geometry,
      createVoxelMaterial(THREE, uniforms),
      resolutionX * resolutionZ,
    );
    const identity = new THREE.Matrix4();
    for (let i = 0; i < resolutionX * resolutionZ; i++) terrainMesh.setMatrixAt(i, identity);
    terrainMesh.instanceMatrix.needsUpdate = true;
    terrainMesh.frustumCulled = false;
  } else if (mode === 'tunnel') {
    const tunnelRadius = Math.max(1, config.tunnel?.radius ?? 7);
    const geometry = new THREE.CylinderGeometry(
      tunnelRadius,
      tunnelRadius,
      depth,
      resolutionX,
      resolutionZ,
      true,
    );
    geometry.rotateX(Math.PI * 0.5);
    terrainMesh = new THREE.Mesh(
      geometry,
      createTunnelMaterial(THREE, uniforms, config.material?.wireframe === true),
    );
    terrainMesh.frustumCulled = false;
  } else {
    const geometry = new THREE.PlaneGeometry(width, depth, resolutionX, resolutionZ);
    geometry.rotateX(-Math.PI * 0.5);
    terrainMesh = new THREE.Mesh(
      geometry,
      createSurfaceMaterial(THREE, uniforms, config.material?.wireframe === true),
    );
    terrainMesh.frustumCulled = false;
  }

  terrainMesh.name = mode === 'voxel'
    ? 'Voxel Terrain'
    : mode === 'tunnel' ? 'Tunnel Terrain' : 'Terrain Surface';
  group.add(terrainMesh);
  group.userData = { ...group.userData, config, terrainMesh };
  scene.add(group);
  return group;
}

export function updateLandscapeField(group, time, audioData = {}) {
  const material = group?.userData?.terrainMesh?.material;
  if (!material?.uniforms) return;
  material.uniforms.uTime.value = time;
  material.uniforms.uBass.value = audioData.bass || 0;
}

export function applyLandscapeFieldConfig(config, group) {
  applyProceduralPlacementRotation(config, group);
  const material = group?.userData?.terrainMesh?.material;
  const uniforms = material?.uniforms;
  if (!uniforms) return;
  const terrain = config.terrain || {};
  const colors = config.colors || {};
  const settings = config.material || {};
  const animation = config.animation || {};
  const tunnel = config.tunnel || {};
  const assignments = {
    uHeight: terrain.height,
    uFrequency: terrain.frequency,
    uRidge: terrain.ridge,
    uWarp: terrain.warp,
    uTerrace: terrain.terrace,
    uSpeed: animation.speed,
    uAudioPulse: animation.audioPulse,
    uTunnelRadius: tunnel.radius,
    uTunnelTwist: tunnel.twist,
    uTunnelRotateSpeed: tunnel.rotateSpeed,
    uEmissiveIntensity: settings.emissiveIntensity,
    uMetalness: settings.metalness,
    uRoughness: settings.roughness,
    uEnvIntensity: settings.envMapIntensity,
    uContourDensity: settings.contourDensity,
    uContourStrength: settings.contourStrength,
    uNormalSmoothing: settings.normalSmoothing,
    uLightIntensity: config.light?.intensity,
    uAmbientIntensity: config.light?.ambientIntensity,
    uFogNear: config.fog?.near,
    uFogFar: config.fog?.far,
  };
  for (const [name, value] of Object.entries(assignments)) {
    if (value !== undefined && uniforms[name]) uniforms[name].value = value;
  }
  const colorAssignments = {
    uLowColor: colors.low,
    uHighColor: colors.high,
    uAccentColor: colors.accent,
    uEmissiveColor: colors.emissive,
    uLightColor: config.light?.color,
    uAmbientColor: config.light?.ambientColor,
    uFogColor: config.fog?.color,
  };
  for (const [name, value] of Object.entries(colorAssignments)) {
    if (value && uniforms[name]) uniforms[name].value.set(value);
  }
  uniforms.uOffset.value.set(config.offsetX ?? 0, config.offsetY ?? 0, config.offsetZ ?? 0);
  const wireframe = settings.wireframe === true;
  if (material.wireframe !== wireframe) {
    material.wireframe = wireframe;
    material.needsUpdate = true;
  }
}
