/**
 * Generate the open graph.
 * It's highly inspired by the code from https://github.com/yuaanlin/yual.in/blob/main/pages/og_image/%5Bslug%5D.tsx
 * The original open source code don't have any license.
 * But I have get the approvement to use them here by asking the author https://twitter.com/yuaanlin.
 */
import { openGraphHeight, openGraphWidth } from '@/helpers/images';
import { options } from '@/helpers/schema';
import { Canvas, GlobalFonts, Image, type SKRSContext2D } from '@napi-rs/canvas';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import font from '../asserts/og/NotoSansSC-Bold.ttf?arraybuffer';
import logoDark from '../asserts/og/logo-dark.png?arraybuffer';

const getStringWidth = (text: string, fontSize: number) => {
  let result = 0;
  for (let idx = 0; idx < text.length; idx++) {
    if (text.charCodeAt(idx) > 255) {
      result += fontSize;
    } else {
      result += fontSize * 0.5;
    }
  }
  return result;
};

// Print text on SKRSContext with wrapping
const printAt = (
  context: SKRSContext2D,
  text: string,
  x: number,
  y: number,
  lineHeight: number,
  fitWidth: number,
  fontSize: number,
) => {
  // Avoid invalid fitWidth.
  const width = fitWidth || 0;

  if (width <= 0) {
    context.fillText(text, x, y);
    return;
  }

  for (let idx = 1; idx <= text.length; idx++) {
    const str = text.substring(0, idx);
    if (getStringWidth(str, fontSize) > width) {
      context.fillText(text.substring(0, idx - 1), x, y);
      printAt(context, text.substring(idx - 1), x, y + lineHeight, lineHeight, width, fontSize);
      return;
    }
  }
  context.fillText(text, x, y);
};

// Modified snippet from https://stackoverflow.com/questions/21961839/simulation-background-size-cover-in-canvas
const drawImageProp = (
  ctx: SKRSContext2D,
  img: Image,
  x: number,
  y: number,
  w: number,
  h: number,
  offsetX: number,
  offsetY: number,
) => {
  // keep bounds [0.0, 1.0]
  let ox = offsetX;
  if (offsetX < 0) ox = 0;
  if (offsetX > 1) ox = 1;
  let oy = offsetY;
  if (offsetY < 0) oy = 0;
  if (offsetY > 1) oy = 1;

  const iw = img.width;
  const ih = img.height;
  const r = Math.min(w / iw, h / ih);

  // new prop.width
  let nw = iw * r;
  // new prop.height
  let nh = ih * r;
  let ar = 1;

  // decide which gap to fill
  if (nw < w) ar = w / nw;
  if (Math.abs(ar - 1) < 1e-14 && nh < h) ar = h / nh; // updated
  nw *= ar;
  nh *= ar;

  // calc source rectangle
  let cw = iw / (nw / w);
  let ch = ih / (nh / h);

  let cx = (iw - cw) * ox;
  let cy = (ih - ch) * oy;

  // make sure source rectangle is valid
  if (cx < 0) cx = 0;
  if (cy < 0) cy = 0;
  if (cw > iw) cw = iw;
  if (ch > ih) ch = ih;

  // fill image in dest. rectangle
  ctx.drawImage(img, cx, cy, cw, ch, x, y, w, h);
};

const fetchCover = async (cover: string): Promise<Buffer> => {
  if (cover.startsWith('http')) {
    return Buffer.from(await (await fetch(cover)).arrayBuffer());
  }

  const coverPath = join(process.cwd(), 'public', cover);
  return await readFile(coverPath);
};

export { default as defaultOpenGraph } from '../asserts/og/open-graph.png?arraybuffer';

export interface OpenGraphProps {
  title: string;
  summary: string;
  cover: string;
}

export const drawOpenGraph = async ({ title, summary, cover }: OpenGraphProps) => {
  // Register the font if it doesn't exist
  if (!GlobalFonts.has('NotoSansSC-Bold')) {
    const fontBuffer = Buffer.from(font);
    GlobalFonts.register(fontBuffer, 'NotoSansSC-Bold');
  }

  // Fetch the cover image as the background
  const coverImage = new Image();
  coverImage.src = await fetchCover(cover);

  // Generate the logo image
  const logoImage = new Image();
  logoImage.src = Buffer.from(logoDark);

  // Mark sure the summary length is small enough to fit in
  const description = `${summary
    .replace(/<[^>]+>/g, '')
    .slice(0, 80)
    .trim()} ...`;

  // Start drawing the open graph
  const canvas = new Canvas(openGraphWidth, openGraphHeight);
  const ctx = canvas.getContext('2d');
  drawImageProp(ctx, coverImage, 0, 0, openGraphWidth, openGraphHeight, 0.5, 0.5);
  ctx.fillStyle = 'rgba(0,0,0,0.6)';
  ctx.fillRect(0, 0, openGraphWidth, openGraphHeight);
  ctx.save();

  // Add website title
  ctx.fillStyle = '#e0c2bb';
  ctx.font = '800 64px NotoSansSC-Bold';
  printAt(ctx, options.title, 96, 180, 96, openGraphWidth, 64);

  // Add website logo
  ctx.drawImage(logoImage, 940, 120, 160, 160);

  // Add article title
  ctx.fillStyle = '#fff';
  ctx.font = '800 48px NotoSansSC-Bold';
  printAt(ctx, title, 96, openGraphHeight / 2 - 64, 96, openGraphWidth - 192, 64);

  // Add article summary
  ctx.font = '800 36px NotoSansSC-Bold';
  ctx.fillStyle = 'rgba(255,255,255,0.5)';
  printAt(ctx, description, 96, openGraphHeight - 200, 48, openGraphWidth - 192, 36);

  ctx.restore();

  return await canvas.encode('png');
};