import { hash, isLocalImage, rgbToHex } from "@lib/string.ts";
import * as cache from "@lib/cache/cache.ts";
import {
  ImageMagick,
  MagickGeometry,
} from "https://deno.land/x/imagemagick_deno@0.0.25/mod.ts";
import { createLogger } from "@lib/log.ts";
import { generateThumbhash } from "@lib/thumbhash.ts";
import { SILVERBULLET_SERVER } from "@lib/env.ts";

type ImageCacheOptionsBasic = {
  url: string;
  mediaType?: string;
};

interface ImageCacheOptionsDimensions extends ImageCacheOptionsBasic {
  width: number;
  height: number;
}

interface ImageCacheOptionsSuffix extends ImageCacheOptionsBasic {
  suffix: string;
}

type ImageCacheOptions = ImageCacheOptionsDimensions | ImageCacheOptionsSuffix;

const CACHE_KEY = "images";
const log = createLogger("cache/image");

function getCacheKey(
  opts: ImageCacheOptions,
) {
  const isLocal = isLocalImage(opts.url);
  const url = new URL(
    isLocal ? `${SILVERBULLET_SERVER}/${opts.url}` : opts.url,
  );

  const _suffix = "suffix" in opts
    ? opts.suffix
    : `${opts.width}:${opts.height}`;

  const cacheId = `${CACHE_KEY}:${url.hostname}:${
    url.pathname.replaceAll("/", ":")
  }:${_suffix}`
    .replace(
      "::",
      ":",
    );
  return cacheId;
}

export function createThumbhash(
  image: Uint8Array,
  url: string,
): Promise<string> {
  return new Promise((res, rej) => {
    try {
      ImageMagick.read(image.slice(), (_image) => {
        _image.resize(new MagickGeometry(100, 100));
        _image.getPixels((pixels) => {
          const bytes = pixels.toByteArray(
            0,
            0,
            _image.width,
            _image.height,
            "RGBA",
          );
          if (!bytes) return;
          const [hash, average] = generateThumbhash(
            bytes,
            _image.width,
            _image.height,
          );

          if (average) {
            cache.set(
              getCacheKey({
                url,
                suffix: "average",
              }),
              rgbToHex(average.r, average.g, average.b),
            );
          }

          if (hash) {
            const b64 = btoa(String.fromCharCode(...hash));
            cache.set(
              getCacheKey({
                url,
                suffix: "thumbnail",
              }),
              b64,
            );
            res(b64);
          }
        });
      });
    } catch (err) {
      rej(err);
    }
  });
}

function verifyImage(
  imageBuffer: Uint8Array,
) {
  return new Promise<boolean>((resolve) => {
    try {
      ImageMagick.read(imageBuffer, (image) => {
        resolve(image.height !== 0 && image.width !== 0);
      });
    } catch (_err) {
      resolve(false);
    }
  });
}

export function getThumbhash({ url }: { url: string }) {
  return Promise.all(
    [
      cache.get<Uint8Array>(
        getCacheKey({
          url,
          suffix: "thumbnail",
        }),
      ),
      cache.get<string>(
        getCacheKey({
          url,
          suffix: "average",
        }),
      ),
    ] as const,
  );
}

export async function getImage(opts: ImageCacheOptions) {
  const cacheKey = getCacheKey(opts);

  const pointerCacheRaw = await cache.get<string>(cacheKey);
  if (!pointerCacheRaw) return;

  const pointerCache = typeof pointerCacheRaw === "string"
    ? JSON.parse(pointerCacheRaw)
    : pointerCacheRaw;

  const imageContent = await cache.get(`image:${pointerCache.id}`, true);
  if (!imageContent) return;

  return {
    ...pointerCache,
    buffer: imageContent,
  };
}

export async function setImage(
  buffer: Uint8Array,
  opts: ImageCacheOptions,
) {
  const clone = new Uint8Array(buffer);

  const imageCorrect = await verifyImage(clone);
  if (!imageCorrect) {
    log.info("failed to store image", { url: opts.url });
    return;
  }

  const cacheKey = getCacheKey(opts);
  const pointerId = await hash(cacheKey);

  await cache.set(`image:${pointerId}`, clone, { expires: 60 * 60 * 24 });

  await cache.set(
    cacheKey,
    JSON.stringify({
      id: pointerId,
      ...("suffix" in opts
        ? { suffix: opts.suffix }
        : { width: opts.width, height: opts.height }),
    }),
    { expires: 60 * 60 * 24 },
  );
}
