import React, { useEffect, useRef } from 'react';
import { styled } from '@mui/system';

const SpipaWrapper = styled('div')`
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: radial-gradient(#555, #111);
`;

const UI = styled('div')`
  display: none;
  position: fixed;
  z-index: 5;
  bottom: 0;
  left: 0;
  width: 120px;
  padding: 10px;
  background: rgba(255, 255, 255, 0.7);

  p {
    font-size: 11px;
    font-weight: 700;

    &.zoom {
      margin-bottom: 5px;

      span {
        margin-right: 5px;
        border: solid 1px #777;
        cursor: pointer;
        border-radius: 2px;

        &.zoomin {
          padding: 2px 5px;
        }
        &.zoomout {
          padding: 2px 8px;
        }
        &:hover {
          background: black;
          color: white;
        }
      }
    }
  }
`;

const SpipaBackground = ({ children }) => {
  const canvasRef = useRef(null);
  const uiRef = useRef(null);

  useEffect(() => {
    const App = {};
    App.setup = function () {
      const canvas = canvasRef.current;
      this.filename = 'spipa';
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      this.canvas = canvas;
      this.ctx = this.canvas.getContext('2d');
      this.width = this.canvas.width;
      this.height = this.canvas.height;
      this.dataToImageRatio = 1;
      this.ctx.imageSmoothingEnabled = false;
      this.ctx.webkitImageSmoothingEnabled = false;
      this.ctx.msImageSmoothingEnabled = false;
      this.xC = this.width / 2;
      this.yC = this.height / 2;

      this.stepCount = 0;
      this.particles = [];
      this.lifespan = 1000;
      this.popPerBirth = 1;
      this.maxPop = 300;
      this.birthFreq = 2;

      // Build grid
      this.gridSize = 8; // Motion coords
      this.gridSteps = Math.floor(1000 / this.gridSize);
      this.grid = [];
      let i = 0;
      for (let xx = -500; xx < 500; xx += this.gridSize) {
        for (let yy = -500; yy < 500; yy += this.gridSize) {
          // Radial field, triangular function of r with max around r0
          const r = Math.sqrt(xx * xx + yy * yy);
          const r0 = 100;
          let field;

          if (r < r0) field = (255 / r0) * r;
          else if (r > r0) field = 255 - Math.min(255, (r - r0) / 2);

          this.grid.push({
            x: xx,
            y: yy,
            busyAge: 0,
            spotIndex: i,
            isEdge:
              xx === -500
                ? 'left'
                : xx === -500 + this.gridSize * (this.gridSteps - 1)
                ? 'right'
                : yy === -500
                ? 'top'
                : yy === -500 + this.gridSize * (this.gridSteps - 1)
                ? 'bottom'
                : false,
            field: field,
          });
          i++;
        }
      }
      this.gridMaxIndex = i;

      // Counters for UI
      this.drawnInLastFrame = 0;
      this.deathCount = 0;

      this.initDraw();
    };

    App.evolve = function () {
      const time1 = performance.now();

      this.stepCount++;

      // Increment all grid ages
      this.grid.forEach((e) => {
        if (e.busyAge > 0) e.busyAge++;
      });

      if (this.stepCount % this.birthFreq === 0 && this.particles.length + this.popPerBirth < this.maxPop) {
        this.birth();
      }
      App.move();
      App.draw();

      const time2 = performance.now();

      // Update UI
      uiRef.current.querySelector('.dead').textContent = this.deathCount;
      uiRef.current.querySelector('.alive').textContent = this.particles.length;
      uiRef.current.querySelector('.fps').textContent = Math.floor(1000 / (time2 - time1));
      uiRef.current.querySelector('.drawn').textContent = this.drawnInLastFrame;
    };

    App.birth = function () {
      let x, y;
      const gridSpotIndex = Math.floor(Math.random() * this.gridMaxIndex);
      const gridSpot = this.grid[gridSpotIndex];
      x = gridSpot.x;
      y = gridSpot.y;

      const particle = {
        hue: 200,
        sat: 95,
        lum: 20 + Math.floor(40 * Math.random()),
        x: x,
        y: y,
        xLast: x,
        yLast: y,
        xSpeed: 0,
        ySpeed: 0,
        age: 0,
        ageSinceStuck: 0,
        attractor: {
          oldIndex: gridSpotIndex,
          gridSpotIndex: gridSpotIndex,
        },
        name: 'seed-' + Math.ceil(10000000 * Math.random()),
      };
      this.particles.push(particle);
    };

    App.kill = function (particleName) {
      const newArray = this.particles.filter((seed) => seed.name !== particleName);
      this.particles = newArray;
    };

    App.move = function () {
      for (let i = 0; i < this.particles.length; i++) {
        // Get particle
        const p = this.particles[i];

        // Save last position
        p.xLast = p.x;
        p.yLast = p.y;

        // Attractor and corresponding grid spot
        const index = p.attractor.gridSpotIndex;
        let gridSpot = this.grid[index];

        // Maybe move attractor and with certain constraints
        if (Math.random() < 0.5) {
          // Move attractor
          if (!gridSpot.isEdge) {
            // Change particle's attractor grid spot and local move function's grid spot
            const topIndex = index - 1;
            const bottomIndex = index + 1;
            const leftIndex = index - this.gridSteps;
            const rightIndex = index + this.gridSteps;
            const topSpot = this.grid[topIndex];
            const bottomSpot = this.grid[bottomIndex];
            const leftSpot = this.grid[leftIndex];
            const rightSpot = this.grid[rightIndex];

            // Choose neighbour with highest field value (with some disobedience...)
            const chaos = 30;
            const maxFieldSpot = [topSpot, bottomSpot, leftSpot, rightSpot].reduce(
              (max, e) => (e.field + chaos * Math.random() > max.field ? e : max),
              { field: -Infinity }
            );

            const potentialNewGridSpot = maxFieldSpot;
            if (potentialNewGridSpot.busyAge === 0 || potentialNewGridSpot.busyAge > 15) {
              p.ageSinceStuck = 0;
              p.attractor.oldIndex = index;
              p.attractor.gridSpotIndex = potentialNewGridSpot.spotIndex;
              gridSpot = potentialNewGridSpot;
              gridSpot.busyAge = 1;
            } else {
              p.ageSinceStuck++;
            }
          } else {
            p.ageSinceStuck++;
          }

          if (p.ageSinceStuck === 10) this.kill(p.name);
        }

        // Spring attractor to center with viscosity
        const k = 8;
        const visc = 0.4;
        const dx = p.x - gridSpot.x;
        const dy = p.y - gridSpot.y;
        const dist = Math.sqrt(dx * dx + dy * dy);

        // Spring
        const xAcc = -k * dx;
        const yAcc = -k * dy;

        p.xSpeed += xAcc;
        p.ySpeed += yAcc;

        // Calm the f*ck down
        p.xSpeed *= visc;
        p.ySpeed *= visc;

        // Store stuff in particle brain
        p.speed = Math.sqrt(p.xSpeed * p.xSpeed + p.ySpeed * p.ySpeed);
        p.dist = dist;

        // Update position
        p.x += 0.1 * p.xSpeed;
        p.y += 0.1 * p.ySpeed;

        // Get older
        p.age++;

        // Kill if too old
        if (p.age > this.lifespan) {
          this.kill(p.name);
          this.deathCount++;
        }
      }
    };

    App.initDraw = function () {
      this.ctx.beginPath();
      this.ctx.rect(0, 0, this.width, this.height);
      this.ctx.fillStyle = 'black';
      this.ctx.fill();
      this.ctx.closePath();
    };

    App.draw = function () {
      this.drawnInLastFrame = 0;
      if (!this.particles.length) return false;

      this.ctx.beginPath();
      this.ctx.rect(0, 0, this.width, this.height);
      this.ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
      this.ctx.fill();
      this.ctx.closePath();

      for (let i = 0; i < this.particles.length; i++) {
        // Draw particle
        const p = this.particles[i];

        let h, s, l, a;

        h = p.hue + this.stepCount / 30;
        s = p.sat;
        l = p.lum;
        a = 1;

        const last = this.dataXYtoCanvasXY(p.xLast, p.yLast);
        const now = this.dataXYtoCanvasXY(p.x, p.y);
        const attracSpot = this.grid[p.attractor.gridSpotIndex];
        const attracXY = this.dataXYtoCanvasXY(attracSpot.x, attracSpot.y);
        const oldAttracSpot = this.grid[p.attractor.oldIndex];
        const oldAttracXY = this.dataXYtoCanvasXY(oldAttracSpot.x, oldAttracSpot.y);

        this.ctx.beginPath();

        this.ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, ${a})`;
        this.ctx.fillStyle = `hsla(${h}, ${s}%, ${l}%, ${a})`;

        // Particle trail
        this.ctx.moveTo(last.x, last.y);
        this.ctx.lineTo(now.x, now.y);

        this.ctx.lineWidth = 1.5 * this.dataToImageRatio;
        this.ctx.stroke();
        this.ctx.closePath();

        // Attractor positions
        this.ctx.beginPath();
        this.ctx.lineWidth = 1.5 * this.dataToImageRatio;
        this.ctx.moveTo(oldAttracXY.x, oldAttracXY.y);
        this.ctx.lineTo(attracXY.x, attracXY.y);
        this.ctx.arc(attracXY.x, attracXY.y, 1.5 * this.dataToImageRatio, 0, 2 * Math.PI, false);

        this.ctx.strokeStyle = `hsla(${h}, ${s}%, ${l}%, ${a})`;
        this.ctx.fillStyle = `hsla(${h}, ${s}%, ${l}%, ${a})`;
        this.ctx.stroke();
        this.ctx.fill();

        this.ctx.closePath();

        // UI counter
        this.drawnInLastFrame++;
      }
    };

    App.dataXYtoCanvasXY = function (x, y) {
      const zoom = 1.6;
      const xx = this.xC + x * zoom * this.dataToImageRatio;
      const yy = this.yC + y * zoom * this.dataToImageRatio;

      return { x: xx, y: yy };
    };

    App.setup();
    App.draw();

    const frame = function () {
      App.evolve();
      requestAnimationFrame(frame);
    };
    frame();
  }, []);

  return (
    <SpipaWrapper>
      <canvas ref={canvasRef}></canvas>
      <UI ref={uiRef} className="ui">
        <p className="zoom">
          <span className="zoom zoomin">+</span>
          <span className="zoom zoomout">-</span>
        </p>
        <p className="zoomlevel">
          <span className="percent">100</span> % - (<span className="width"></span>px)(<span className="height"></span>px)
        </p>
        <p>
          Dead: <span className="dead">0</span>
        </p>
        <p>
          Alive: <span className="alive">0</span>
        </p>
        <p>
          Drawn: <span className="drawn">0</span>
        </p>
        <p>
          <span className="fps">0</span> FPS
        </p>
        <a className="save" href="" download="capture.png">
          Save
        </a>
      </UI>
      {children}
    </SpipaWrapper>
  );
};

export default SpipaBackground;
