import type { View } from "../lib/model";

export type FrameEntry = {
  view: View;
  instance: number;
  keepAlive: boolean;
  lastUsed: number;
};

export type FrameCacheOptions = {
  /**
   * Maximum number of keep-alive documents, including the active document.
   *
   * A non-keep-alive active document may temporarily exist in addition to this
   * limit. The active document is never evicted; inactive entries are removed
   * from least to most recently used.
   */
  maxKeepAliveEntries: number;
  shouldKeepAlive: (view: View) => boolean;
};

export type FrameTransition = {
  entries: FrameEntry[];
  removed: FrameEntry[];
  activated: FrameEntry;
};

/**
 * Owns the bounded set of iframe document identities.
 *
 * Entries retain object identity between snapshots so Solid's `<For>` keeps
 * the corresponding iframe DOM node alive. `instance` is never reused, which
 * makes a recreated document observable in its URL and prevents stale browser
 * history or service-worker responses from masquerading as a retained frame.
 */
export class FrameCache {
  readonly #options: FrameCacheOptions;
  readonly #entries = new Map<View, FrameEntry>();
  #active: View;
  #nextInstance = 1;
  #clock = 0;

  constructor(initialView: View, options: FrameCacheOptions) {
    if (!Number.isInteger(options.maxKeepAliveEntries) || options.maxKeepAliveEntries < 0) {
      throw new RangeError("maxKeepAliveEntries must be a non-negative integer");
    }
    this.#options = options;
    this.#active = initialView;
    this.#entries.set(initialView, this.#create(initialView));
  }

  get active(): View {
    return this.#active;
  }

  snapshot(): FrameEntry[] {
    return [...this.#entries.values()];
  }

  activate(view: View): FrameTransition {
    const removed: FrameEntry[] = [];
    const previous = this.#entries.get(this.#active);

    if (previous && previous.view !== view && !previous.keepAlive) {
      this.#entries.delete(previous.view);
      removed.push(previous);
    }

    let activated = this.#entries.get(view);
    if (!activated) {
      activated = this.#create(view);
      this.#entries.set(view, activated);
    }
    activated.lastUsed = ++this.#clock;
    this.#active = view;

    while (this.#keepAliveCount() > this.#options.maxKeepAliveEntries) {
      const candidate = this.#oldestInactiveKeepAlive();
      if (!candidate) break;
      this.#entries.delete(candidate.view);
      removed.push(candidate);
    }

    return {
      entries: this.snapshot(),
      removed,
      activated,
    };
  }

  #create(view: View): FrameEntry {
    return {
      view,
      instance: this.#nextInstance++,
      keepAlive: this.#options.shouldKeepAlive(view),
      lastUsed: ++this.#clock,
    };
  }

  #keepAliveCount(): number {
    let count = 0;
    for (const entry of this.#entries.values()) {
      if (entry.keepAlive) count += 1;
    }
    return count;
  }

  #oldestInactiveKeepAlive(): FrameEntry | undefined {
    let oldest: FrameEntry | undefined;
    for (const entry of this.#entries.values()) {
      if (!entry.keepAlive || entry.view === this.#active) continue;
      if (!oldest || entry.lastUsed < oldest.lastUsed) oldest = entry;
    }
    return oldest;
  }
}
