import { Vector2 } from "three";

/**
 * Screen-space half of the PSX look — GDD §11.
 *
 * Runs on a low-resolution render target, so `uResolution` is the *virtual*
 * framebuffer size (e.g. 320x240), not the canvas size. Order matters: dither
 * first, then quantise. Adding the ordered-dither offset before rounding is
 * what turns banding into the PSX's characteristic cross-hatch texture; doing
 * it the other way round just adds noise.
 */
export const PSXShader = {
  name: "PSXShader",

  uniforms: {
    tDiffuse: { value: null as unknown },
    /** Virtual framebuffer size, in pixels. */
    uResolution: { value: new Vector2(320, 240) },
    /** Levels per channel. 32 reproduces the console's 15-bit colour. */
    uColorLevels: { value: 32 },
    uDitherStrength: { value: 1 },
    uScanlineStrength: { value: 0.12 },
    uVignetteStrength: { value: 0.25 },
  },

  vertexShader: /* glsl */ `
    varying vec2 vUv;

    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
    }
  `,

  fragmentShader: /* glsl */ `
    uniform sampler2D tDiffuse;
    uniform vec2 uResolution;
    uniform float uColorLevels;
    uniform float uDitherStrength;
    uniform float uScanlineStrength;
    uniform float uVignetteStrength;

    varying vec2 vUv;

    // Standard 4x4 ordered (Bayer) matrix, normalised to 0..1.
    float bayer4x4( vec2 pixel ) {
      int x = int( mod( pixel.x, 4.0 ) );
      int y = int( mod( pixel.y, 4.0 ) );
      int index = y * 4 + x;

      if ( index == 0 )  return  0.0 / 16.0;
      if ( index == 1 )  return  8.0 / 16.0;
      if ( index == 2 )  return  2.0 / 16.0;
      if ( index == 3 )  return 10.0 / 16.0;
      if ( index == 4 )  return 12.0 / 16.0;
      if ( index == 5 )  return  4.0 / 16.0;
      if ( index == 6 )  return 14.0 / 16.0;
      if ( index == 7 )  return  6.0 / 16.0;
      if ( index == 8 )  return  3.0 / 16.0;
      if ( index == 9 )  return 11.0 / 16.0;
      if ( index == 10 ) return  1.0 / 16.0;
      if ( index == 11 ) return  9.0 / 16.0;
      if ( index == 12 ) return 15.0 / 16.0;
      if ( index == 13 ) return  7.0 / 16.0;
      if ( index == 14 ) return 13.0 / 16.0;
      return 5.0 / 16.0;
    }

    // MeshLambert renders into the composer target in linear space. This pass
    // blits straight to the canvas, so without an sRGB encode the image is
    // displayed as if it were already gamma-corrected — which reads as near-
    // black on a modern display (the classic EffectComposer + color-management
    // trap in three r152+).
    vec3 linearToSRGB( vec3 value ) {
      return mix(
        value * 12.92,
        1.055 * pow( value, vec3( 1.0 / 2.4 ) ) - 0.055,
        step( vec3( 0.0031308 ), value )
      );
    }

    void main() {
      vec4 texel = texture2D( tDiffuse, vUv );
      vec3 color = texel.rgb;

      // Dither in low-res texel space, not screen space, or the pattern
      // stretches into visible blobs once the image is upscaled.
      vec2 virtualPixel = vUv * uResolution;
      float threshold = bayer4x4( virtualPixel ) - 0.5;
      color += threshold * uDitherStrength / uColorLevels;

      color = floor( color * uColorLevels + 0.5 ) / uColorLevels;

      if ( uScanlineStrength > 0.0 ) {
        float scanline = sin( virtualPixel.y * 3.14159265 );
        color *= 1.0 - uScanlineStrength * scanline * scanline;
      }

      if ( uVignetteStrength > 0.0 ) {
        vec2 centered = vUv - 0.5;
        float vignette = 1.0 - dot( centered, centered ) * uVignetteStrength * 2.0;
        color *= clamp( vignette, 0.0, 1.0 );
      }

      color = linearToSRGB( clamp( color, 0.0, 1.0 ) );
      gl_FragColor = vec4( color, texel.a );
    }
  `,
};
