import React, { useState, useEffect } from "react";

class WaveBalls extends React.Component {
  constructor(props) {
    super(props);
    this.componentDidMount = this.componentDidMount.bind(this);
    this.updateDimensions = this.updateDimensions.bind(this);
    this.updatePositions = this.updatePositions.bind(this);
    this.updateFrame = this.updateFrame.bind(this);
    this.drawFrame = this.drawFrame.bind(this);
    this.mouseMove = this.mouseMove.bind(this);
    this.mouse = { x: 0, y: 0 };
  }

  mouseMove(event) {
    const rect = this.canvas.parentElement.getBoundingClientRect();
    this.mouse.x = (event.clientX - rect.left) / this.width;
    this.mouse.y = (event.clientY - rect.top) / this.height;
  }

  updateDimensions() {
    if (this.updateSizeTimer) {
      clearTimeout(this.updateSizeTimer);
    } else {
      const ctx = this.canvas.getContext("2d");
      ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }

    this.updateSizeTimer = setTimeout(() => {
      this.canvas.width = this.canvas.offsetWidth;
      this.canvas.height = this.canvas.offsetHeight;
      this.width = this.canvas.parentElement.clientWidth;
      this.height = this.canvas.parentElement.clientHeight;
      this.updateSizeTimer = 0;
      this.updateFrame();
    }, 250);
  }

  /* Max framerate of 30FPS, regardless of display frame rate to prevent UX starvation.
   *
   * If a frame render has not been requested, request the frame to be drawn at the next
   * opportunity */
  updateFrame() {
    if (this.updateFrameTimeout || !this.canvas) {
      return;
    }

    const isVisible = window.getComputedStyle(this.canvas).opacity != 0;
    /* If the WaveBalls are not faded completely out, update ball
     * positions and scheduel a render, which will then set a new
     * timeout for updateFrame().
     *
     * Otherwise schedule another a check in 250ms to see if the
     * window is now visible */
    if (isVisible) {
      this.updatePositions();
      window.requestAnimationFrame(this.drawFrame);
    } else {
      this.updateFrameTimeout = setTimeout(() => {
        this.updateFrameTimeout = 0;
        this.updateFrame();
      }, 250);
    }
  }

  drawFrame() {
    if (!this.canvas) {
      return;
    }

    const ctx = this.canvas.getContext("2d");
    ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);

    /* If the window is being resized, don't render balls as
     * they will have a distorted aspect ratio until the canvas
     * width re-snaps to the new dimensions */
    if (!this.updateSizeTimer) {
      this.balls.forEach((ball) => {
        ctx.beginPath();
        ctx.fillStyle = ball.color;
        ctx.arc(ball.pos.x, ball.pos.y, ball.size, 0, 2 * Math.PI);
        ctx.fill();
      });
    }

    this.updateFrameTimeout = setTimeout(() => {
      this.updateFrameTimeout = 0;
      this.updateFrame();
    }, 1000 / 30);
  }

  updatePositions() {
    if (!this.canvas) {
      return;
    }

    const interval = 20000,
      alpha = ((Date.now() - this.start.getTime()) % interval) / interval,
      sun = { r: 0xFC, g: 0xD4, b: 0x40 },
      normal = { r: 197, g: 197, b: 197 },
      gravity = 0.5;

    this.balls.forEach((ball) => {
      ball.alpha.x = ball.start - alpha;
      if (ball.alpha.x < 0.) {
        ball.alpha.x += 1.;
      }

      ball.opacity.current = 0.5 * Math.cos(8. * (alpha - ball.opacity.start) * Math.PI);
      ball.pos.x = ball.alpha.x;
      ball.pos.y =  
        (0.187 * Math.cos(2. * Math.PI * (alpha - ball.alpha.x)) +
        (0.5 + ball.offset + ball.alpha.y * Math.sin(4. * Math.PI * (alpha - ball.alpha.x)) * 0.5));

      /* Gravity influence needs to ramp to 0 near alpha [0, 1] borders */
      const localGravity = gravity * (1. - Math.pow(Math.abs(ball.pos.x - 0.5) / 0.5, 2)),
        alphaDistance = 1. - Math.min(Math.sqrt(Math.pow(ball.pos.x - this.mouse.x, 2) + Math.pow(ball.pos.y - this.mouse.y, 2)), localGravity) / localGravity;
      if (alphaDistance > 0.) {
        ball.pos.x -= alphaDistance * alphaDistance * (ball.pos.x - this.mouse.x);
        ball.pos.y -= alphaDistance * alphaDistance * (ball.pos.y - this.mouse.y);
        ball.opacity.current += alphaDistance * (1 - ball.opacity.current);
        const rgba = [
          normal.r + (sun.r - normal.r) * alphaDistance,
          normal.g + (sun.g - normal.g) * alphaDistance,
          normal.b + (sun.b - normal.b) * alphaDistance,
          ball.opacity.current
        ];
        ball.color = "rgba(" + rgba.join(",") + ")";
      } else {
        ball.color = "rgba(197,197,197," + ball.opacity.current + ")";
      }

      ball.pos.x = ball.pos.x * (this.canvas.width + 64) - 32;
      ball.pos.y = ball.pos.y * (this.canvas.height + 64) - 32;
    });
  }

  componentDidMount() {
    this.balls = [];
    this.start = new Date();

    for (var i = 0; i < 100; i++) {
      let ball = {
        pos: {
          x: 0.,
          y: 0.
        },
        alpha: {
          x: 0.,
          y: (Math.random() * 0.7) - 0.35
        },
        start: Math.random(),
        offset: Math.pow(Math.random() - 0.5, 3),
        brightness: Math.floor(255 * Math.random()),
        opacity:{
          start: Math.random(),
          current: 0
        },
        size: 1 + Math.random() * 6
      };

      this.balls.push(ball);
    }

    this.updateDimensions();
    this.updatePositions();
    window.addEventListener("mousemove", this.mouseMove);
    window.addEventListener("resize", this.updateDimensions);
  }

  componentWillUnmount() {
    if (this.updatePositionsTimer) {
      clearTimeout(this.updatePositionsTimer);
      this.updatePositionsTimer = 0;
    }
    if (this.updateSizeTimer) {
      clearTimeout(this.updateSizeTimer);
      this.updateSizeTimer = 0;
    }
    window.removeEventListener("mousemove", this.mouseMove);
    window.removeEventListener("resize", this.updateDimensions);
  }

  render() {
    return (
      <canvas className="WaveBalls" ref={el => this.canvas = el}></canvas>
    );
  }
}
export default WaveBalls;
