import React, { useState } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { createGIF } from "./animated-gif";
import { getSvgMetadata, SvgWithBackground } from "./auto-sizing-svg";

import "./export-svg.css";

function getSvgDocument(svgMarkup: string): string {
  return [
    `<?xml version="1.0" encoding="utf-8"?>`,
    "<!-- Generator: https://github.com/toolness/mystic-symbolic -->",
    '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">',
    svgMarkup,
  ].join("\n");
}

type ProgressHandler = (value: number | null) => void;

type ImageExporter = (
  svgEl: SVGSVGElement,
  onProgress: ProgressHandler
) => Promise<string>;

/**
 * Initiates a download on the user's browser which downloads the given
 * SVG element under the given filename, using the given export algorithm.
 */
async function exportImage(
  svgRef: React.RefObject<SVGSVGElement>,
  basename: string,
  ext: string,
  onProgress: ProgressHandler,
  exporter: ImageExporter
) {
  const svgEl = svgRef.current;
  if (!svgEl) {
    alert("Oops, an error occurred! Please try again later.");
    return;
  }
  const url = await exporter(svgEl, onProgress);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = `${basename}.${ext}`;
  document.body.append(anchor);
  anchor.click();
  document.body.removeChild(anchor);
  onProgress(null);
}

function getCanvasContext2D(
  canvas: HTMLCanvasElement
): CanvasRenderingContext2D {
  const ctx = canvas.getContext("2d");
  if (!ctx) throw new Error(`Unable to get 2D context for canvas!`);
  return ctx;
}

function getSvgUrl(svgMarkup: string): string {
  return `data:image/svg+xml;utf8,${encodeURIComponent(
    getSvgDocument(svgMarkup)
  )}`;
}

/**
 * Exports the given SVG as an SVG in a data URL.
 */
const exportSvg: ImageExporter = async (svgEl) => getSvgUrl(svgEl.outerHTML);

/**
 * Exports the given SVG as a PNG in a data URL.
 */
const exportPng: ImageExporter = async (svgEl, onProgress) => {
  const dataURL = await exportSvg(svgEl, onProgress);

  return new Promise((resolve, reject) => {
    const canvas = document.createElement("canvas");
    const img = document.createElement("img");

    img.onload = () => {
      canvas.width = img.width;
      canvas.height = img.height;
      const ctx = getCanvasContext2D(canvas);
      ctx.drawImage(img, 0, 0);
      resolve(canvas.toDataURL());
    };
    img.onerror = reject;
    img.src = dataURL;
  });
};

function drawImage(
  canvas: HTMLCanvasElement,
  dataURL: string,
  scale: number
): Promise<void> {
  return new Promise((resolve, reject) => {
    const img = document.createElement("img");

    img.onload = () => {
      const scaledWidth = Math.floor(img.width * scale);
      const scaledHeight = Math.floor(img.height * scale);
      canvas.width = scaledWidth;
      canvas.height = scaledHeight;
      const ctx = getCanvasContext2D(canvas);
      ctx.drawImage(
        img,
        0,
        0,
        img.width,
        img.height,
        0,
        0,
        scaledWidth,
        scaledHeight
      );
      resolve();
    };
    img.onerror = reject;
    img.src = dataURL;
  });
}

/**
 * Exports the given SVG as a GIF in a data URL.
 */
async function exportGif(
  animate: ExportableAnimation,
  scale: number,
  svgEl: SVGSVGElement,
  onProgress: (value: number) => void
): Promise<string> {
  const fps = animate.fps || 15;
  const msecPerFrame = 1000 / fps;
  const numFrames = Math.floor(animate.duration / msecPerFrame);
  const svgMeta = getSvgMetadata(svgEl);
  const render = (animPct: number) => (
    <SvgWithBackground {...svgMeta}>
      {animate.render(animPct)}
    </SvgWithBackground>
  );
  const gif = createGIF();

  for (let i = 0; i < numFrames; i++) {
    onProgress(i / numFrames);
    const canvas = document.createElement("canvas");
    const animPct = i / numFrames;
    const markup = renderToStaticMarkup(render(animPct));
    const url = getSvgUrl(markup);
    await drawImage(canvas, url, scale);
    gif.addFrame(canvas, { delay: msecPerFrame });
  }

  return new Promise((resolve, reject) => {
    gif.on("finished", function (blob) {
      onProgress(1);
      resolve(URL.createObjectURL(blob));
    });
    gif.render();
  });
}

export type AnimationRenderer = (time: number) => JSX.Element;

export type ExportableAnimation = {
  duration: number;
  fps?: number;
  render: AnimationRenderer;
};

const DEFAULT_GIF_SCALE = 0.5;

export const ExportWidget: React.FC<{
  svgRef: React.RefObject<SVGSVGElement>;
  animate?: ExportableAnimation | false;
  gifScale?: number;
  basename: string;
}> = ({ svgRef, basename, animate, gifScale }) => {
  const [progress, setProgress] = useState<number | null>(null);

  if (progress !== null) {
    return (
      <div className="overlay-wrapper">
        <p>Exporting&hellip;</p>
        <progress value={progress} />
      </div>
    );
  }

  return (
    <>
      <button
        onClick={() =>
          exportImage(svgRef, basename, "svg", setProgress, exportSvg)
        }
      >
        Export SVG
      </button>{" "}
      <button
        onClick={() =>
          exportImage(svgRef, basename, "png", setProgress, exportPng)
        }
      >
        Export PNG
      </button>{" "}
      {animate && (
        <button
          onClick={() =>
            exportImage(
              svgRef,
              basename,
              "gif",
              setProgress,
              exportGif.bind(null, animate, gifScale || DEFAULT_GIF_SCALE)
            )
          }
        >
          Export GIF
        </button>
      )}
    </>
  );
};
