import React, {
  useRef,
  useEffect,
  useState,
  RefObject,
  MutableRefObject,
  FC,
} from "react";

/* -------------------------------------------------------------
   1. Type Definitions
------------------------------------------------------------- */
interface ColorStop {
  offset: number; // must be in [0..1]
  r: number;
  g: number;
  b: number;
}

export type PixelatedPerlinCanvasProps = {
  // # of grid nodes in each dimension
  nodes?: number;
  // fractal octaves for added detail
  octaves?: number;
  // amplitude decay factor
  persistence?: number;
  // how fast the swirl "flows" each frame
  angleStep?: number;
  // color gradient array
  colorStops?: ColorStop[];
};

/* -------------------------------------------------------------
   2. PerlinNoise Class (Multi-Octave)
------------------------------------------------------------- */
interface GradientVector {
  x: number;
  y: number;
  angle: number;
}

class PerlinNoise {
  nodes: number;
  grid: GradientVector[][];

  constructor(nodes = 8) {
    this.nodes = nodes;
    // Create a 2D array for gradient vectors
    this.grid = Array.from(
      { length: nodes },
      () => new Array<GradientVector>(nodes)
    );

    for (let i = 0; i < nodes; i++) {
      for (let j = 0; j < nodes; j++) {
        this.grid[i][j] = this.randomUnitVector();
      }
    }
  }

  private randomUnitVector(): GradientVector {
    const angle = Math.random() * 2 * Math.PI;
    return { x: Math.cos(angle), y: Math.sin(angle), angle };
  }

  private unitVector(angle: number): GradientVector {
    return { x: Math.cos(angle), y: Math.sin(angle), angle };
  }

  private smoothify(t: number): number {
    // 6t^5 - 15t^4 + 10t^3
    return t * t * t * (t * (6 * t - 15) + 10);
  }

  private interpolation(t: number, a: number, b: number): number {
    return a + this.smoothify(t) * (b - a);
  }

  private dotProduct(x: number, y: number, i: number, j: number): number {
    const g = this.grid[i][j];
    const dx = x - i;
    const dy = y - j;
    return dx * g.x + dy * g.y;
  }

  // Single-octave Perlin: returns [-1..1]
  noise(x: number, y: number, width: number, height: number): number {
    const scaleX = (this.nodes - 1) / width;
    const scaleY = (this.nodes - 1) / height;
    const sx = x * scaleX;
    const sy = y * scaleY;

    const xCell = Math.floor(sx);
    const yCell = Math.floor(sy);

    const tx = sx - xCell;
    const ty = sy - yCell;

    const topLeft = this.dotProduct(sx, sy, xCell, yCell);
    const topRight = this.dotProduct(sx, sy, xCell + 1, yCell);
    const bottomLeft = this.dotProduct(sx, sy, xCell, yCell + 1);
    const bottomRight = this.dotProduct(sx, sy, xCell + 1, yCell + 1);

    const top = this.interpolation(tx, topLeft, topRight);
    const bottom = this.interpolation(tx, bottomLeft, bottomRight);

    return this.interpolation(ty, top, bottom);
  }

  // Fractal noise: sum of multiple octaves
  fractalNoise(
    x: number,
    y: number,
    width: number,
    height: number,
    octaves = 4,
    persistence = 0.5
  ): number {
    let total = 0;
    let amplitude = 1;
    let frequency = 1;
    let maxValue = 0;

    for (let i = 0; i < octaves; i++) {
      const val = this.noise(
        x * frequency,
        y * frequency,
        width * frequency,
        height * frequency
      );
      total += val * amplitude;
      maxValue += amplitude;
      amplitude *= persistence;
      frequency *= 2;
    }
    // returns ~[-1..1]
    return total / maxValue;
  }

  // Rotate each gradient vector slightly to animate
  movePoints(angleDelta: number): void {
    for (let i = 0; i < this.nodes; i++) {
      for (let j = 0; j < this.nodes; j++) {
        const oldAngle = this.grid[i][j].angle;
        this.grid[i][j] = this.unitVector(oldAngle + angleDelta);
      }
    }
  }
}

/* -------------------------------------------------------------
   3. Color Helpers
------------------------------------------------------------- */
function lerpColor(c1: ColorStop, c2: ColorStop, t: number): ColorStop {
  return {
    offset: 0, // not used after blending
    r: c1.r + (c2.r - c1.r) * t,
    g: c1.g + (c2.g - c1.g) * t,
    b: c1.b + (c2.b - c1.b) * t,
  };
}

// Map [0..1] to an array of color stops
function getGradientColor(value: number, stops: ColorStop[]): ColorStop {
  if (value <= stops[0].offset) {
    return stops[0];
  }
  const lastStop = stops[stops.length - 1];
  if (value >= lastStop.offset) {
    return lastStop;
  }
  for (let i = 0; i < stops.length - 1; i++) {
    const cur = stops[i];
    const nxt = stops[i + 1];
    if (value >= cur.offset && value <= nxt.offset) {
      const range = nxt.offset - cur.offset;
      const t = (value - cur.offset) / range;
      return lerpColor(cur, nxt, t);
    }
  }
  return lastStop;
}

/* -------------------------------------------------------------
   4. PixelatedPerlinCanvas Component (No Grain Overlay)
------------------------------------------------------------- */
const PixelatedPerlinCanvas: FC<PixelatedPerlinCanvasProps> = ({
  nodes = 5,
  octaves = 4,
  persistence = 0.5,
  angleStep = 0.02,
  colorStops = [
    { offset: 0.0, r: 255, g: 200, b: 180 }, // pastel peach
    { offset: 0.3, r: 128, g: 255, b: 180 }, // mint green
    { offset: 0.6, r: 128, g: 255, b: 230 }, // teal
    { offset: 1.0, r: 255, g: 255, b: 200 }, // near-white pastel
  ],
}) => {
  const canvasRef: RefObject<HTMLCanvasElement> = useRef(null);
  const wrapperRef: RefObject<HTMLDivElement> = useRef(null);
  const animationIdRef: MutableRefObject<number | null> = useRef(null);

  const [width, setWidth] = useState(800);
  const [height, setHeight] = useState(600);

  // Create the Perlin Noise generator
  const perlinRef = useRef<PerlinNoise | null>(null);
  if (!perlinRef.current) {
    perlinRef.current = new PerlinNoise(nodes);
  }
  const noiseGenerator = perlinRef.current;

  // Measure parent container => set canvas size
  useEffect(() => {
    function handleResize() {
      if (wrapperRef.current) {
        const rect = wrapperRef.current.getBoundingClientRect();
        setWidth(Math.floor(rect.width));
        setHeight(Math.floor(rect.height));
      }
    }
    handleResize();
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  // The main render loop
  const renderFrame = () => {
    const canvas = canvasRef.current;
    if (!canvas) {
      animationIdRef.current = requestAnimationFrame(renderFrame);
      return;
    }
    const ctx = canvas.getContext("2d");
    if (!ctx) {
      animationIdRef.current = requestAnimationFrame(renderFrame);
      return;
    }

    if (width === 0 || height === 0) {
      animationIdRef.current = requestAnimationFrame(renderFrame);
      return;
    }

    // Set canvas dimensions
    canvas.width = width;
    canvas.height = height;

    const imageData = ctx.getImageData(0, 0, width, height);
    const data = imageData.data;

    // Fill each pixel
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        const i = (y * width + x) * 4;
        // fractal noise in [-1..1]
        const n = noiseGenerator.fractalNoise(
          x,
          y,
          width,
          height,
          octaves,
          persistence
        );
        // map to [0..1]
        const val = (n + 1) / 2;

        // get color from gradient
        const c = getGradientColor(val, colorStops);

        data[i + 0] = c.r;
        data[i + 1] = c.g;
        data[i + 2] = c.b;
        data[i + 3] = 255;
      }
    }

    // put image data on canvas
    ctx.putImageData(imageData, 0, 0);

    // swirl: rotate perlin vectors slightly
    noiseGenerator.movePoints(angleStep);

    // schedule next frame
    animationIdRef.current = requestAnimationFrame(renderFrame);
  };

  // start animation on mount, stop on unmount
  useEffect(() => {
    renderFrame();
    return () => {
      if (animationIdRef.current) {
        cancelAnimationFrame(animationIdRef.current);
      }
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div
      ref={wrapperRef}
      style={{
        width: "100%",
        height: "100%",
        overflow: "hidden",
        position: "relative",
      }}
    >
      <canvas ref={canvasRef} style={{ display: "block" }} />
    </div>
  );
};

export default PixelatedPerlinCanvas;
