<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import {
  ChevronLeft as ChevronLeftIcon,
  ChevronRight as ChevronRightIcon,
  Download as DownloadIcon,
  ExternalLink as ExternalLinkIcon,
  Printer as PrinterIcon,
  Search as SearchIcon,
  X as XIcon,
  ZoomIn as ZoomInIcon,
  ZoomOut as ZoomOutIcon,
} from 'lucide-vue-next'
import VuePdfEmbed, { usePdfDocument, usePdfSearch } from 'vue-pdf-embed'
import 'vue-pdf-embed/dist/styles/annotationLayer.css'
import 'vue-pdf-embed/dist/styles/textLayer.css'
import type { PDFDocumentProxy } from 'pdfjs-dist'

interface Props {
  isOpen: boolean
  openUrl: string
  pdfTitle: string
  pdfUrl: string
}

interface PdfEmbedActions {
  download: (filename: string) => Promise<void>
  print: (dpi: number, filename: string, allPages: boolean) => Promise<void>
}

const props = defineProps<Props>()
const emit = defineEmits<{
  close: []
}>()

const closeButton = ref<HTMLButtonElement | null>(null)
const viewerFrame = ref<HTMLDivElement | null>(null)
const pdfEmbed = ref<PdfEmbedActions | null>(null)
const pageCount = ref(0)
const currentPage = ref(1)
const zoomLevel = ref(1)
const viewerWidth = ref(720)
const isLoading = ref(false)
const isRendering = ref(false)
const errorMessage = ref<string | null>(null)
const searchQuery = ref('')
const minimumZoom = 0.75
const maximumZoom = 2
const zoomStep = 0.25
const defaultViewerWidth = 720

const pdfSource = computed(() => {
  if (!props.isOpen || props.pdfUrl === '') {
    return null
  }

  return props.pdfUrl
})

const safeTitle = computed(() => {
  return props.pdfTitle.trim() === '' ? 'PDF' : props.pdfTitle
})

const downloadFileName = computed(() => {
  const normalizedTitle = safeTitle.value
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '-')
    .replace(/^-|-$/g, '')

  return normalizedTitle === '' ? 'document.pdf' : `${normalizedTitle}.pdf`
})

const renderedWidth = computed(() => {
  return Math.round(viewerWidth.value * zoomLevel.value)
})

const canGoToPreviousPage = computed(() => {
  return currentPage.value > 1
})

const canGoToNextPage = computed(() => {
  return pageCount.value > 0 && currentPage.value < pageCount.value
})

const pageStatus = computed(() => {
  if (pageCount.value === 0) {
    return 'Page -'
  }

  return `Page ${currentPage.value} of ${pageCount.value}`
})

const searchStatus = computed(() => {
  if (searchQuery.value.trim() === '') {
    return ''
  }

  if (matchCount.value === 0) {
    return 'No matches'
  }

  return `${currentMatch.value} of ${matchCount.value}`
})

const { doc: pdfDocument } = usePdfDocument({
  source: pdfSource,
  onError: handlePdfLoadingError,
})

const {
  clear: clearSearchResults,
  currentMatch,
  currentMatchPage,
  find,
  findController,
  matchCount,
  next: nextSearchResult,
  previous: previousSearchResult,
} = usePdfSearch(pdfDocument)

watch(
  () => [props.isOpen, props.pdfUrl],
  () => {
    if (!props.isOpen) {
      return
    }

    resetViewerState()
    void focusCloseButton()
  },
  { immediate: true },
)

watch(currentMatchPage, (matchedPage) => {
  if (matchedPage < 1 || matchedPage > pageCount.value) {
    return
  }

  currentPage.value = matchedPage
})

watch(searchQuery, (query) => {
  if (query.trim() !== '') {
    return
  }

  clearSearchResults()
})

watch(
  () => [currentPage.value, renderedWidth.value],
  () => {
    if (!props.isOpen || pdfDocument.value === null) {
      return
    }

    isRendering.value = true
  },
)

onMounted(() => {
  window.addEventListener('keydown', handleWindowKeydown)
  window.addEventListener('resize', updateViewerWidth)
  updateViewerWidth()
})

onBeforeUnmount(() => {
  window.removeEventListener('keydown', handleWindowKeydown)
  window.removeEventListener('resize', updateViewerWidth)
})

async function focusCloseButton(): Promise<void> {
  await nextTick()
  closeButton.value?.focus()
  updateViewerWidth()
}

function resetViewerState(): void {
  pdfDocument.value = null
  pageCount.value = 0
  currentPage.value = 1
  zoomLevel.value = 1
  isLoading.value = true
  isRendering.value = false
  errorMessage.value = null
  searchQuery.value = ''
  clearSearchResults()
}

function handlePdfLoaded(loadedDocument: PDFDocumentProxy): void {
  pageCount.value = loadedDocument.numPages
  currentPage.value = 1
  isLoading.value = false
  errorMessage.value = null
}

function handlePdfLoadingError(): void {
  isLoading.value = false
  isRendering.value = false
  pageCount.value = 0
  errorMessage.value = 'Could not load PDF'
}

function handlePdfRendered(): void {
  isRendering.value = false
}

function handlePdfRenderingError(): void {
  isRendering.value = false
  errorMessage.value = 'Could not render PDF'
}

function goToPreviousPage(): void {
  if (!canGoToPreviousPage.value) {
    return
  }

  currentPage.value -= 1
}

function goToNextPage(): void {
  if (!canGoToNextPage.value) {
    return
  }

  currentPage.value += 1
}

function zoomOut(): void {
  zoomLevel.value = Math.max(minimumZoom, zoomLevel.value - zoomStep)
}

function zoomIn(): void {
  zoomLevel.value = Math.min(maximumZoom, zoomLevel.value + zoomStep)
}

function searchPdf(): void {
  const query = searchQuery.value.trim()
  if (query === '') {
    clearSearchResults()
    return
  }

  find(query)
}

async function downloadPdf(): Promise<void> {
  await pdfEmbed.value?.download(downloadFileName.value)
}

async function printPdf(): Promise<void> {
  await pdfEmbed.value?.print(150, downloadFileName.value, true)
}

function closeViewer(): void {
  emit('close')
}

function handleWindowKeydown(keyboardEvent: KeyboardEvent): void {
  if (!props.isOpen || keyboardEvent.key !== 'Escape') {
    return
  }

  closeViewer()
}

function updateViewerWidth(): void {
  const frameWidth = viewerFrame.value?.clientWidth ?? defaultViewerWidth
  const paddedWidth = frameWidth - 32
  viewerWidth.value = Math.max(320, Math.min(defaultViewerWidth, paddedWidth))
}
</script>

<template>
  <Teleport to="body">
    <div
      v-if="isOpen"
      class="pdf-viewer"
      data-cy="pdf-viewer-modal"
      role="dialog"
      aria-modal="true"
      :aria-label="safeTitle"
      @click.self="closeViewer"
    >
      <section class="pdf-viewer__panel">
        <header class="pdf-viewer__header">
          <h2 class="pdf-viewer__title" data-cy="pdf-viewer-title">
            {{ safeTitle }}
          </h2>
          <button
            ref="closeButton"
            type="button"
            class="pdf-viewer__icon-button"
            data-cy="pdf-viewer-close"
            title="Close PDF"
            aria-label="Close PDF"
            @click="closeViewer"
          >
            <XIcon :size="20" aria-hidden="true" />
          </button>
        </header>

        <div class="pdf-viewer__toolbar" aria-label="PDF tools">
          <div class="pdf-viewer__tool-group">
            <button
              type="button"
              class="pdf-viewer__icon-button"
              data-cy="pdf-viewer-previous-page"
              title="Previous page"
              aria-label="Previous page"
              :disabled="!canGoToPreviousPage"
              @click="goToPreviousPage"
            >
              <ChevronLeftIcon :size="18" aria-hidden="true" />
            </button>
            <span
              class="pdf-viewer__page-status"
              data-cy="pdf-viewer-page-status"
            >
              {{ pageStatus }}
            </span>
            <button
              type="button"
              class="pdf-viewer__icon-button"
              data-cy="pdf-viewer-next-page"
              title="Next page"
              aria-label="Next page"
              :disabled="!canGoToNextPage"
              @click="goToNextPage"
            >
              <ChevronRightIcon :size="18" aria-hidden="true" />
            </button>
          </div>

          <div class="pdf-viewer__tool-group">
            <button
              type="button"
              class="pdf-viewer__icon-button"
              data-cy="pdf-viewer-zoom-out"
              title="Zoom out"
              aria-label="Zoom out"
              :disabled="zoomLevel === minimumZoom"
              @click="zoomOut"
            >
              <ZoomOutIcon :size="18" aria-hidden="true" />
            </button>
            <span class="pdf-viewer__zoom-status">
              {{ Math.round(zoomLevel * 100) }}%
            </span>
            <button
              type="button"
              class="pdf-viewer__icon-button"
              data-cy="pdf-viewer-zoom-in"
              title="Zoom in"
              aria-label="Zoom in"
              :disabled="zoomLevel === maximumZoom"
              @click="zoomIn"
            >
              <ZoomInIcon :size="18" aria-hidden="true" />
            </button>
          </div>

          <form class="pdf-viewer__search" @submit.prevent="searchPdf">
            <input
              v-model="searchQuery"
              class="pdf-viewer__search-input"
              data-cy="pdf-viewer-search-input"
              type="search"
              aria-label="Search PDF"
              placeholder="Search"
            />
            <button
              type="submit"
              class="pdf-viewer__icon-button"
              title="Search PDF"
              aria-label="Search PDF"
            >
              <SearchIcon :size="18" aria-hidden="true" />
            </button>
            <button
              type="button"
              class="pdf-viewer__icon-button"
              title="Previous match"
              aria-label="Previous match"
              :disabled="matchCount === 0"
              @click="previousSearchResult"
            >
              <ChevronLeftIcon :size="18" aria-hidden="true" />
            </button>
            <button
              type="button"
              class="pdf-viewer__icon-button"
              title="Next match"
              aria-label="Next match"
              :disabled="matchCount === 0"
              @click="nextSearchResult"
            >
              <ChevronRightIcon :size="18" aria-hidden="true" />
            </button>
            <span class="pdf-viewer__search-status">
              {{ searchStatus }}
            </span>
          </form>

          <div class="pdf-viewer__tool-group">
            <button
              type="button"
              class="pdf-viewer__icon-button"
              data-cy="pdf-viewer-download"
              title="Download PDF"
              aria-label="Download PDF"
              @click="downloadPdf"
            >
              <DownloadIcon :size="18" aria-hidden="true" />
            </button>
            <button
              type="button"
              class="pdf-viewer__icon-button"
              data-cy="pdf-viewer-print"
              title="Print PDF"
              aria-label="Print PDF"
              @click="printPdf"
            >
              <PrinterIcon :size="18" aria-hidden="true" />
            </button>
            <a
              :href="openUrl"
              class="pdf-viewer__icon-link"
              data-cy="pdf-viewer-open-link"
              target="_blank"
              rel="noreferrer"
              title="Open PDF in new tab"
              aria-label="Open PDF in new tab"
            >
              <ExternalLinkIcon :size="18" aria-hidden="true" />
            </a>
          </div>
        </div>

        <div ref="viewerFrame" class="pdf-viewer__frame">
          <p v-if="isLoading" class="pdf-viewer__status">Loading PDF...</p>
          <p
            v-if="errorMessage !== null"
            class="pdf-viewer__status pdf-viewer__status--error"
            data-cy="pdf-viewer-error"
          >
            {{ errorMessage }}
          </p>
          <div
            v-if="errorMessage === null"
            class="pdf-viewer__document"
            data-cy="pdf-viewer-document"
          >
            <VuePdfEmbed
              ref="pdfEmbed"
              annotation-layer
              text-layer
              :find-controller="findController"
              :page="currentPage"
              :source="pdfDocument"
              :width="renderedWidth"
              @loaded="handlePdfLoaded"
              @rendered="handlePdfRendered"
              @rendering-failed="handlePdfRenderingError"
            />
            <p v-if="isRendering" class="pdf-viewer__rendering-status">
              Rendering page...
            </p>
          </div>
        </div>
      </section>
    </div>
  </Teleport>
</template>

<style scoped>
.pdf-viewer {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgb(31 31 31 / 62%);
}

.pdf-viewer__panel {
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr);
  width: min(72rem, 100%);
  height: min(52rem, calc(100vh - 2rem));
  overflow: hidden;
  background: var(--color-white);
  border-radius: 8px;
  box-shadow: 0 24px 80px rgb(31 31 31 / 26%);
}

.pdf-viewer__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.85rem 1rem;
  border-bottom: 1px solid var(--color-border);
}

.pdf-viewer__title {
  margin: 0;
  color: var(--color-slate-dark);
  font-family: var(--font-serif);
  font-size: 1.1rem;
  line-height: 1.25;
}

.pdf-viewer__toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.65rem;
  padding: 0.75rem 1rem;
  border-bottom: 1px solid var(--color-border);
  background: #f8f6ef;
}

.pdf-viewer__tool-group,
.pdf-viewer__search {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

.pdf-viewer__icon-button,
.pdf-viewer__icon-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  color: var(--color-slate);
  background: var(--color-white);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  line-height: 1;
  transition:
    border-color 180ms ease,
    color 180ms ease,
    background-color 180ms ease;
}

.pdf-viewer__icon-button:hover:not(:disabled),
.pdf-viewer__icon-button:focus-visible:not(:disabled),
.pdf-viewer__icon-link:hover,
.pdf-viewer__icon-link:focus-visible {
  color: var(--color-white);
  background: var(--color-slate);
  border-color: var(--color-slate);
}

.pdf-viewer__icon-button:focus-visible,
.pdf-viewer__icon-link:focus-visible,
.pdf-viewer__search-input:focus-visible {
  outline: 3px solid rgb(61 78 93 / 24%);
  outline-offset: 3px;
}

.pdf-viewer__icon-button:disabled {
  cursor: not-allowed;
  opacity: 0.45;
}

.pdf-viewer__page-status,
.pdf-viewer__zoom-status,
.pdf-viewer__search-status {
  min-width: max-content;
  color: var(--color-text-muted);
  font-size: 0.88rem;
  font-weight: 700;
}

.pdf-viewer__search {
  flex: 1 1 18rem;
}

.pdf-viewer__search-input {
  width: min(16rem, 100%);
  min-height: 2.25rem;
  padding: 0.45rem 0.65rem;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  color: var(--color-text);
  background: var(--color-white);
  font: inherit;
}

.pdf-viewer__frame {
  min-height: 0;
  overflow: auto;
  padding: 1rem;
  background: #e9e4d7;
}

.pdf-viewer__status {
  margin: 0 0 1rem;
  color: var(--color-text-muted);
  font-size: 0.95rem;
  font-weight: 700;
  text-align: center;
}

.pdf-viewer__status--error {
  color: #8f2f25;
}

.pdf-viewer__document {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-width: max-content;
}

.pdf-viewer__document :deep(.vue-pdf-embed) {
  display: block;
}

.pdf-viewer__document :deep(.vue-pdf-embed__page) {
  overflow: hidden;
  background: var(--color-white);
  box-shadow: 0 10px 32px rgb(31 31 31 / 16%);
}

.pdf-viewer__rendering-status {
  margin: 0.8rem 0 0;
  color: var(--color-text-muted);
  font-size: 0.85rem;
  font-weight: 700;
}

@media (max-width: 700px) {
  .pdf-viewer {
    padding: 0;
  }

  .pdf-viewer__panel {
    width: 100%;
    height: 100vh;
    border-radius: 0;
  }

  .pdf-viewer__header,
  .pdf-viewer__toolbar,
  .pdf-viewer__frame {
    padding-right: 0.75rem;
    padding-left: 0.75rem;
  }

  .pdf-viewer__search {
    flex-basis: 100%;
  }
}
</style>
