<template>
  <div class="fc-masonry" ref="containerEl">
    <div class="fc-masonry__cols">
      <div v-for="(col, ci) in columns" :key="ci" class="fc-masonry__col">
        <button
          v-for="item in col" :key="item.id"
          class="fc-masonry__item"
          :class="{ 'fc-masonry__item--anim': shouldAnimate(item) }"
          type="button"
          @click="$emit('open', item.id)"
        >
          <img
            :src="item.thumbnail_url" :alt="`image ${item.id}`"
            loading="lazy"
            :style="aspectStyle(item)"
          />
        </button>
      </div>
    </div>

    <div v-if="loading" class="fc-masonry__sentinel">
      <v-progress-circular indeterminate color="accent" size="28" />
    </div>
    <div v-else-if="hasMore" ref="sentinelEl" class="fc-masonry__sentinel" />
    <div v-else-if="items.length" class="fc-masonry__end text-caption">
      End.
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { usePolyMasonry } from '../../composables/usePolyMasonry.js'
import { useInfiniteScroll } from '../../composables/useInfiniteScroll.js'

const props = defineProps({
  items: { type: Array, default: () => [] },
  loading: { type: Boolean, default: false },
  hasMore: { type: Boolean, default: false },
  // Items at indices >= animateFromIndex get the stagger fade-in. Opt-in
  // — defaults to Infinity (no animation) so views that don't want it
  // (ArtistView, etc.) don't pay the layout-shift cost. ShowcaseView
  // uses 0 on initial load / shuffle and prevCount on infinite-scroll
  // appends.
  animateFromIndex: { type: Number, default: Number.POSITIVE_INFINITY },
})
const emit = defineEmits(['load-more', 'open'])

const containerEl = ref(null)
const sentinelEl = ref(null)
const { columnCount, distribute } = usePolyMasonry(containerEl)

const columns = computed(() => distribute(props.items, columnCount.value))

// id → index lookup so we can derive the stagger from natural reading
// order even after the masonry distributes items across columns.
const idxById = computed(() => {
  const m = new Map()
  props.items.forEach((it, i) => m.set(it.id, i))
  return m
})

function shouldAnimate(item) {
  const idx = idxById.value.get(item.id)
  return idx !== undefined && idx >= props.animateFromIndex
}

function aspectStyle(item) {
  const w = Number(item.width)
  const h = Number(item.height)
  if (!w || !h) return {}
  return { aspectRatio: `${w} / ${h}` }
}

// Larger rootMargin than the composable default (600px) because the
// sentinel sits at the BOTTOM of the masonry container, whose height is
// the MAX of the column heights. A single tall image (long manga page,
// panorama) in one column pushes the sentinel way past the visible
// bottom of the SHORTER columns — the user reads the short-column
// bottoms long before the sentinel comes into view, and load-more
// fires too late. 2400px ≈ 2-3 screen-heights of pre-emptive trigger,
// comfortably covering typical tall-image heights. Operator-flagged
// 2026-05-30.
useInfiniteScroll(sentinelEl, () => {
  if (props.hasMore && !props.loading) emit('load-more')
}, { rootMargin: '2400px' })
</script>

<style scoped>
.fc-masonry__cols { display: flex; gap: 8px; align-items: flex-start; }
.fc-masonry__col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8px; }
.fc-masonry__item {
  display: block; padding: 0; border: 0; background: none;
  cursor: pointer; width: 100%;
  overflow: hidden; border-radius: 4px;
}
.fc-masonry__item img {
  width: 100%; height: auto; display: block;
  background: rgb(var(--v-theme-surface-light));
  /* IR-parity hover: zoom + brighten the thumbnail (style.css ~1772). */
  transition: transform 0.3s ease, filter 0.3s ease;
}
.fc-masonry__item:hover img {
  transform: scale(1.03);
  filter: brightness(1.1);
}
.fc-masonry__sentinel {
  display: flex; justify-content: center; padding: 32px 0; min-height: 60px;
}
.fc-masonry__end { text-align: center; padding: 32px 0; }

/* Cascade entry: each tile flips up out of a backward tilt and settles
   into place — more pronounced than a plain fade so the showcase reads as an
   "experience" (operator-flagged 2026-05-28). The reveal is paced entirely by
   the store, which pushes one fully-decoded item at a time (showcase.js); each
   tile therefore animates the instant it mounts, with NO per-index CSS delay —
   the old `animation-delay: index×70ms` compounded on top of the insert
   cadence and made the cascade drag and desync as it grew (operator-flagged
   2026-06-04). The `both` fill holds the hidden/tilted 0% state until mount;
   the cubic-bezier overshoots slightly past flat then settles. Honors
   prefers-reduced-motion. Tunables: tilt (-28deg), duration (0.6s). */
@keyframes fc-masonry-item-in {
  0% {
    opacity: 0;
    transform: perspective(1000px) rotateX(-28deg) translateY(26px) scale(0.95);
  }
  55% { opacity: 1; }
  100% {
    opacity: 1;
    transform: perspective(1000px) rotateX(0deg) translateY(0) scale(1);
  }
}
.fc-masonry__item--anim {
  transform-origin: center top;
  backface-visibility: hidden;
  animation: fc-masonry-item-in 0.6s cubic-bezier(0.34, 1.45, 0.64, 1) both;
}
@media (prefers-reduced-motion: reduce) {
  .fc-masonry__item--anim {
    animation: none;
    opacity: 1;
    transform: none;
  }
  .fc-masonry__item img { transition: none; }
  .fc-masonry__item:hover img { transform: none; }
}
</style>
