import { BackSide, Color, ShaderMaterial, UniformsLib, UniformsUtils, Vector3 } from "three";

export interface FogQualityMaterialOptions {
  colorHex: string;
  density: number;
  padding: number;
  time: number;
  halfSize: {
    x: number;
    y: number;
    z: number;
  };
  opacityMultiplier?: number;
  colorLift?: number;
}

export interface FogQualityMaterialResult {
  material: ShaderMaterial;
  animationUniform: { value: number };
}

const MIN_FOG_HALF_SIZE = 0.05;

export function createFogQualityMaterial(options: FogQualityMaterialOptions): FogQualityMaterialResult {
  const halfSize = new Vector3(
    Math.max(MIN_FOG_HALF_SIZE, options.halfSize.x),
    Math.max(MIN_FOG_HALF_SIZE, options.halfSize.y),
    Math.max(MIN_FOG_HALF_SIZE, options.halfSize.z)
  );
  const minHalfExtent = Math.min(halfSize.x, halfSize.y, halfSize.z);
  const padding = Math.max(0, Math.min(options.padding, minHalfExtent * 0.82));
  const animationUniform = { value: options.time };
  const uniforms = UniformsUtils.clone(UniformsLib.fog) as Record<string, { value: unknown }>;

  uniforms["time"] = animationUniform;
  uniforms["volumeFogColor"] = { value: new Color(options.colorHex) };
  uniforms["volumeFogDensity"] = { value: Math.max(0, options.density) };
  uniforms["volumeHalfSize"] = { value: halfSize };
  uniforms["volumePadding"] = { value: padding };
  uniforms["opacityMultiplier"] = { value: Math.max(0.6, Math.min(1.5, options.opacityMultiplier ?? 1)) };
  uniforms["colorLift"] = { value: Math.max(0, Math.min(0.22, options.colorLift ?? 0)) };
  uniforms["localCameraPosition"] = { value: new Vector3() };

  const vertexShader = /* glsl */ `
    varying vec3 vLocalPosition;
    #include <fog_pars_vertex>

    void main() {
      vLocalPosition = position;
      vec4 worldPosition = modelMatrix * vec4(position, 1.0);
      vec4 mvPosition = viewMatrix * worldPosition;
      gl_Position = projectionMatrix * mvPosition;
      #include <fog_vertex>
    }
  `;

  const fragmentShader = /* glsl */ `
    uniform vec3 volumeFogColor;
    uniform float volumeFogDensity;
    uniform vec3 volumeHalfSize;
    uniform float volumePadding;
    uniform float opacityMultiplier;
    uniform float colorLift;
    uniform float time;
    uniform vec3 localCameraPosition;

    varying vec3 vLocalPosition;
    #include <fog_pars_fragment>

    #define FOG_STEPS 10

    float hash13(vec3 point) {
      point = fract(point * 0.1031);
      point += dot(point, point.yzx + 33.33);
      return fract((point.x + point.y) * point.z);
    }

    float noise3(vec3 point) {
      vec3 cell = floor(point);
      vec3 local = fract(point);
      vec3 smoothLocal = local * local * (3.0 - 2.0 * local);

      float n000 = hash13(cell + vec3(0.0, 0.0, 0.0));
      float n100 = hash13(cell + vec3(1.0, 0.0, 0.0));
      float n010 = hash13(cell + vec3(0.0, 1.0, 0.0));
      float n110 = hash13(cell + vec3(1.0, 1.0, 0.0));
      float n001 = hash13(cell + vec3(0.0, 0.0, 1.0));
      float n101 = hash13(cell + vec3(1.0, 0.0, 1.0));
      float n011 = hash13(cell + vec3(0.0, 1.0, 1.0));
      float n111 = hash13(cell + vec3(1.0, 1.0, 1.0));

      float nx00 = mix(n000, n100, smoothLocal.x);
      float nx10 = mix(n010, n110, smoothLocal.x);
      float nx01 = mix(n001, n101, smoothLocal.x);
      float nx11 = mix(n011, n111, smoothLocal.x);
      float nxy0 = mix(nx00, nx10, smoothLocal.y);
      float nxy1 = mix(nx01, nx11, smoothLocal.y);
      return mix(nxy0, nxy1, smoothLocal.z);
    }

    float fbm(vec3 point) {
      float value = 0.0;
      float amplitude = 0.5;

      for (int octave = 0; octave < 3; octave += 1) {
        value += amplitude * noise3(point);
        point = point * 2.04 + vec3(17.1, 31.7, 9.2);
        amplitude *= 0.5;
      }

      return value;
    }

    vec2 intersectBox(vec3 rayOrigin, vec3 rayDirection, vec3 halfSize) {
      vec3 safeDirection = sign(rayDirection) * max(abs(rayDirection), vec3(1e-4));
      vec3 invDirection = 1.0 / safeDirection;
      vec3 t0 = (-halfSize - rayOrigin) * invDirection;
      vec3 t1 = (halfSize - rayOrigin) * invDirection;
      vec3 tMin = min(t0, t1);
      vec3 tMax = max(t0, t1);
      float nearHit = max(max(tMin.x, tMin.y), tMin.z);
      float farHit = min(min(tMax.x, tMax.y), tMax.z);
      return vec2(nearHit, farHit);
    }

    float sampleShape(vec3 samplePosition) {
      float minHalfExtent = min(min(volumeHalfSize.x, volumeHalfSize.y), volumeHalfSize.z);
      float edgeSoftness = max(0.08, min(volumePadding + minHalfExtent * 0.16, minHalfExtent * 0.72));
      vec3 innerHalfSize = max(volumeHalfSize - vec3(edgeSoftness), vec3(minHalfExtent * 0.18));
      vec3 distanceToCore = abs(samplePosition) - innerHalfSize;
      float outsideDistance = length(max(distanceToCore, 0.0));
      float insideDistance = min(max(distanceToCore.x, max(distanceToCore.y, distanceToCore.z)), 0.0);
      float roundedBoxDistance = outsideDistance + insideDistance;
      float edgeMask = 1.0 - smoothstep(-edgeSoftness * 0.7, edgeSoftness * 1.35, roundedBoxDistance);

      vec3 ellipsoidPosition = samplePosition / max(volumeHalfSize - vec3(edgeSoftness * 0.18), vec3(1e-3));
      float roundedMask = 1.0 - smoothstep(0.54, 1.03, length(ellipsoidPosition * vec3(0.96, 1.08, 0.96)));

      return edgeMask * mix(0.42, 1.0, roundedMask);
    }

    float sampleVolumeDensity(vec3 samplePosition) {
      vec3 normalizedPosition = samplePosition / max(volumeHalfSize, vec3(1e-3));
      float shape = sampleShape(samplePosition);

      if (shape <= 1e-3) {
        return 0.0;
      }

      vec3 drift = vec3(time * 0.1, time * 0.04, -time * 0.065);
      float primary = fbm(samplePosition * 0.58 + drift);
      float secondary = fbm(samplePosition * 1.18 - drift * 1.45 + vec3(4.3, 9.7, 2.1));
      float wisps = noise3(samplePosition * 2.15 + vec3(0.0, time * 0.08, 0.0));
      float cloud = smoothstep(0.34, 0.92, primary * 0.68 + secondary * 0.24 + wisps * 0.08);
      float centerBias = 1.0 - smoothstep(0.18, 1.08, length(normalizedPosition * vec3(1.05, 0.92, 1.05)));
      float verticalBias = mix(0.9, 1.08, smoothstep(-0.75, 0.35, normalizedPosition.y));
      float carvedCloud = mix(0.42, 1.04, cloud) * mix(0.72, 1.0, centerBias);

      return volumeFogDensity * shape * carvedCloud * verticalBias;
    }

    void main() {
      vec3 rayDirection = normalize(vLocalPosition - localCameraPosition);
      vec2 hitRange = intersectBox(localCameraPosition, rayDirection, volumeHalfSize);
      float startDistance = max(hitRange.x, 0.0);
      float endDistance = hitRange.y;

      if (endDistance <= startDistance) {
        discard;
      }

      float rayLength = endDistance - startDistance;
      float stepLength = rayLength / float(FOG_STEPS);
      float jitter = hash13(vLocalPosition * 1.73 + vec3(time * 0.17)) - 0.5;
      float transmittance = 1.0;
      vec3 accumulatedColor = vec3(0.0);

      for (int stepIndex = 0; stepIndex < FOG_STEPS; stepIndex += 1) {
        float sampleDistance = startDistance + (float(stepIndex) + 0.5 + jitter * 0.35) * stepLength;
        vec3 samplePosition = localCameraPosition + rayDirection * sampleDistance;
        float sampleDensity = sampleVolumeDensity(samplePosition);

        if (sampleDensity <= 1e-4) {
          continue;
        }

        vec3 normalizedPosition = samplePosition / max(volumeHalfSize, vec3(1e-3));
        float topLight = smoothstep(-0.2, 0.95, normalizedPosition.y);
        float forwardScatter = 1.0 - abs(dot(rayDirection, normalize(samplePosition + vec3(1e-3, 2e-3, -1e-3))));
        float coolShadow = smoothstep(0.18, 0.88, noise3(samplePosition * 0.88 - vec3(time * 0.08, 0.0, time * 0.05)));
        vec3 sampleColor = mix(volumeFogColor * 0.76, vec3(1.0), 0.06 + topLight * 0.12 + forwardScatter * 0.12);
        sampleColor = mix(sampleColor * 0.92, sampleColor, coolShadow);

        float extinction = sampleDensity * stepLength * 1.5;
        float sampleAlpha = 1.0 - exp(-extinction);
        accumulatedColor += transmittance * sampleColor * sampleAlpha;
        transmittance *= 1.0 - sampleAlpha;

        if (transmittance < 0.03) {
          break;
        }
      }

      float baseAlpha = 1.0 - transmittance;
      float alpha = clamp(baseAlpha * opacityMultiplier, 0.0, 0.96);

      if (alpha <= 0.01) {
        discard;
      }

      vec3 color = accumulatedColor / max(baseAlpha, 1e-4);
      color = mix(color, vec3(1.0), colorLift);

      gl_FragColor = vec4(color, alpha);
      #include <fog_fragment>
    }
  `;

  return {
    material: new ShaderMaterial({
      vertexShader,
      fragmentShader,
      uniforms,
      transparent: true,
      depthWrite: false,
      fog: true,
      side: BackSide
    }),
    animationUniform
  };
}