// lib/editor/notebook/page_map.dart
//
// The one-notebook-per-PDF logical page model (F6). A notebook is an ordered
// list of LOGICAL pages; each is either a SOURCE page (shows PDF page N, vector
// preserved) or a BLANK page inserted between/after PDF pages (SpeedyNote-style
// binding the user asked for). Ink hosts attach to logical pages, so inserting
// or reordering pages must NOT renumber the PDF underlay — the source index is
// carried on each page.
//
// Pure, immutable value type (every edit returns a NEW PageMap) so the model is
// fully unit-tested; the `notebook_pages` table + the viewport wire it later.

import 'package:flutter/foundation.dart';

enum NotebookPageKind { source, blank }

/// One logical page. [sourcePageIndex] is the 0-based PDF page it renders, or
/// null for a [NotebookPageKind.blank] inserted page.
@immutable
class NotebookPage {
  const NotebookPage.source(this.sourcePageIndex)
      : kind = NotebookPageKind.source;

  const NotebookPage.blank()
      : kind = NotebookPageKind.blank,
        sourcePageIndex = null;

  final NotebookPageKind kind;
  final int? sourcePageIndex;

  bool get isBlank => kind == NotebookPageKind.blank;
  bool get isSource => kind == NotebookPageKind.source;

  @override
  bool operator ==(Object other) =>
      other is NotebookPage &&
      other.kind == kind &&
      other.sourcePageIndex == sourcePageIndex;

  @override
  int get hashCode => Object.hash(kind, sourcePageIndex);

  @override
  String toString() =>
      isBlank ? 'NotebookPage.blank' : 'NotebookPage.source($sourcePageIndex)';
}

/// Ordered logical page list with copy-on-write edits.
@immutable
class PageMap {
  const PageMap(this.pages);

  /// Identity map: one logical SOURCE page per PDF page, in order.
  factory PageMap.fromSource(int sourcePageCount) {
    assert(sourcePageCount >= 0);
    return PageMap(
      List<NotebookPage>.unmodifiable(
        List<NotebookPage>.generate(
          sourcePageCount,
          (i) => NotebookPage.source(i),
        ),
      ),
    );
  }

  final List<NotebookPage> pages;

  int get length => pages.length;
  bool get isEmpty => pages.isEmpty;

  NotebookPage operator [](int index) => pages[index];

  /// Source PDF page rendered at logical [index], or null when it's a blank.
  int? sourceIndexAt(int index) => pages[index].sourcePageIndex;

  int get blankCount => pages.where((p) => p.isBlank).length;
  int get sourceCount => pages.where((p) => p.isSource).length;

  /// Insert a blank page at logical [index] (0..length). Throws [RangeError]
  /// for an out-of-range index.
  PageMap insertBlankAt(int index) {
    RangeError.checkValueInInterval(index, 0, length, 'index');
    final next = List<NotebookPage>.of(pages)
      ..insert(index, const NotebookPage.blank());
    return PageMap(List<NotebookPage>.unmodifiable(next));
  }

  /// Insert a blank page immediately after logical [index] (-1 prepends).
  PageMap insertBlankAfter(int index) {
    RangeError.checkValueInInterval(index, -1, length - 1, 'index');
    return insertBlankAt(index + 1);
  }

  /// Remove the logical page at [index].
  PageMap removeAt(int index) {
    RangeError.checkValidIndex(index, pages, 'index');
    final next = List<NotebookPage>.of(pages)..removeAt(index);
    return PageMap(List<NotebookPage>.unmodifiable(next));
  }

  /// Move the page at [from] to position [to] (drag-reorder).
  PageMap move(int from, int to) {
    RangeError.checkValidIndex(from, pages, 'from');
    RangeError.checkValueInInterval(to, 0, length - 1, 'to');
    if (from == to) return this;
    final next = List<NotebookPage>.of(pages);
    final page = next.removeAt(from);
    next.insert(to, page);
    return PageMap(List<NotebookPage>.unmodifiable(next));
  }

  @override
  bool operator ==(Object other) =>
      other is PageMap && listEquals(other.pages, pages);

  @override
  int get hashCode => Object.hashAll(pages);

  @override
  String toString() => 'PageMap($pages)';
}
