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

type Column = number[];

interface Props {
  items: unknown[];
  columnWidth?: number;
  gap?: number;
  rtl?: boolean;
  ssrColumns?: number;
  scrollContainer?: HTMLElement | null;
}

const props = withDefaults(defineProps<Props>(), {
  columnWidth: 400,
  gap: 0,
  rtl: false,
  ssrColumns: 0,
  scrollContainer: null,
});

const emit = defineEmits<{
  (event: "redraw"): void;
  (event: "redrawSkip"): void;
}>();

const { columnWidth, items, gap, rtl, ssrColumns, scrollContainer } =
  toRefs(props);
const columns = ref<Column[]>([]);
const wall = ref<HTMLDivElement>() as Ref<HTMLDivElement>;

function columnCount(): number {
  const count = Math.floor(
    (wall.value.getBoundingClientRect().width + gap.value) /
      (columnWidth.value + gap.value)
  );
  return count > 0 ? count : 1;
}

function createColumns(count: number): Column[] {
  return [...new Array(count)].map(() => []);
}

if (ssrColumns.value > 0) {
  const newColumns = createColumns(ssrColumns.value);
  items.value.forEach((_: unknown, i: number) =>
    newColumns[i % ssrColumns.value]!.push(i)
  );
  columns.value = newColumns;
}

async function fillColumns(itemIndex: number) {
  if (itemIndex >= items.value.length) {
    return;
  }
  await nextTick();
  const columnDivs = [...wall.value.children] as HTMLDivElement[];
  if (rtl.value) {
    columnDivs.reverse();
  }
  const target = columnDivs.reduce((prev, curr) =>
    curr.getBoundingClientRect().height < prev.getBoundingClientRect().height
      ? curr
      : prev
  );
  columns.value[+target.dataset.index!]!.push(itemIndex);
  await fillColumns(itemIndex + 1);
}

async function redraw(force = false) {
  if (columns.value.length === columnCount() && !force) {
    emit("redrawSkip");
    return;
  }
  columns.value = createColumns(columnCount());
  const scrollTarget = scrollContainer?.value;
  const scrollY = scrollTarget ? scrollTarget.scrollTop : window.scrollY;
  await fillColumns(0);
  scrollTarget
    ? scrollTarget.scrollBy({ top: scrollY - scrollTarget.scrollTop })
    : window.scrollTo({ top: scrollY });
  emit("redraw");
}

const resizeObserver =
  typeof ResizeObserver === "undefined"
    ? undefined
    : new ResizeObserver(() => redraw());

onMounted(() => {
  redraw();
  resizeObserver?.observe(wall.value);
});
onBeforeUnmount(() => resizeObserver?.unobserve(wall.value));

watch([items, rtl], () => redraw(true));
watch([columnWidth, gap], () => redraw());
</script>

<template>
  <div
    ref="wall"
    class="masonry-wall"
    :style="{ display: 'flex', gap: `${gap}px` }"
  >
    <div
      v-for="(column, columnIndex) in columns"
      :key="columnIndex"
      class="masonry-column"
      :data-index="columnIndex"
      :style="{
        display: 'flex',
        'flex-basis': '0px',
        'flex-direction': 'column',
        'flex-grow': 1,
        gap: `${gap}px`,
        height: ['-webkit-max-content', '-moz-max-content', 'max-content'] as any,
        'min-width': 0
      }"
    >
      <div v-for="itemIndex in column" :key="itemIndex" class="masonry-item">
        <slot :item="items[itemIndex]" :index="itemIndex">
          {{ items[itemIndex] }}
        </slot>
      </div>
    </div>
  </div>
</template>
