// lib/editor/board/board.dart
//
// Infinite-board model (F7 — 便利贴 + 双链). A board is a set of positioned
// cards (sticky notes) in board content coordinates; each card has text that may
// contain [[links]] to other cards, so a board derives a LinkGraph for
// backlinks. Cards also host ink (via a StrokeHost keyed by the card id) — the
// same host-agnostic engine as PDF pages.
//
// Pure, immutable value model (no widgets/storage); fully unit-tested. The board
// canvas + persistence wrap it.

import 'dart:ui' show Offset, Rect, Size;

import 'package:flutter/foundation.dart';

import '../link/link_graph.dart';

/// One sticky-note card on the board.
@immutable
class BoardCard {
  const BoardCard({
    required this.id,
    required this.position,
    required this.size,
    this.text = '',
  });

  final String id;

  /// Top-left in board content coordinates.
  final Offset position;
  final Size size;

  /// Card body; may contain `[[other-card]]` links.
  final String text;

  Rect get bounds => position & size;

  BoardCard copyWith({Offset? position, Size? size, String? text}) => BoardCard(
        id: id,
        position: position ?? this.position,
        size: size ?? this.size,
        text: text ?? this.text,
      );

  @override
  bool operator ==(Object other) =>
      other is BoardCard &&
      other.id == id &&
      other.position == position &&
      other.size == size &&
      other.text == text;

  @override
  int get hashCode => Object.hash(id, position, size, text);
}

/// An immutable infinite board: an ordered list of cards with copy-on-write
/// edits. Card ids are unique.
@immutable
class Board {
  Board(List<BoardCard> cards) : cards = List<BoardCard>.unmodifiable(cards);

  static final Board empty = Board(const []);

  final List<BoardCard> cards;

  int get length => cards.length;

  BoardCard? cardById(String id) {
    for (final c in cards) {
      if (c.id == id) return c;
    }
    return null;
  }

  /// Add a card (throws if [card].id already exists).
  Board add(BoardCard card) {
    if (cardById(card.id) != null) {
      throw ArgumentError('duplicate card id: ${card.id}');
    }
    return Board([...cards, card]);
  }

  Board removeById(String id) =>
      Board([for (final c in cards) if (c.id != id) c]);

  /// Replace card [id] via [update]; no-op if absent.
  Board updateCard(String id, BoardCard Function(BoardCard) update) =>
      Board([for (final c in cards) if (c.id == id) update(c) else c]);

  Board moveCard(String id, Offset position) =>
      updateCard(id, (c) => c.copyWith(position: position));

  Board setText(String id, String text) =>
      updateCard(id, (c) => c.copyWith(text: text));

  /// Cards whose bounds overlap [viewport] (board broad-phase culling).
  List<BoardCard> cardsIn(Rect viewport) =>
      [for (final c in cards) if (c.bounds.overlaps(viewport)) c];

  /// Derive the 双链 graph from card texts (card id → its [[links]]).
  LinkGraph linkGraph() =>
      LinkGraph.fromTexts({for (final c in cards) c.id: c.text});

  /// Backlinks to card [id] (ids of cards whose text links to it).
  Set<String> backlinksOf(String id) => linkGraph().backlinksOf(id);

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

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