/* eslint-disable @typescript-eslint/no-explicit-any */
import { dedupeMixin } from '@open-wc/dedupe-mixin'
import { html, TemplateResult, render, nothing } from 'lit'
import reactive from '../decorators/reactive.js'

export const renderingValue = Symbol('renderingValue')
export const renderPage = Symbol('renderPage')
export const setUpdatePromise = Symbol('setUpdatePromise')
export const updateResolver = Symbol('updateResolver')
export const hasPendingUpdatePromise = Symbol('hasPendingUpdatePromise')
export const resolveUpdatePromise = Symbol('resolveUpdatePromise')

type Constructor<T = object> = new (...args: any[]) => T

export declare class RenderableMixinInterface {
  /**
   * Determines whether the initial render had run and the `firstRender()`
   * function was called.
   *
   * @default false
   */
  accessor firstRendered: boolean

  /**
   * @type A promise resolved when the render finished.
   */
  get updateComplete(): Promise<boolean>

  /**
   * True when rendering debouncer is running.
   */
  get rendering(): boolean

  /**
   * Helper function to be overridden by child classes. It is called when the view
   * is rendered for the first time.
   */
  firstRender(): void

  /**
   * A function called when the template has been rendered
   */
  updated(): void

  /**
   * This to be used by the child classes to render page template.
   * @returns Application page template
   */
  pageTemplate(): TemplateResult | typeof nothing

  /**
   * The main render function. Sub classes should not override this method.
   * Override `[renderPage]()` instead.
   *
   * The function calls `[renderPage]()` in a micro task so it is safe to call this
   * multiple time in the same event loop.
   */
  render(): void
}

/**
 * Adds methods that helps with asynchronous template rendering.
 *
 * The application page content is rendered into the `#app` container.
 *
 * @mixin
 */
export const RenderableMixin = dedupeMixin(
  <T extends Constructor<any>>(superClass: T): Constructor<RenderableMixinInterface> & T => {
    class RenderableMixinClass extends superClass {
      /**
       * Determines whether the initial render had run and the `firstRender()`
       * function was called.
       *
       * @default false
       */
      @reactive() accessor firstRendered = false;

      [renderingValue] = false;

      [hasPendingUpdatePromise] = false;

      [updateResolver]?: (value?: any) => void

      #updateComplete?: Promise<boolean>

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

      /**
       * True when rendering debouncer is running.
       */
      get rendering(): boolean {
        return this[renderingValue]
      }

      constructor(...args: any[]) {
        super(...args)
        this[setUpdatePromise]()
      }

      /**
       * Helper function to be overridden by child classes. It is called when the view
       * is rendered for the first time.
       */
      firstRender(): void {
        //
      }

      /**
       * A function called when the template has been rendered
       */
      updated(): void {
        //
      }

      /**
       * This to be used by the child classes to render page template.
       * @returns Application page template
       */
      pageTemplate(): TemplateResult | typeof nothing {
        return html``
      }

      /**
       * The main render function. Sub classes should not override this method.
       * Override `[renderPage]()` instead.
       *
       * The function calls `[renderPage]()` in a micro task so it is safe to call this
       * multiple time in the same event loop.
       */
      render(): void {
        if (this.rendering) {
          return
        }
        this[renderingValue] = true
        if (!this[hasPendingUpdatePromise]) {
          this[setUpdatePromise]()
        }
        requestAnimationFrame(() => {
          this[renderingValue] = false
          this[renderPage]()
        })
      }

      [renderPage](): void {
        const root = document.querySelector('#app') as HTMLElement
        if (!root) {
          // eslint-disable-next-line no-console
          console.warn(`The <div id="app"></div> is not in the document.`)
          return
        }
        if (!this.firstRendered) {
          this.firstRendered = true
          // cleanup any pre-existing content.
          Array.from(root.childNodes).forEach((node) => node.parentNode?.removeChild(node))
          setTimeout(() => this.firstRender())
        }
        render(this.pageTemplate(), root, { host: this })
        this[resolveUpdatePromise]()
        this.updated()
      }

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

      [resolveUpdatePromise](): void {
        if (!this[hasPendingUpdatePromise]) {
          return
        }
        this[hasPendingUpdatePromise] = false
        const resolver = this[updateResolver]
        if (resolver) {
          resolver()
        }
      }
    }
    return RenderableMixinClass as Constructor<RenderableMixinInterface> & T
  }
)
