/**
 * @license
 * Copyright 2022 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import {directive, AsyncDirective} from 'lit/async-directive.js';
import {DirectiveParameters, ChildPart} from 'lit/directive.js';
import {
  insertPart,
  setChildPartValue,
  removePart,
} from 'lit/directive-helpers.js';

interface RepeatOptions<T> {
  values: T[];
  mapFn?: (val: T, idx: number) => unknown;
  initialCount: number;
  targetFrameRate?: number;
  startAt?: number;
  // TODO: targetFramerate
}

interface RepeatState<T> {
  values: T[];
  mapFn?: (val: T, idx: number) => unknown;
  startAt: number;
  incrementAmount: number;
  lastRenderedAt: number;
  targetFrameRate: number;
}

class IncrementalRepeat<T> extends AsyncDirective {
  private children: {part: ChildPart; options: RepeatOptions<T>}[] = [];

  private part!: ChildPart;

  private state!: RepeatState<T>;

  render(options: RepeatOptions<T>) {
    const values = options.values.slice(
      options.startAt ?? 0,
      (options.startAt ?? 0) + options.initialCount
    );
    if (options.mapFn) {
      return values.map(options.mapFn);
    }
    return values;
  }

  override update(part: ChildPart, [options]: DirectiveParameters<this>) {
    if (options.values !== this.state?.values) {
      if (this.nextScheduledFrameWork !== undefined)
        cancelAnimationFrame(this.nextScheduledFrameWork);
      this.part = part;
      this.clearParts();
      this.state = {
        values: options.values,
        mapFn: options.mapFn,
        startAt: options.initialCount,
        incrementAmount: options.initialCount,
        lastRenderedAt: performance.now(),
        targetFrameRate: options.targetFrameRate ?? 30,
      };
      this.nextScheduledFrameWork = requestAnimationFrame(
        this.animationFrameHandler
      );
    } else {
      this.updateParts();
    }
    return this.render(options);
  }

  private appendPart(options: RepeatOptions<T>) {
    const part = insertPart(this.part);
    this.children.push({part, options});
    setChildPartValue(part, this.render(options));
  }

  private clearParts() {
    for (const child of this.children) {
      removePart(child.part);
    }
    this.children = [];
  }

  private updateParts() {
    for (const child of this.children) {
      setChildPartValue(child.part, this.render(child.options));
    }
  }

  private nextScheduledFrameWork: number | undefined;

  private animationFrameHandler = () => {
    const now = performance.now();
    const frameRate = 1000 / (now - this.state.lastRenderedAt);
    if (frameRate < this.state.targetFrameRate) {
      // https://en.wikipedia.org/wiki/Additive_increase/multiplicative_decrease
      this.state.incrementAmount = Math.max(
        1,
        Math.round(this.state.incrementAmount / 2)
      );
    } else {
      this.state.incrementAmount++;
    }
    this.state.lastRenderedAt = now;
    this.appendPart({
      mapFn: this.state.mapFn,
      values: this.state.values,
      initialCount: this.state.incrementAmount,
      startAt: this.state.startAt,
    });

    this.state.startAt += this.state.incrementAmount;
    if (this.state.startAt < this.state.values.length) {
      this.nextScheduledFrameWork = requestAnimationFrame(
        this.animationFrameHandler
      );
    }
  };
}

export const incrementalRepeat = directive(IncrementalRepeat);
