const KEY = 'backLayer';
const SETTLE_MS = 400;

export interface HistoryLike {
  state: unknown;
  pushState(state: unknown, unused: string, url?: string): void;
  replaceState(state: unknown, unused: string, url?: string): void;
  back(): void;
}

interface Layer {
  id: string;
  close: (id: string) => void;
}

function layerOf(state: unknown): string | undefined {
  if (state && typeof state === 'object' && KEY in state) return String((state as Record<string, unknown>)[KEY]);
  return undefined;
}

function plain(state: unknown): Record<string, unknown> {
  const { [KEY]: _drop, ...rest } = (state && typeof state === 'object' ? state : {}) as Record<string, unknown>;
  return rest;
}

export class BackStack {
  private stack: Layer[] = [];
  private queue: Array<() => void> = [];
  private waiting = false;
  private timer: ReturnType<typeof setTimeout> | null = null;

  constructor(private readonly history: HistoryLike) {}

  /** A reload can land on a layer entry whose layer no longer exists. */
  init(): void {
    if (layerOf(this.history.state) !== undefined) this.history.replaceState(plain(this.history.state), '');
  }

  get depth(): number {
    return this.stack.length;
  }

  sync(id: string, open: boolean, close: (id: string) => void): void {
    const index = this.stack.findIndex((l) => l.id === id);
    if (open && index < 0) {
      this.stack.push({ id, close });
      this.run(() => this.history.pushState({ ...plain(this.history.state), [KEY]: id }, ''));
    } else if (open) {
      this.stack[index].close = close;
    } else if (index >= 0) {
      const top = index === this.stack.length - 1;
      this.stack.splice(index, 1);
      if (top) this.run(() => this.rewind());
    }
  }

  /** Records a route, replacing the layer entry on top when layers are being dismissed by the navigation. */
  route(state: unknown, url: string): void {
    this.run(() => {
      if (this.stack.length > 0 && layerOf(this.history.state) !== undefined) {
        this.stack = [];
        this.history.replaceState(state, '', url);
      } else {
        this.history.pushState(state, '', url);
      }
    });
  }

  /** Returns true when the popstate belonged to the layer stack, false when it is a plain route change. */
  pop(state: unknown): boolean {
    if (this.waiting) {
      this.settle();
      return true;
    }
    const id = layerOf(state);
    if (id === undefined) {
      if (this.stack.length === 0) return false;
      this.closeAbove(-1);
      return true;
    }
    const index = this.stack.findIndex((l) => l.id === id);
    if (index < 0) {
      this.history.back();
      return true;
    }
    this.closeAbove(index);
    return true;
  }

  private closeAbove(index: number): void {
    const closing = this.stack.splice(index + 1);
    for (const layer of closing.reverse()) layer.close(layer.id);
  }

  private rewind(): void {
    if (layerOf(this.history.state) === undefined) return;
    this.waiting = true;
    this.timer = setTimeout(() => this.settle(), SETTLE_MS);
    this.history.back();
  }

  private settle(): void {
    this.waiting = false;
    if (this.timer !== null) clearTimeout(this.timer);
    this.timer = null;
    const next = this.queue.shift();
    if (next) this.run(next);
  }

  private run(op: () => void): void {
    if (this.waiting) {
      this.queue.push(op);
      return;
    }
    op();
    if (!this.waiting) {
      const next = this.queue.shift();
      if (next) this.run(next);
    }
  }
}

export const backStack = new BackStack(typeof window === 'undefined' ? ({} as HistoryLike) : window.history);

let instances = 0;

export function instanceLayerId(prefix: string): string {
  return `${prefix}-${++instances}`;
}
