/*
 * https://github.com/morethanwords/tweb
 * Copyright (C) 2019-2021 Eduard Kuzmenko, Artem Kolnogorov and unknown creator of the script taken from http://useless.altervista.org/gradient.html
 * https://github.com/morethanwords/tweb/blob/master/LICENSE
 */

import {animateSingle} from '../../helpers/animation';
import {hexToRgb} from '../../helpers/color';
import {easeOutQuadApply} from '../../helpers/easing/easeOutQuad';

const WIDTH = 50;
const HEIGHT = WIDTH;

export default class ChatBackgroundGradientRenderer {
  private readonly _width = WIDTH;
  private readonly _height = HEIGHT;
  private _phase: number;
  private _tail: number;
  private readonly _tails = 90;
  private readonly _scrollTails = 50;
  private _frames: ImageData[];
  private _colors: {r: number, g: number, b: number}[];
  private readonly _curve = [
    0, 0.25, 0.50, 0.75, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 9, 10, 11, 12,
    13, 14, 15, 16, 17, 18, 18.3, 18.6, 18.9, 19.2, 19.5, 19.8, 20.1, 20.4, 20.7,
    21.0, 21.3, 21.6, 21.9, 22.2, 22.5, 22.8, 23.1, 23.4, 23.7, 24.0, 24.3, 24.6,
    24.9, 25.2, 25.5, 25.8, 26.1, 26.3, 26.4, 26.5, 26.6, 26.7, 26.8, 26.9, 27
  ];
  private readonly _incrementalCurve: number[];
  private readonly _positions = [
    {x: 0.80, y: 0.10},
    {x: 0.60, y: 0.20},
    {x: 0.35, y: 0.25},
    {x: 0.25, y: 0.60},
    {x: 0.20, y: 0.90},
    {x: 0.40, y: 0.80},
    {x: 0.65, y: 0.75},
    {x: 0.75, y: 0.40}
  ];
  private readonly _phases = this._positions.length;
  private _onWheelRAF: number;
  private _scrollDelta: number;

  // private _ts = 0;
  // private _fps = 15;
  // private _frametime = 1000 / this._fps;
  // private _raf: number;

  private _canvas: HTMLCanvasElement;
  private _ctx: CanvasRenderingContext2D;
  private _hc: HTMLCanvasElement;
  private _hctx: CanvasRenderingContext2D;

  private _addedScrollListener: boolean;
  private _animatingToNextPosition: boolean;
  private _nextPositionTail: number;
  private _nextPositionTails: number;
  private _nextPositionLeft: number;

  constructor() {
    const diff = this._tails / this._curve[this._curve.length - 1];

    for(let i = 0, length = this._curve.length; i < length; ++i) {
      this._curve[i] = this._curve[i] * diff;
    }

    this._incrementalCurve = this._curve.map((v, i, arr) => {
      return v - (arr[i - 1] ?? 0);
    });
  }

  private hexToRgb(hex: string) {
    const result = hexToRgb(hex);
    return {r: result[0], g: result[1], b: result[2]};
  }

  private getPositions(shift: number) {
    const positions = this._positions.slice();
    positions.push(...positions.splice(0, shift));

    const result: typeof positions = [];
    for(let i = 0; i < positions.length; i += 2) {
      result.push(positions[i]);
    }
    return result;
  }

  private getNextPositions(phase: number, curveMax: number, curve: number[]) {
    const pos = this.getPositions(phase);
    if(!curve[0] && curve.length === 1) {
      return [pos];
    }

    const nextPos = this.getPositions(++phase % this._phases);
    const distances = nextPos.map((nextPos, idx) => {
      return {
        x: (nextPos.x - pos[idx].x) / curveMax,
        y: (nextPos.y - pos[idx].y) / curveMax
      };
    });

    const positions = curve.map((value) => {
      return distances.map((distance, idx) => {
        return {
          x: pos[idx].x + distance.x * value,
          y: pos[idx].y + distance.y * value
        };
      });
    });

    return positions;
  }

  private curPosition(phase: number, tail: number) {
    const positions = this.getNextPositions(phase, this._tails, [tail]);
    return positions[0];
  }

  private changeTail(diff: number) {
    this._tail += diff;

    while(this._tail >= this._tails) {
      this._tail -= this._tails;
      if(++this._phase >= this._phases) {
        this._phase -= this._phases;
      }
    }

    while(this._tail < 0) {
      this._tail += this._tails;
      if(--this._phase < 0) {
        this._phase += this._phases;
      }
    }
  }

  private onWheel = (e: {deltaY: number}) => {
    if(this._animatingToNextPosition) {
      return;
    }

    this._scrollDelta += e.deltaY;
    if(this._onWheelRAF === undefined) {
      this._onWheelRAF = requestAnimationFrame(this.drawOnWheel);
    }
  };

  private changeTailAndDraw(diff: number) {
    this.changeTail(diff);
    const curPos = this.curPosition(this._phase, this._tail);
    this.drawGradient(curPos);
  }

  private drawOnWheel = () => {
    const value = this._scrollDelta / this._scrollTails;
    this._scrollDelta %= this._scrollTails;
    const diff = value > 0 ? Math.floor(value) : Math.ceil(value);
    if(diff) {
      this.changeTailAndDraw(diff);
    }
    this._onWheelRAF = undefined;
  };

  private drawNextPositionAnimated = (getProgress?: () => number) => {
    let done: boolean, id: ImageData;
    if(getProgress) {
      const value = getProgress();
      done = value >= 1;
      const transitionValue = easeOutQuadApply(value, 1);
      const nextPositionTail = this._nextPositionTail ?? 0;
      const tail = this._nextPositionTail = this._nextPositionTails * transitionValue;
      const diff = tail - nextPositionTail;
      if(diff) {
        this._nextPositionLeft -= diff;
        this.changeTailAndDraw(diff);
      }
    } else {
      const frames = this._frames;
      id = frames.shift();
      done = !frames.length;
    }

    if(id) {
      this.drawImageData(id);
    }

    if(done) {
      this._nextPositionLeft = undefined;
      this._nextPositionTails = undefined;
      this._nextPositionTail = undefined;
      this._animatingToNextPosition = undefined;
    }

    return !done;
  };

  private getGradientImageData(positions: {x: number, y: number}[]) {
    const id = this._hctx.createImageData(this._width, this._height);
    const pixels = id.data;

    let offset = 0;
    for(let y = 0; y < this._height; ++y) {
      const directPixelY = y / this._height;
      const centerDistanceY = directPixelY - 0.5;
      const centerDistanceY2 = centerDistanceY * centerDistanceY;

      for(let x = 0; x < this._width; ++x) {
        const directPixelX = x / this._width;

        const centerDistanceX = directPixelX - 0.5;
        const centerDistance = Math.sqrt(centerDistanceX * centerDistanceX + centerDistanceY2);

        const swirlFactor = 0.35 * centerDistance;
        const theta = swirlFactor * swirlFactor * 0.8 * 8.0;
        const sinTheta = Math.sin(theta);
        const cosTheta = Math.cos(theta);

        const pixelX = Math.max(0.0, Math.min(1.0, 0.5 + centerDistanceX * cosTheta - centerDistanceY * sinTheta));
        const pixelY = Math.max(0.0, Math.min(1.0, 0.5 + centerDistanceX * sinTheta + centerDistanceY * cosTheta));

        let distanceSum = 0.0;

        let r = 0.0;
        let g = 0.0;
        let b = 0.0;

        for(let i = 0; i < this._colors.length; i++) {
          const colorX = positions[i].x;
          const colorY = positions[i].y;

          const distanceX = pixelX - colorX;
          const distanceY = pixelY - colorY;

          let distance = Math.max(0.0, 0.9 - Math.sqrt(distanceX * distanceX + distanceY * distanceY));
          distance = distance * distance * distance * distance;
          distanceSum += distance;

          r += distance * this._colors[i].r / 255;
          g += distance * this._colors[i].g / 255;
          b += distance * this._colors[i].b / 255;
        }

        pixels[offset++] = r / distanceSum * 255.0;
        pixels[offset++] = g / distanceSum * 255.0;
        pixels[offset++] = b / distanceSum * 255.0;
        pixels[offset++] = 0xFF;
      }
    }
    return id;
  }

  private drawImageData(id: ImageData) {
    this._hctx.putImageData(id, 0, 0);
    this._ctx.drawImage(this._hc, 0, 0, this._width, this._height);
  }

  private drawGradient(positions: {x: number, y: number}[]) {
    this.drawImageData(this.getGradientImageData(positions));
  }

  // private doAnimate = () => {
  //   const now = +Date.now();
  //   if(!document.hasFocus() || (now - this._ts) < this._frametime) {
  //     this._raf = requestAnimationFrame(this.doAnimate);
  //     return;
  //   }

  //   this._ts = now;
  //   this.changeTail(1);
  //   const cur_pos = this.curPosition(this._phase, this._tail);
  //   this.drawGradient(cur_pos);
  //   this._raf = requestAnimationFrame(this.doAnimate);
  // };

  // public animate(start?: boolean) {
  //   if(!start) {
  //     cancelAnimationFrame(this._raf);
  //     return;
  //   }
  //   this.doAnimate();
  // }

  public init(el: HTMLCanvasElement) {
    this._frames = [];
    this._phase = 0;
    this._tail = 0;
    this._scrollDelta = 0;
    if(this._onWheelRAF !== undefined) {
      cancelAnimationFrame(this._onWheelRAF);
      this._onWheelRAF = undefined;
    }

    const colors = el.getAttribute('data-colors').split(',').reverse();
    this._colors = colors.map((color) => {
      return this.hexToRgb(color);
    });

    if(!this._hc) {
      this._hc = document.createElement('canvas');
      this._hc.width = this._width;
      this._hc.height = this._height;
      this._hctx = this._hc.getContext('2d', {alpha: false});
    }

    this._canvas = el;
    this._ctx = this._canvas.getContext('2d', {alpha: false});
    this.update();
  }

  private update() {
    if(this._colors.length < 2) {
      const color = this._colors[0];
      this._ctx.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b})`;
      this._ctx.fillRect(0, 0, this._width, this._height);
      return;
    }

    const pos = this.curPosition(this._phase, this._tail);
    this.drawGradient(pos);
  }

  public toNextPosition(getProgress?: () => number) {
    if(this._colors.length < 2) {
      return;
    }

    if(getProgress) {
      this._nextPositionLeft = this._tails + (this._nextPositionLeft ?? 0);
      this._nextPositionTails = this._nextPositionLeft;
      this._nextPositionTail = undefined;
      this._animatingToNextPosition = true;
      animateSingle(this.drawNextPositionAnimated.bind(this, getProgress), this);
      return;
    }

    const tail = this._tail;
    const tails = this._tails;

    let nextPhaseOnIdx: number;

    const curve: number[] = [];
    for(let i = 0, length = this._incrementalCurve.length; i < length; ++i) {
      const inc = this._incrementalCurve[i];
      let value = (curve[i - 1] ?? tail) + inc;

      if(+value.toFixed(2) > tails && nextPhaseOnIdx === undefined) {
        nextPhaseOnIdx = i;
        value %= tails;
      }

      curve.push(value);
    }

    const currentPhaseCurve = curve.slice(0, nextPhaseOnIdx);
    const nextPhaseCurve = nextPhaseOnIdx !== undefined ? curve.slice(nextPhaseOnIdx) : [];

    [currentPhaseCurve, nextPhaseCurve].forEach((curve, idx, curves) => {
      const last = curve[curve.length - 1];
      if(last !== undefined && last > tails) {
        curve[curve.length - 1] = +last.toFixed(2);
      }

      this._tail = last ?? 0;

      if(!curve.length) {
        return;
      }

      const positions = this.getNextPositions(this._phase, tails, curve);
      if(idx !== (curves.length - 1)) {
        if(++this._phase >= this._phases) {
          this._phase -= this._phases;
        }
      }

      const ids = positions.map((pos) => {
        return this.getGradientImageData(pos);
      });

      this._frames.push(...ids);
    });

    this._animatingToNextPosition = true;
    animateSingle(this.drawNextPositionAnimated, this);
  }

  // public toNextPositionThrottled = throttle(this.toNextPosition.bind(this), 100, true);

  public scrollAnimate(start?: boolean) {
    return;

    if(this._colors.length < 2 && start) {
      return;
    }

    if(start && !this._addedScrollListener) {
      document.addEventListener('wheel', this.onWheel);
      this._addedScrollListener = true;
    } else if(!start && this._addedScrollListener) {
      document.removeEventListener('wheel', this.onWheel);
      this._addedScrollListener = false;
    }
  }

  public cleanup() {
    this.scrollAnimate(false);
    // this.animate(false);
  }

  public static createCanvas(colors?: string) {
    const canvas = document.createElement('canvas');
    canvas.width = WIDTH;
    canvas.height = HEIGHT;
    if(colors !== undefined) {
      canvas.dataset.colors = colors;
    }

    return canvas;
  }

  public static create(colors?: string) {
    const canvas = this.createCanvas(colors);
    const gradientRenderer = new ChatBackgroundGradientRenderer();
    gradientRenderer.init(canvas);

    return {gradientRenderer, canvas};
  }
}
