import { HandlerContext, Handlers } from "$fresh/server.ts";
import {
  ImageMagick,
  initialize,
  MagickFormat,
  MagickGeometry,
} from "https://deno.land/x/imagemagick_deno@0.0.25/mod.ts";

import { parseMediaType } from "https://deno.land/std@0.175.0/media_types/parse_media_type.ts";
import * as cache from "@lib/cache/image.ts";
import { SILVERBULLET_SERVER } from "@lib/env.ts";
import { ConcurrentPromiseQueue, PromiseQueue } from "@lib/promise.ts";
import { BadRequestError } from "@lib/errors.ts";
import { createLogger } from "@lib/log.ts";

await initialize();

type ImageParams = {
  width: number;
  height: number;
};

const log = createLogger("api/image");

async function getRemoteImage(image: string) {
  log.debug("fetching", { image });
  const sourceRes = await fetch(image);
  if (!sourceRes.ok) {
    return "Error retrieving image from URL.";
  }
  const mediaType = parseMediaType(sourceRes.headers.get("Content-Type")!)[0];
  if (mediaType.split("/")[0] !== "image") {
    return "URL is not image type.";
  }
  return {
    buffer: new Uint8Array(await sourceRes.arrayBuffer()),
    mediaType,
  };
}

function getWidthHeight(
  current: { width: number; height: number },
  final: { width: number; height: number },
) {
  const ratio = (current.width / final.width) > (current.height / final.height)
    ? (current.height / final.height)
    : (current.width / final.width);

  return new MagickGeometry(
    Math.floor(current.width / ratio),
    Math.floor(current.height / ratio),
  );
}

function modifyImage(
  imageBuffer: Uint8Array,
  params: {
    width: number;
    mediaType: string;
    height: number;
    mode: "resize" | "crop";
  },
) {
  return new Promise<Uint8Array>((resolve) => {
    let format = MagickFormat.Jpeg;

    if (params.mediaType === "image/webp") {
      format = MagickFormat.Webp;
    }

    if (params.mediaType === "image/png") {
      format = MagickFormat.Png;
    }

    ImageMagick.read(imageBuffer, format, (image) => {
      const sizingData = getWidthHeight(image, params);
      if (params.mode === "resize") {
        image.resize(sizingData);
      } else {
        image.crop(sizingData);
      }
      image.write((data) => resolve(data));
    });
  });
}

function parseParams(reqUrl: URL) {
  const image = reqUrl.searchParams.get("image")?.replace(/^\//, "");
  if (image == null) {
    return "Missing 'image' query parameter.";
  }

  const height = Number(reqUrl.searchParams.get("height")) || 0;
  const width = Number(reqUrl.searchParams.get("width")) || 0;
  if (height === 0 && width === 0) {
    //return "Missing non-zero 'height' or 'width' query parameter.";
  }
  if (height < 0 || width < 0) {
    return "Negative height or width is not supported.";
  }
  const maxDimension = 2048;
  if (height > maxDimension || width > maxDimension) {
    return `Width and height cannot exceed ${maxDimension}.`;
  }
  return {
    image,
    height,
    width,
  };
}

const queue = new ConcurrentPromiseQueue(2);

async function processImage(imageUrl: string, params: ImageParams) {
  const remoteImage = await getRemoteImage(imageUrl);
  if (typeof remoteImage === "string") {
    log.warn("error fetching ", { remoteImage });
    throw new BadRequestError();
  }

  const modifiedImage = await modifyImage(remoteImage.buffer, {
    ...params,
    mediaType: remoteImage.mediaType,
    mode: "resize",
  });

  log.debug("resized image", {
    imageUrl,
    length: modifiedImage.length,
  });

  return [modifiedImage, remoteImage.mediaType] as const;
}

const GET = async (
  _req: Request,
  _ctx: HandlerContext,
): Promise<Response> => {
  const url = new URL(_req.url);

  const params = parseParams(url);
  if (typeof params === "string") {
    return new Response(params, { status: 400 });
  }

  const imageUrl = SILVERBULLET_SERVER + "/" + params.image;

  const cachedResponse = await cache.getImage({
    url: imageUrl,
    width: params.width,
    height: params.height,
  });
  if (cachedResponse) {
    log.debug("cached", { imageUrl });
    return new Response(cachedResponse.buffer.slice(), {
      headers: {
        "Content-Type": cachedResponse.mediaType,
      },
    });
  } else {
    log.debug("no image in cache");
  }

  const [resizedImage, mediaType] = await queue.enqueue(() =>
    processImage(imageUrl, params)
  );

  const clonedImage = resizedImage.slice();
  setTimeout(() => {
    cache.setImage(clonedImage, {
      url: imageUrl,
      width: params.width,
      height: params.height,
      mediaType: mediaType,
    });
  }, 50);

  log.debug("not-cached", { imageUrl });

  cache.getThumbhash({ url: imageUrl }).then(([hash]) => {
    if (!hash) {
      cache.createThumbhash(clonedImage.slice(), imageUrl).catch((_err) => {
        //
      });
    }
  });

  return new Response(resizedImage.slice(), {
    headers: {
      "Content-Type": mediaType,
    },
  });
};

export const handler: Handlers = {
  GET,
};
