import ffmpeg from "ffmpeg";
import { createCanvas, loadImage } from "canvas";
import fs from "fs";
import path from "path";
import * as dotenv from "dotenv";
dotenv.config();

const videoUrl = process.env.VIDEO_SOURCE || "./frames.mp4";
const extractionFrameSize = Number(process.env.FRAME_SIZE) || 30;
const framesDirectory = process.env.FRAMES_DIR || "./frames";
const spritesDirectory = process.env.SPRITES_DIR || "./sprites";
const spriteRowSize = Number(process.env.SPRITE_ROW_SIZE) || 6;
const spriteMaxLines = Number(process.env.SPRITE_MAX_LINES) || 12;

const sortAlphaNum = (a: string, b: string) =>
  a.localeCompare(b, "en", { numeric: true });

/**
 * Create sprite images and write to the filesystem based on input files, and needed width/height of the sprite
 *
 * @param files
 * @param width
 * @param height
 */
const createSprites = async (
  files: string[],
  width: number,
  height: number,
  videoId: number
) => {
  const thumbWidth = width / spriteRowSize; //e.g. 142
  const thumbHeight = ((width / spriteRowSize) * height) / width; //e.g. 80
  const canvas = createCanvas(
    thumbWidth * spriteRowSize,
    thumbHeight * spriteMaxLines
  );
  const ctx = canvas.getContext("2d");
  let currentPosition = 0;
  let currentLine = 0;
  let currentImg = 0;
  let dx = 0;
  let dy = 0;
  for (let i = 0; i <= files.length; i++) {
    await loadImage(files[i]).then((image) => {
      console.log(
        `draw image[${i}] at dx[${dx}] dy[${dy}] w[${thumbWidth}] h[${thumbHeight}]`
      );
      ctx.drawImage(image, dx, dy, thumbWidth, thumbHeight);
      if (currentPosition < spriteRowSize - 1) {
        dx += thumbWidth;
        currentPosition += 1;
      } else {
        dx = 0;
        dy += thumbHeight;
        currentPosition = 0;
        currentLine += 1;
      }
      if (currentLine === spriteMaxLines) {
        console.log("Sprite is full, write it to fs and reset canvas.");
        currentLine = 0;
        const buffer = canvas.toBuffer("image/jpeg");
        fs.writeFileSync(
          `${spritesDirectory}/video-${videoId}-sprite-${currentImg}.jpeg`,
          buffer
        );
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        dx = 0;
        dy = 0;
        currentImg += 1;
      } else if (i + 1 === files.length) {
        console.log(
          "Last frame file is processed, write last non full image to fs."
        );
        const buffer = canvas.toBuffer("image/jpeg");
        fs.writeFileSync(
          `${spritesDirectory}/video-${videoId}-sprite-${currentImg}.jpeg`,
          buffer
        );
        cleanupDirectory(framesDirectory);
      }
    });
  }
};

/**
 * Delete if exist and recreate directories.
 *
 * @param directories
 */
const createDirectories = (directories: string[]) => {
  directories.forEach((d) => {
    fs.rmdir(
      d,
      {
        recursive: true,
        //force: true,
      },
      (err) => {
        if (err) console.log(err);
        fs.mkdir(d, (err) => {
          if (err) return;
        });
      }
    );
  });
};

/**
 * Remove everything in the directory
 *
 * @param directory
 */
const cleanupDirectory = (directory: string) => {
  fs.readdir(directory, (err, files) => {
    if (err) console.error(err);
    for (const file of files) {
      fs.unlink(path.join(directory, file), (err) => {
        if (err) console.error(err);
      });
    }
  });
};

/**
 * Extract frames from a video source.
 * Generate sprites based on that images.
 *
 * @param src
 */
export const extractFrames = (src: string, videoId: number): void => {
  console.log(`Extract frames from [${videoUrl}]`);
  createDirectories([spritesDirectory, framesDirectory]);
  try {
    new ffmpeg(src).then(
      async (video) => {
        video
          .fnExtractFrameToJPG(framesDirectory, {
            every_n_frames: extractionFrameSize,
          })
          .then((files: string[]) => {
            createSprites(
              files.sort(sortAlphaNum),
              video.metadata.width || 852,
              video.metadata.height || 480,
              videoId
            ).then(() => {
              console.log("Extraction done.");
            });
          })
          .catch((err) => {
            console.log(`Error: ${err}`);
          });
      },
      (err: string) => {
        console.log(`Error: ${err}`);
      }
    );
  } catch (e) {
    console.log(`Error: ${e.code} ${e.msg}`);
  }
};

extractFrames(videoUrl, 0);
