<script setup lang="ts">
import { createIcons, icons } from "lucide";
import { ref, onMounted, useTemplateRef, nextTick, onUpdated } from "vue";

enum Filter {
  None = "None",
  Gray = "Gray",
  Sepia = "Sepia",
  Invert = "Invert",
  Blur = "Blur",
  Brightness = "Brightness",
  Contrast = "Contrast",
  HueShift = "Hue shift",
  Saturate = "Saturate",
}
enum PAPosition {
  None = "None",
  BL = "Bottom Left",
  BC = "Bottom Center",
  BR = "Bottom Right",
  TL = "Top Left",
  TC = "Top Center",
  TR = "Top Right",
}

const PA_RATIO = 404 / 646;
const PREVIEW_SIZE = 512;

const visible = ref<boolean>(false);
const srcData = ref<string | null>(null);
const srcLoaded = ref<boolean>(false);

const centerX = ref<number>(0.5);
const centerY = ref<number>(0.5);
const zoom = ref<number>(1);
const filter = ref<Filter>(Filter.None);
const filterValue = ref<number>(1);
const paPos = ref<PAPosition>(PAPosition.BR);
const paScale = ref<number>(0.2);
const paMargin = ref<number>(0.05);
const targetSize = ref<number>(1024);

const input = useTemplateRef<HTMLInputElement>("input");
const image = useTemplateRef<HTMLImageElement>("image");
const parentalAdvisory = useTemplateRef<HTMLImageElement>("parentalAdvisory");
const canvas = useTemplateRef<HTMLCanvasElement>("canvas");

function openImage() {
  if (!input.value?.files?.length) {
    return;
  }
  const reader = new FileReader();
  reader.onload = () => {
    srcData.value = reader.result as string;
  };
  reader.readAsDataURL(input.value.files[0] as Blob);
}

function imageOnLoad() {
  srcLoaded.value = true;
  setTimeout(draw);
}

function randomElement<T>(items: T[]): T {
  // @ts-expect-error: arbitrary type bullshit
  return items[Math.floor(Math.random() * items.length)];
}

function randomize() {
  centerX.value = 0.25 + Math.random() * 0.5;
  centerY.value = 0.25 + Math.random() * 0.5;
  zoom.value = 1 + Math.random();
  filter.value = randomElement(Object.values(Filter));
  filterValue.value = 0.5 + Math.random() * 2;
  paPos.value = randomElement(Object.values(PAPosition));
  paScale.value = 0.1 + Math.random() * 0.2;
  paMargin.value = Math.random() * 0.1;
  asyncDraw();
}

function reset() {
  centerX.value = 0.5;
  centerY.value = 0.5;
  zoom.value = 1;
  filter.value = Filter.None;
  filterValue.value = 1;
  paPos.value = PAPosition.BR;
  paScale.value = 0.2;
  paMargin.value = 0.05;
  asyncDraw();
}

function newImage() {
  srcLoaded.value = false;
  reset();
}

function download() {
  const link = document.createElement("a");
  link.download = `coverify-${new Date().getTime().toString()}.png`;
  if (targetSize.value !== PREVIEW_SIZE) {
    draw(targetSize.value);
  }
  link.href = canvas.value?.toDataURL() ?? "#";
  link.click();
}

const drawTimeout = ref<number | undefined>(undefined);

function asyncDraw() {
  clearTimeout(drawTimeout.value);
  drawTimeout.value = setTimeout(draw);
}

function draw(size = PREVIEW_SIZE) {
  if (!canvas.value) {
    return;
  }
  const ctx = canvas.value.getContext("2d");
  if (!ctx || !image.value) {
    return;
  }
  canvas.value.width = size;
  canvas.value.height = size;
  ctx.clearRect(0, 0, size, size);

  const imgRatio = image.value.height / image.value.width;
  const widthFirst = image.value.width < image.value.height;
  const imgWidth = size * (widthFirst ? 1 : 1 / imgRatio) * zoom.value;
  const imgHeight = size * (widthFirst ? imgRatio : 1) * zoom.value;
  const dx = (size - imgWidth) * centerX.value;
  const dy = (size - imgHeight) * centerY.value;

  switch (filter.value) {
    case Filter.Gray:
      ctx.filter = `grayscale(${(filterValue.value * 100).toFixed(2)}%)`;
      break;
    case Filter.Sepia:
      ctx.filter = `sepia(${(filterValue.value * 100).toFixed(2)}%)`;
      break;
    case Filter.Invert:
      ctx.filter = `invert(${(filterValue.value * 100).toFixed(2)}%)`;
      break;
    case Filter.Blur:
      ctx.filter = `blur(${((filterValue.value * 2 * size) / PREVIEW_SIZE).toFixed(2)}px)`;
      break;
    case Filter.Brightness:
      ctx.filter = `brightness(${(filterValue.value * 100).toFixed(2)}%)`;
      break;
    case Filter.Contrast:
      ctx.filter = `contrast(${(filterValue.value * 100).toFixed(2)}%)`;
      break;
    case Filter.HueShift:
      ctx.filter = `hue-rotate(${(filterValue.value * 360).toFixed(2)}deg)`;
      break;
    case Filter.Saturate:
      ctx.filter = `saturate(${(filterValue.value * 100).toFixed(2)}%)`;
      break;
  }

  ctx.drawImage(image.value, dx, dy, imgWidth, imgHeight);

  ctx.filter = "none";

  if (paPos.value !== PAPosition.None && parentalAdvisory.value) {
    const paWidth = size * paScale.value;
    const paHeight = paWidth * PA_RATIO;
    let padx = paMargin.value * size;
    let pady = paMargin.value * size;
    if (
      paPos.value === PAPosition.BC ||
      paPos.value === PAPosition.BL ||
      paPos.value === PAPosition.BR
    ) {
      pady = size - paHeight - pady;
    }
    if (paPos.value === PAPosition.BR || paPos.value === PAPosition.TR) {
      padx = size - paWidth - padx;
    } else if (paPos.value === PAPosition.BC || paPos.value === PAPosition.TC) {
      padx = (size - paWidth) * 0.5;
    }
    ctx.drawImage(parentalAdvisory.value, padx, pady, paWidth, paHeight);
  }
}

async function updateIcons() {
  await nextTick();
  createIcons({
    icons,
    nameAttr: "icon",
    attrs: {
      width: "1.1em",
      height: "1.1em",
    },
  });
}

onMounted(() => {
  setTimeout(() => {
    visible.value = true;
  });
});

onUpdated(updateIcons);
</script>

<template>
  <main :style="{ display: visible ? 'inherit' : 'none' }">
    <h1>
      <i icon="disc-3"></i>
      Coverify
    </h1>
    <p><i>Create album covers from mundane photos</i></p>
    <img
      ref="image"
      style="display: none"
      :src="srcData ?? undefined"
      @load="imageOnLoad"
    />
    <img
      ref="parentalAdvisory"
      style="display: none"
      src="./parental_advisory.png"
    />
    <input
      v-if="!srcLoaded"
      ref="input"
      type="file"
      accept="image/*"
      @change="openImage"
    />
    <canvas
      v-show="srcLoaded"
      ref="canvas"
      :width="targetSize"
      :height="targetSize"
    ></canvas>
    <br />
    <template v-if="srcLoaded">
      <div class="buttons">
        <button @click="randomize"><i icon="dices"></i> Randomize</button>
        <button @click="reset"><i icon="rotate-ccw"></i> Reset</button>
        <button @click="newImage"><i icon="trash-2"></i> New picture</button>
        <button @click="download">
          <i icon="arrow-down-to-line"></i> Download
        </button>
      </div>
      <br />
      <table class="config">
        <colgroup>
          <col style="width: 25%" />
          <col />
          <col style="width: 10%" />
        </colgroup>
        <tbody>
          <tr>
            <td><label for="center-x">Center X:</label></td>
            <td>
              <input
                id="center-x"
                v-model="centerX"
                type="range"
                min="0"
                max="1"
                step="0.01"
                @input="asyncDraw"
              />
            </td>
            <td>{{ (centerX * 100).toFixed(0) }}%</td>
          </tr>
          <tr>
            <td><label for="center-y">Center Y:</label></td>
            <td>
              <input
                id="center-y"
                v-model="centerY"
                type="range"
                min="0"
                max="1"
                step="0.01"
                @input="asyncDraw"
              />
            </td>
            <td>{{ (centerY * 100).toFixed(0) }}%</td>
          </tr>
          <tr>
            <td><label for="zoom">Zoom:</label></td>
            <td>
              <input
                id="zoom"
                v-model="zoom"
                type="range"
                min="1"
                max="4"
                step="0.01"
                @input="asyncDraw"
              />
            </td>
            <td>{{ (zoom * 100).toFixed(0) }}%</td>
          </tr>
          <tr>
            <td><label for="filter">Filter:</label></td>
            <td>
              <select id="filter" v-model="filter" @change="asyncDraw">
                <option
                  v-for="value in Object.values(Filter)"
                  :key="`filter-${value}`"
                >
                  {{ value }}
                </option>
              </select>
            </td>
            <td></td>
          </tr>
          <tr>
            <td><label for="filter-value">Filter Value:</label></td>
            <td>
              <input
                id="filter-value"
                v-model="filterValue"
                type="range"
                min="0"
                max="3"
                step="0.01"
                @input="asyncDraw"
              />
            </td>
            <td>{{ (filterValue * 100).toFixed(0) }}%</td>
          </tr>
          <tr>
            <td><label for="pa-pos">Sticker Position:</label></td>
            <td>
              <select id="pa-pos" v-model="paPos" @change="asyncDraw">
                <option
                  v-for="value in Object.values(PAPosition)"
                  :key="`pa-pos-${value}`"
                >
                  {{ value }}
                </option>
              </select>
            </td>
            <td></td>
          </tr>
          <tr>
            <td><label for="pa-scale">Sticker Scale:</label></td>
            <td>
              <input
                id="pa-scale"
                v-model="paScale"
                type="range"
                min="0.10"
                max="0.30"
                step="0.01"
                @input="asyncDraw"
              />
            </td>
            <td>{{ (paScale * 100).toFixed(0) }}%</td>
          </tr>
          <tr>
            <td><label for="pa-margin">Sticker Margin:</label></td>
            <td>
              <input
                id="pa-margin"
                v-model="paMargin"
                type="range"
                min="0"
                max="0.1"
                step="0.01"
                @input="asyncDraw"
              />
            </td>
            <td>{{ (paMargin * 100).toFixed(0) }}%</td>
          </tr>
          <tr>
            <td><label for="size">Image Size:</label></td>
            <td>
              <select id="size" v-model="targetSize" @change="asyncDraw">
                <option :value="512">512x512</option>
                <option :value="1024">1024x1024</option>
                <option :value="2048">2048x2048</option>
                <option :value="4096">4096x4096</option>
              </select>
            </td>
            <td></td>
          </tr>
        </tbody>
      </table>
    </template>
    <br />
    <hr />
    <small class="footer">
      <i icon="at-sign"></i>&nbsp;
      <a href="https://git.klemek.fr/klemek" target="_blank">Kleπek</a>&nbsp;
      |&nbsp; <i icon="git-branch"></i>&nbsp;
      <a href="https://git.klemek.fr/klemek/coverify" target="_blank"
        >Repository</a
      >&nbsp; |&nbsp; <i icon="copyright"></i>&nbsp;2025
    </small>
  </main>
</template>

<style scoped>
h1 {
  margin-top: 0;
}

.buttons {
  display: flex;
  width: 100%;
}

.buttons > button {
  flex: 1;
}

canvas {
  width: min(40vh, 100%);
  margin: auto;
}

select,
input {
  width: 100%;
}

.button:hover {
  background-color: var(--background-secondary);
}

.footer {
  opacity: 50%;
}
</style>
