import getResampledWaveformBounds from './getResampledWaveformBounds';

export enum WaveformMode {
  Bars = 'Bars',
  FullRes = 'FullRes',
}

export enum SymmetryMode {
  Unlocked = 'Unlocked',
  Asymmetrical = 'Asymmetrical',
  Symmetrical = 'Symmetrical',
  OneHalf = 'OneHalf',
}

export type GenerateWaveformSettings = {
  foregroundColor: string;
  backgroundColor: string;
  taper: number;
  taperCurve: number;
  trimStart: number;
  trimEnd: number;
  width: number;
  height: number;
  mode: WaveformMode;
  barWidth: number;
  barGap: number;
  barRounding: number;
  minThickness: number;
  symmetryMode: SymmetryMode;
};

const iterateBounds = (bounds: Float32Array, callback: (min: number, max: number, index: number) => void) => {
  for (let i = 0; i < bounds.length; i += 2) {
    callback(bounds[i], bounds[i + 1], i / 2);
  }
};

const trim = (buffer: Float32Array, start: number, end: number) => {
  const startIndex = Math.floor(start * buffer.length);
  const endIndex = buffer.length - Math.floor(end * buffer.length);
  return buffer.subarray(startIndex, endIndex);
};

const taper = (settings: GenerateWaveformSettings, progress: number) => {
  const taperAmount =
    progress <= settings.taper / 2
      ? progress / (settings.taper / 2)
      : progress >= 1 - settings.taper / 2
      ? (1 - progress) / (settings.taper / 2)
      : 1;
  return Math.pow(taperAmount, settings.taperCurve * 2);
};

const makeSVG = (
  { width, height, backgroundColor, foregroundColor, symmetryMode }: GenerateWaveformSettings,
  watermarked: boolean,
  content: string
) =>
  `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">${
    backgroundColor !== 'transparent'
      ? `<rect x="0" y="0" width="${width}" height="${height}" fill="${backgroundColor}" />`
      : ''
  }<g fill="${foregroundColor}">${content}${
    watermarked
      ? `<style>.text{ font: 200 12px Arial; }</style><text class="text" x="${width - 132}" y="${
          symmetryMode === SymmetryMode.OneHalf ? 15 : height - 5
        }">wavtool.com/waveforms</text>`
      : ''
  }</g></svg>`;

const generateBars = (buffer: Float32Array, watermarked: boolean, settings: GenerateWaveformSettings) => {
  const barTotalWidth = settings.barGap + settings.barWidth;
  const numBars = Math.floor(settings.width / barTotalWidth);
  const bounds = getResampledWaveformBounds(trim(buffer, settings.trimStart, settings.trimEnd), numBars);
  const barDefs: string[] = [];
  const barRounding = Math.min((settings.barRounding * settings.barWidth) / 2, settings.barWidth / 2);
  const minBarHeight = Math.max(2 * barRounding, 2);
  const contentWidth = barTotalWidth * numBars - settings.barGap;
  const contentXOffset = (settings.width - contentWidth) / 2;
  iterateBounds(bounds, (min, max, index) => {
    const progress = (index + 0.5) / numBars;
    const taperAmount = taper(settings, progress);
    min = settings.symmetryMode !== SymmetryMode.Unlocked ? Math.min(0, min) : min;
    max = settings.symmetryMode !== SymmetryMode.Unlocked ? Math.max(0, max) : max;

    if (settings.symmetryMode === SymmetryMode.Symmetrical || settings.symmetryMode === SymmetryMode.OneHalf) {
      max = Math.max(-min, max);
      min = -max;
    }

    let top = (1 - max * taperAmount) * settings.height * 0.5;
    let bottom = (1 - min * taperAmount) * settings.height * 0.5;

    if (settings.symmetryMode === SymmetryMode.OneHalf) {
      top = settings.height - (bottom - top);
      bottom = settings.height;
    }

    let height = bottom - top;
    if (height < minBarHeight) {
      const diff = minBarHeight - height;
      top -= diff / 2;
      height += diff;
    }
    barDefs.push(
      `<rect x="${(barTotalWidth * index + contentXOffset).toFixed(2)}" y="${top.toFixed(
        2
      )}" width="${settings.barWidth.toFixed(2)}" height="${height.toFixed(
        2
      )}" rx="${barRounding}" ry="${barRounding}" />`
    );
  });

  return makeSVG(settings, watermarked, `${barDefs.join('')}`);
};

const generateFullRes = (buffer: Float32Array, watermarked: boolean, settings: GenerateWaveformSettings) => {
  const bounds = getResampledWaveformBounds(trim(buffer, settings.trimStart, settings.trimEnd), settings.width * 2);
  const path = [];
  const pathPoints: Float32Array = new Float32Array(bounds.length * 2);
  let pointIndex = 0;
  if (settings.symmetryMode === SymmetryMode.OneHalf) {
    pathPoints[pointIndex] = 0;
    pathPoints[pointIndex + 1] = settings.height;
    pointIndex += 2;
    pathPoints[pointIndex] = settings.width;
    pathPoints[pointIndex + 1] = settings.height;
    pointIndex += 2;
    for (let i = bounds.length - 2; i >= 0; i -= 2) {
      const progress = (i + 1) / bounds.length;
      const taperAmount = taper(settings, progress);

      const max = (Math.max(bounds[i + 1], -bounds[i]) - Math.min(bounds[i], -bounds[i + 1])) / 2;
      const x = (i / 2) * (settings.width / (bounds.length / 2));
      const y = (1 - max * taperAmount) * settings.height - settings.minThickness;
      pathPoints[pointIndex] = x;
      pathPoints[pointIndex + 1] = y;
      pointIndex += 2;
    }
  } else if (settings.symmetryMode === SymmetryMode.Symmetrical) {
    for (let i = 0; i < bounds.length; i += 2) {
      const progress = (i + 1) / bounds.length;
      const taperAmount = taper(settings, progress);
      const min = Math.min(bounds[i], -bounds[i + 1]);
      const x = (i / 2) * (settings.width / (bounds.length / 2));
      const y = (1 - min * taperAmount) * settings.height * 0.5 + settings.minThickness / 2;
      pathPoints[pointIndex] = x;
      pathPoints[pointIndex + 1] = y;
      pointIndex += 2;
    }
    for (let i = bounds.length - 2; i >= 0; i -= 2) {
      const progress = (i + 1) / bounds.length;
      const taperAmount = taper(settings, progress);

      const max = Math.max(bounds[i + 1], -bounds[i]);
      const x = (i / 2) * (settings.width / (bounds.length / 2));
      const y = (1 - max * taperAmount) * settings.height * 0.5 - settings.minThickness / 2;
      pathPoints[pointIndex] = x;
      pathPoints[pointIndex + 1] = y;
      pointIndex += 2;
    }
  } else {
    for (let i = 0; i < bounds.length; i += 2) {
      const progress = (i + 1) / bounds.length;
      const taperAmount = taper(settings, progress);

      const min = settings.symmetryMode !== SymmetryMode.Unlocked ? Math.min(0, bounds[i]) : bounds[i];
      const x = (i / 2) * (settings.width / (bounds.length / 2));
      const y = (1 - min * taperAmount) * settings.height * 0.5 + settings.minThickness / 2;
      pathPoints[pointIndex] = x;
      pathPoints[pointIndex + 1] = y;
      pointIndex += 2;
    }
    for (let i = bounds.length - 2; i >= 0; i -= 2) {
      const progress = (i + 1) / bounds.length;
      const taperAmount = taper(settings, progress);

      const max = settings.symmetryMode !== SymmetryMode.Unlocked ? Math.max(0, bounds[i + 1]) : bounds[i + 1];
      const x = (i / 2) * (settings.width / (bounds.length / 2));
      const y = (1 - max * taperAmount) * settings.height * 0.5 - settings.minThickness / 2;
      pathPoints[pointIndex] = x;
      pathPoints[pointIndex + 1] = y;
      pointIndex += 2;
    }
  }
  for (let i = 0; i < pathPoints.length; i += 2) {
    path.push(`${pathPoints[i].toFixed(2)},${pathPoints[i + 1].toFixed(2)}`);
  }
  return makeSVG(settings, watermarked, `<polyline points="${path.join(' ')}" />`);
};

const generateWaveform = (buffer: Float32Array, watermarked: boolean, settings: GenerateWaveformSettings) => {
  if (settings.mode === WaveformMode.Bars) {
    return generateBars(buffer, watermarked, settings);
  } else if (settings.mode === WaveformMode.FullRes) {
    return generateFullRes(buffer, watermarked, settings);
  }
};

export default generateWaveform;
