const options = {
  borderSize: 2,
  fontSize: 9,
  backgroundColor: "black",
  tickColor: "#ddd",
  labelColor: "#ddd",
  gradient: ["red 1%", "#ff0 16%", "lime 45%", "#080 100%"],
  dbRange: 48,
  dbTickSize: 6,
  maskTransition: "0.1s",
};

export default class PeakMeter {
  tickWidth?: number;
  elementWidth?: number;
  elementHeight?: number;
  meterHeight?: number;
  meterWidth?: number;
  meterTop?: number;
  vertical?: boolean;
  channelCount: number;
  channelMasks: any[];
  channelPeaks: any[];
  channelPeakLabels: any[];
  maskSizes: any[];
  textLabels: any[];

  constructor() {
    this.vertical = true;
    this.channelCount = 1;
    this.channelMasks = [];
    this.channelPeaks = [];
    this.channelPeakLabels = [];
    this.maskSizes = [];
    this.textLabels = [];
  }
  getBaseLog = (x, y) => {
    return Math.log(y) / Math.log(x);
  };

  dbFromFloat = (floatVal) => {
    return this.getBaseLog(10, floatVal) * 20;
  };

  setOptions = (userOptions) => {
    for (let k in userOptions) {
      if (userOptions.hasOwnProperty(k)) {
        options[k] = userOptions[k];
      }
    }
    this.tickWidth = options.fontSize * 2.0;
    this.meterTop = options.fontSize * 1.5 + options.borderSize;
  };

  createMeterNode = (sourceNode, audioCtx) => {
    const c = sourceNode.channelCount;
    const meterNode = audioCtx.createScriptProcessor(2048, c, c);
    sourceNode.connect(meterNode);
    meterNode.connect(audioCtx.destination);
    return meterNode;
  };

  createContainerDiv = (parent) => {
    const meterElement = document.createElement("div");
    meterElement.style.position = "relative";
    meterElement.style.width = this.elementWidth + "px";
    meterElement.style.height = this.elementHeight + "px";
    meterElement.style.backgroundColor = options.backgroundColor;
    parent.appendChild(meterElement);
    return meterElement;
  };

  createMeter = (domElement, meterNode, optionsOverrides) => {
    this.setOptions(optionsOverrides);
    this.elementWidth = domElement.clientWidth;
    this.elementHeight = domElement.clientHeight;

    const meterElement = this.createContainerDiv(domElement);
    if (this.elementHeight && this.elementWidth) {
      if (this.elementWidth > this.elementHeight) {
        this.vertical = false;
      }
      this.meterHeight =
        this.elementHeight -
        (this.meterTop || 0) -
        options.borderSize;
      this.meterWidth =
        this.elementWidth -
        (this.tickWidth || 0) -
        options.borderSize;
      this.createTicks(meterElement);
      this.createRainbow(
        meterElement,
        this.meterWidth,
        this.meterHeight,
        this.meterTop,
        this.tickWidth,
      );
      this.channelCount = meterNode.channelCount;
      let channelWidth = this.meterWidth / this.channelCount;
      if (!this.vertical) {
        channelWidth = this.meterHeight / this.channelCount;
      }
      let channelLeft = this.tickWidth;
      if (!this.vertical) {
        channelLeft = this.meterTop;
      }
      for (let i = 0; i < this.channelCount; i++) {
        this.createChannelMask(
          meterElement,
          options.borderSize,
          this.meterTop,
          channelLeft,
          false,
        );
        this.channelMasks[i] = this.createChannelMask(
          meterElement,
          channelWidth,
          this.meterTop,
          channelLeft,
          options.maskTransition,
        );
        this.channelPeaks[i] = 0.0;
        this.channelPeakLabels[i] = this.createPeakLabel(
          meterElement,
          channelWidth,
          channelLeft,
        );
        if (channelLeft) {
          channelLeft += channelWidth;
        }

        this.maskSizes[i] = 0;
        this.textLabels[i] = "-∞";
      }
      meterNode.onaudioprocess = this.updateMeter;
      meterElement.addEventListener(
        "click",
        () => {
          for (let i = 0; i < this.channelCount; i++) {
            this.channelPeaks[i] = 0.0;
            this.textLabels[i] = "-∞";
          }
        },
        false,
      );
      this.paintMeter();
    }
  };

  createTicks = (parent) => {
    const numTicks = Math.floor(options.dbRange / options.dbTickSize);
    let dbTickLabel = 0;
    if (this.vertical) {
      let dbTickTop = options.fontSize + options.borderSize;
      for (let i = 0; i < numTicks; i++) {
        const dbTick = document.createElement("div");
        parent.appendChild(dbTick);
        dbTick.style.width = this.tickWidth + "px";
        dbTick.style.textAlign = "right";
        dbTick.style.color = options.tickColor;
        dbTick.style.fontSize = options.fontSize + "px";
        dbTick.style.position = "absolute";
        dbTick.style.top = dbTickTop + "px";
        dbTick.textContent = dbTickLabel + "";
        dbTickLabel -= options.dbTickSize;
        dbTickTop += (this.meterHeight || 0) / numTicks;
      }
    } else {
      this.tickWidth = (this.meterWidth || 0) / numTicks;
      let dbTickRight = options.fontSize * 2;
      for (let i = 0; i < numTicks; i++) {
        let dbTick = document.createElement("div");
        parent.appendChild(dbTick);
        dbTick.style.width = this.tickWidth + "px";
        dbTick.style.textAlign = "right";
        dbTick.style.color = options.tickColor;
        dbTick.style.fontSize = options.fontSize + "px";
        dbTick.style.position = "absolute";
        dbTick.style.right = dbTickRight + "px";
        dbTick.textContent = dbTickLabel + "";
        dbTickLabel -= options.dbTickSize;
        dbTickRight += this.tickWidth;
      }
    }
  };

  createRainbow = (parent, width, height, top, left) => {
    let rainbow = document.createElement("div");
    parent.appendChild(rainbow);
    rainbow.style.width = width + "px";
    rainbow.style.height = height + "px";
    rainbow.style.position = "absolute";
    rainbow.style.top = top + "px";
    if (this.vertical) {
      rainbow.style.left = left + "px";
      var gradientStyle =
        "linear-gradient(to bottom, " +
        options.gradient.join(", ") +
        ")";
    } else {
      rainbow.style.left = options.borderSize + "px";
      var gradientStyle =
        "linear-gradient(to left, " +
        options.gradient.join(", ") +
        ")";
    }
    rainbow.style.backgroundImage = gradientStyle;
    return rainbow;
  };

  createPeakLabel = (parent, width, left) => {
    const label = document.createElement("div");
    parent.appendChild(label);
    label.style.textAlign = "center";
    label.style.color = options.labelColor;
    label.style.fontSize = options.fontSize + "px";
    label.style.position = "absolute";
    label.textContent = "-∞";
    if (this.vertical) {
      label.style.width = width + "px";
      label.style.top = options.borderSize + "px";
      label.style.left = left + "px";
    } else {
      label.style.width = options.fontSize * 2 + "px";
      label.style.right = options.borderSize + "px";
      label.style.top = width * 0.25 + left + "px";
    }
    return label;
  };

  createChannelMask = (parent, width, top, left, transition) => {
    const channelMask = document.createElement("div");
    parent.appendChild(channelMask);
    channelMask.style.position = "absolute";
    if (this.vertical) {
      channelMask.style.width = width + "px";
      channelMask.style.height = this.meterHeight + "px";
      channelMask.style.top = top + "px";
      channelMask.style.left = left + "px";
    } else {
      channelMask.style.width = this.meterWidth + "px";
      channelMask.style.height = width + "px";
      channelMask.style.top = left + "px";
      channelMask.style.right = options.fontSize * 2 + "px";
    }
    channelMask.style.backgroundColor = options.backgroundColor;
    if (transition) {
      if (this.vertical) {
        channelMask.style.transition =
          "height " + options.maskTransition;
      } else {
        channelMask.style.transition =
          "width " + options.maskTransition;
      }
    }
    return channelMask;
  };

  maskSize = (floatVal) => {
    const meterDimension = this.vertical
      ? this.meterHeight
      : this.meterWidth;
    if (floatVal === 0.0) {
      return meterDimension;
    } else {
      if (meterDimension) {
        const d = options.dbRange * -1;
        const returnVal = Math.floor(
          (this.dbFromFloat(floatVal) * meterDimension) / d,
        );
        if (returnVal > meterDimension) {
          return meterDimension;
        } else {
          return returnVal;
        }
      }
    }
  };

  updateMeter = (audioProcessingEvent) => {
    const inputBuffer = audioProcessingEvent.inputBuffer;
    let i;
    const channelData: any = [];
    const channelMaxes: any = [];
    for (i = 0; i < this.channelCount; i++) {
      channelData[i] = inputBuffer.getChannelData(i);
      channelMaxes[i] = 0.0;
    }
    for (let sample = 0; sample < inputBuffer.length; sample++) {
      for (i = 0; i < this.channelCount; i++) {
        if (Math.abs(channelData[i][sample]) > channelMaxes[i]) {
          channelMaxes[i] = Math.abs(channelData[i][sample]);
        }
      }
    }
    for (i = 0; i < this.channelCount; i++) {
      this.maskSizes[i] = this.maskSize(channelMaxes[i]);
      if (channelMaxes[i] > this.channelPeaks[i]) {
        this.channelPeaks[i] = channelMaxes[i];
        this.textLabels[i] = this.dbFromFloat(
          this.channelPeaks[i],
        ).toFixed(1);
      }
    }
  };

  paintMeter = () => {
    for (let i = 0; i < this.channelCount; i++) {
      if (this.vertical) {
        this.channelMasks[i].style.height = this.maskSizes[i] + "px";
      } else {
        this.channelMasks[i].style.width = this.maskSizes[i] + "px";
      }
      this.channelPeakLabels[i].textContent = this.textLabels[i];
    }
    window.requestAnimationFrame(this.paintMeter);
  };
}
