export enum RenderingState {
  Idle,
  Rendering,
}

export const renderFn = Symbol('renderFn')

/**
 * A class that manages rendering of the application screen.
 */
export abstract class Renderer {
  /**
   * The root element where the activity or fragment will be rendered.
   */
  renderRoot: HTMLElement | null = null

  /**
   * @type A promise resolved when the render finished.
   */
  get updateComplete(): Promise<void> | undefined {
    return this.#updateComplete
  }

  /**
   * Sets the root element where the application will be rendered.
   * @param renderRoot The root element or its selector.
   */
  setRenderRoot(renderRoot: HTMLElement | string): void {
    if (typeof renderRoot === 'string') {
      this.renderRoot = document.querySelector<HTMLElement>(renderRoot)
    } else {
      this.renderRoot = renderRoot
    }
  }

  /**
   * Requests an update of the application UI. This will call the `render()` method
   * and update the DOM.
   */
  requestUpdate(): void {
    if (this.#renderingState !== RenderingState.Idle) {
      // An update is already scheduled.
      return
    }
    this.#renderingState = RenderingState.Rendering
    if (!this.#hasPendingUpdatePromise) {
      this.#setUpdatePromise()
    }
    requestAnimationFrame(() => {
      this[renderFn]()
      this.#renderingState = RenderingState.Idle
    })
  }

  protected firstRenderedFlag = false

  /**
   * Determines whether the initial render had run and the `onFirstRender()`
   * function was called.
   */
  get firstRendered(): boolean {
    return this.firstRenderedFlag
  }

  /**
   * A flag used to determine in which rendering state the UI is.
   */
  #renderingState = RenderingState.Idle

  /**
   * A flag that helps to determine whether the `updateComplete` is setup.
   */
  #hasPendingUpdatePromise = false

  /**
   * A hidden value for the `updateComplete` getter.
   */
  #updateComplete?: Promise<void>

  /**
   * The resolver to call when the update completes.
   */
  #updateResolver?: () => void;

  /**
   * Manages the rendering of the UI.
   */
  abstract [renderFn](): void

  #setUpdatePromise(): void {
    this.#updateComplete = new Promise<void>((resolve) => {
      this.#updateResolver = resolve
      this.#hasPendingUpdatePromise = true
    })
  }

  protected resolveUpdatePromise(): void {
    if (!this.#hasPendingUpdatePromise) {
      return
    }
    this.#hasPendingUpdatePromise = false
    const resolver = this.#updateResolver
    if (resolver) {
      resolver()
    }
  }
}
