import { h, clear } from '../core/dom';
import { store } from '../core/store';
import { icon } from '../ui/icons';
import { renderThumbnail, type PDFDocumentProxy } from '../core/pdf';
import { uid } from '../core/util';
import { effectiveRotation, type PageState } from '../core/model';

export class PageRail {
  private thumbCache = new Map<number, string>();
  private dragIndex = -1;

  constructor(
    private el: HTMLElement,
    private pdfDoc: PDFDocumentProxy | null,
    private onPagesChanged: () => void,
    private onGoToPage: (pageId: string) => void,
  ) {
    store.on('page', () => this.syncActive());
    store.on('doc', () => this.render());
  }

  setPdfDoc(d: PDFDocumentProxy | null, keepCache = false) {
    this.pdfDoc = d;
    if (!keepCache) this.thumbCache.clear();
  }

  /**
   * Re-render only the thumbnail(s) for one source page after a native content
   * edit changed it — the rest of the rail keeps its cached thumbnails. Without
   * this, every native edit re-rendered *every* page's thumbnail (via pdf.js),
   * which on a large document is the second half of the "edit hangs forever" bug.
   */
  refreshThumb(srcIndex: number) {
    this.thumbCache.delete(srcIndex);
    for (const ps of this.live()) {
      if (ps.srcIndex !== srcIndex) continue;
      const box = this.el.querySelector<HTMLElement>(`.rail-item[data-id="${ps.id}"] .thumb-canvas`);
      if (box) { clear(box); void this.loadThumb(ps, box); }
    }
  }

  private live(): PageState[] {
    return store.doc!.pages.filter((p) => !p.deleted);
  }

  /** Below 820px the rail lays out as a horizontal strip (see editor.css),
   *  so drop position must compare X instead of Y. */
  private isRowMode(): boolean {
    return window.matchMedia('(max-width: 820px)').matches;
  }
  private isAfterDrop(item: HTMLElement, e: DragEvent): boolean {
    const rect = item.getBoundingClientRect();
    return this.isRowMode() ? e.clientX > rect.left + rect.width / 2 : e.clientY > rect.top + rect.height / 2;
  }

  render() {
    clear(this.el);
    this.el.appendChild(h('div.rail-title', 'Pages'));
    const list = h('div.rail-list');
    this.el.appendChild(list);
    const live = this.live();
    live.forEach((ps, i) => list.appendChild(this.thumbItem(ps, i)));
    this.el.appendChild(
      h('button.rail-add', { onclick: () => this.insertBlank(live.length) }, icon('plus', 18), 'Add blank page'),
    );
    this.syncActive();
  }

  private thumbItem(ps: PageState, index: number): HTMLElement {
    const canvasBox = h('div.thumb-canvas');
    const item = h(
      'div.rail-item',
      { dataset: { id: ps.id, index: String(index) }, draggable: 'true' },
      h('div.thumb-num', String(index + 1)),
      canvasBox,
      h(
        'div.thumb-actions',
        actBtn('rotate', 'Rotate', () => this.rotate(ps.id)),
        actBtn('duplicate', 'Duplicate', () => this.duplicate(ps.id, index)),
        actBtn('trash', 'Delete', () => this.remove(ps.id), true),
      ),
    );
    item.addEventListener('click', (e) => {
      if ((e.target as HTMLElement).closest('.thumb-actions')) return;
      this.onGoToPage(ps.id);
    });
    // drag reorder
    item.addEventListener('dragstart', () => { this.dragIndex = index; item.classList.add('dragging'); });
    item.addEventListener('dragend', () => { this.dragIndex = -1; item.classList.remove('dragging'); this.el.querySelectorAll('.drop-before,.drop-after,.drop-left,.drop-right').forEach((n) => n.classList.remove('drop-before', 'drop-after', 'drop-left', 'drop-right')); });
    item.addEventListener('dragover', (e) => {
      e.preventDefault();
      const after = this.isAfterDrop(item, e as DragEvent);
      const row = this.isRowMode();
      item.classList.remove('drop-before', 'drop-after', 'drop-left', 'drop-right');
      item.classList.add(row ? (after ? 'drop-right' : 'drop-left') : (after ? 'drop-after' : 'drop-before'));
    });
    item.addEventListener('dragleave', () => item.classList.remove('drop-before', 'drop-after', 'drop-left', 'drop-right'));
    item.addEventListener('drop', (e) => {
      e.preventDefault();
      const after = this.isAfterDrop(item, e as DragEvent);
      this.reorder(this.dragIndex, after ? index + 1 : index);
    });
    this.loadThumb(ps, canvasBox);
    return item;
  }

  private async loadThumb(ps: PageState, box: HTMLElement) {
    if (ps.blank || !this.pdfDoc) {
      box.appendChild(h('div.thumb-blank'));
      return;
    }
    try {
      if (!this.thumbCache.has(ps.srcIndex)) {
        const page = await this.pdfDoc.getPage(ps.srcIndex + 1);
        this.thumbCache.set(ps.srcIndex, await renderThumbnail(page, 120));
      }
      const img = h('img.thumb-img', { src: this.thumbCache.get(ps.srcIndex)! }) as HTMLImageElement;
      const rot = effectiveRotation(ps);
      if (rot) img.style.transform = `rotate(${rot}deg)`;
      box.appendChild(img);
    } catch {
      box.appendChild(h('div.thumb-blank'));
    }
  }

  private syncActive() {
    const live = this.live();
    const activeId = live[store.currentPage]?.id;
    this.el.querySelectorAll('.rail-item').forEach((el) => {
      el.classList.toggle('active', (el as HTMLElement).dataset.id === activeId);
    });
  }

  // ---- operations ----
  private commitPages(pages: PageState[]) {
    store.checkpoint();
    store.doc!.pages = pages;
    this.render();
    this.onPagesChanged();
  }

  private rotate(id: string) {
    const pages = store.doc!.pages.map((p) => (p.id === id ? { ...p, rotation: (p.rotation + 90) % 360 } : p));
    this.commitPages(pages);
  }
  private remove(id: string) {
    if (this.live().length <= 1) return;
    const pages = store.doc!.pages.filter((p) => p.id !== id);
    // drop annotations on the removed page
    store.doc!.annos = store.doc!.annos.filter((a) => a.page !== id);
    this.commitPages(pages);
  }
  private duplicate(id: string, index: number) {
    const orig = store.doc!.pages.find((p) => p.id === id)!;
    const copy: PageState = { ...orig, id: uid('p_') };
    const live = this.live();
    const newLive = [...live.slice(0, index + 1), copy, ...live.slice(index + 1)];
    this.commitPages(newLive);
  }
  private insertBlank(atLive: number) {
    const ref = this.live()[0];
    const blank: PageState = { id: uid('p_'), srcIndex: -1, width: ref?.width ?? 612, height: ref?.height ?? 792, baseRotation: 0, rotation: 0, blank: true };
    const live = this.live();
    const newLive = [...live.slice(0, atLive), blank, ...live.slice(atLive)];
    this.commitPages(newLive);
  }
  private reorder(from: number, to: number) {
    if (from < 0 || from === to || from === to - 1) return;
    const live = this.live();
    const moved = live[from];
    const without = live.filter((_, i) => i !== from);
    const insertAt = to > from ? to - 1 : to;
    without.splice(insertAt, 0, moved);
    this.commitPages(without);
  }
}

function actBtn(ic: string, title: string, onclick: () => void, danger = false): HTMLElement {
  return h('button.thumb-act' + (danger ? ' danger' : ''), { title, onclick: (e: Event) => { e.stopPropagation(); onclick(); } }, icon(ic, 14));
}
