<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from "vue";

import type { Canvas as FabricCanvas } from "fabric";

const props = defineProps<{
  size: number;
  registerCanvas: (payload: {
    canvas: FabricCanvas;
    fabric: typeof import("fabric");
  }) => void;
  unregisterCanvas: () => void;
  backgroundColor: string;
}>();

const canvasElement = ref<HTMLCanvasElement | null>(null);
const containerElement = ref<HTMLDivElement | null>(null);
const isReady = ref(false);

let fabricCanvas: FabricCanvas | null = null;
let fabricModule: typeof import("fabric") | null = null;
let resizeObserver: ResizeObserver | null = null;

const syncCanvasDomStyles = () => {
  if (!fabricCanvas) {
    return;
  }
  const canvases = [fabricCanvas.lowerCanvasEl, fabricCanvas.upperCanvasEl];
  canvases.forEach((element) => {
    if (!element) {
      return;
    }
    element.classList.add("rounded-full");
    element.style.borderRadius = "9999px";
    element.style.backgroundColor = "transparent";
  });
};

const updateCssDimensions = (dimension?: number) => {
  if (!fabricCanvas) {
    return;
  }
  const targetSize =
    dimension ??
    containerElement.value?.clientWidth ??
    containerElement.value?.clientHeight ??
    null;
  if (!targetSize) {
    return;
  }
  fabricCanvas.setDimensions(
    {
      width: targetSize,
      height: targetSize,
    },
    { cssOnly: true }
  );
  fabricCanvas.calcOffset();
  fabricCanvas.requestRenderAll();
  syncCanvasDomStyles();
};

const observeContainer = () => {
  if (!containerElement.value || !fabricCanvas) {
    return;
  }
  resizeObserver?.disconnect();
  resizeObserver = new ResizeObserver((entries) => {
    const entry = entries[0];
    if (!entry) {
      return;
    }
    const dimension = Math.min(entry.contentRect.width, entry.contentRect.height);
    updateCssDimensions(dimension);
  });
  resizeObserver.observe(containerElement.value);
  updateCssDimensions();
};

const setupCanvas = async () => {
  if (typeof window === "undefined" || !canvasElement.value) {
    return;
  }

  fabricModule = await import("fabric");

  const { Canvas } = fabricModule;
  fabricCanvas = new Canvas(canvasElement.value, {
    backgroundColor: "transparent",
    selection: true,
    preserveObjectStacking: true,
  });

  fabricCanvas.setDimensions({ width: props.size, height: props.size });

  props.registerCanvas({ canvas: fabricCanvas, fabric: fabricModule });
  observeContainer();
  syncCanvasDomStyles();
  isReady.value = true;
};

onMounted(() => {
  setupCanvas();
});

onBeforeUnmount(() => {
  props.unregisterCanvas();
  resizeObserver?.disconnect();
  resizeObserver = null;
  fabricCanvas = null;
  fabricModule = null;
  isReady.value = false;
});

watch(
  () => props.size,
  (next, prev) => {
    if (!isReady.value || next === prev || !fabricCanvas) {
      return;
    }
    fabricCanvas.setDimensions({ width: next, height: next });
    updateCssDimensions();
    fabricCanvas.renderAll();
  }
);
</script>

<template>
  <div
    class="relative mx-auto flex max-w-[min(720px,100%)] items-center justify-center overflow-hidden rounded-3xl border border-slate-700/60 bg-slate-900/80 p-6 shadow-xl shadow-slate-950/40"
  >
    <div class="relative aspect-square w-full">
      <div class="absolute inset-4 sm:inset-5 md:inset-6 lg:inset-8">
        <div ref="containerElement" class="relative h-full w-full">
          <canvas
            ref="canvasElement"
            class="absolute inset-0 h-full w-full rounded-full"
            :width="size"
            :height="size"
          />
          <div
            v-if="!isReady"
            class="absolute inset-0 grid place-items-center rounded-full bg-slate-900/70"
          >
            <span class="text-sm font-medium text-slate-400">Loading canvas…</span>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
