import { render, type RootPart, type TemplateResult, type nothing } from 'lit'

export enum RenderingState {
  Idle,
  Rendering,
}

/**
 * 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 or activity will be rendered.
   *
   * @param renderRoot - The root element or a CSS selector string for the element.
   * If a string is provided, it will be queried from the document.
   */
  setRenderRoot(renderRoot: HTMLElement | string): void {
    if (typeof renderRoot === 'string') {
      this.renderRoot = document.querySelector<HTMLElement>(renderRoot)
    } else {
      this.renderRoot = renderRoot
    }
  }

  /**
   * Requests an update of the UI. Schedules a render if one is not already pending.
   *
   * This will call the `renderer()` method and update the DOM in the next animation frame.
   * Ensures only one render is scheduled at a time.
   */
  requestUpdate(): void {
    if (this.#renderingState !== RenderingState.Idle) {
      // An update is already scheduled.
      return
    }
    this.#renderingState = RenderingState.Rendering
    if (!this.#hasPendingUpdatePromise) {
      this.#setUpdatePromise()
    }
    requestAnimationFrame(() => {
      this.renderer()
      this.#renderingState = RenderingState.Idle
    })
  }

  protected firstRenderedFlag = false

  /**
   * Indicates whether the initial render has occurred and `onFirstRender()` was called.
   */
  get firstRendered(): boolean {
    return this.firstRenderedFlag
  }

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

  /**
   * Returns the current rendering state.
   *
   * @returns The current rendering state.
   */
  get state(): RenderingState {
    return this.#renderingState
  }

  /**
   * 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

  /**
   * Abstract method that must be implemented by subclasses to perform the actual rendering logic.
   *
   * Called by `requestUpdate()` when a render is scheduled.
   */
  protected abstract renderer(): void

  /**
   * Sets up the promise that will be resolved when the next render completes.
   * Used for tracking asynchronous rendering.
   */
  #setUpdatePromise(): void {
    this.#updateComplete = new Promise<void>((resolve) => {
      this.#updateResolver = resolve
      this.#hasPendingUpdatePromise = true
    })
  }

  /**
   * Resolves the update promise if one is pending, indicating that rendering has finished.
   */
  protected resolveUpdatePromise(): void {
    if (!this.#hasPendingUpdatePromise) {
      return
    }
    this.#hasPendingUpdatePromise = false
    const resolver = this.#updateResolver
    if (resolver) {
      resolver()
    }
  }

  /**
   * Renders the given Lit template into the specified root element, setting the host context.
   *
   * @param content - The Lit template or `nothing` to render.
   * @param root - The root HTMLElement to render into.
   * @param host - The host object for Lit's context (usually the Application or Activity).
   */
  protected _render(content: TemplateResult | typeof nothing, root: HTMLElement, host: object): void {
    const litPart = root as HTMLElement & { _$litPart$?: RootPart }
    if (litPart._$litPart$ && litPart._$litPart$.options) {
      litPart._$litPart$.options.host = host
    }
    render(content, root, { host })
    this.resolveUpdatePromise()
  }

  /**
   * Removes all child nodes from the given root element.
   *
   * Uses `replaceChildren()` for efficient DOM cleanup.
   * @param root - The root HTMLElement to clear.
   */
  protected _clearRoot(root: HTMLElement): void {
    // Using replaceChildren() is a modern and clean way to clear a node's children.
    root.replaceChildren()
  }
}
