import { Controller } from "@hotwired/stimulus"
import { createElement } from "helpers/html_helpers"
import { delay, nextEvent } from "helpers/timing_helpers"
import { keepingScrollPosition } from "helpers/scroll_helpers"
import { get } from "@rails/request.js"

const DELAY_BEFORE_OBSERVING = 400

export default class extends Controller {
  static targets = [ "paginationLink" ]
  static values = {
    paginateOnIntersection: { type: Boolean, default: false },
    discardFrame: Boolean,
    manualActivation: Boolean
  }

  initialize() {
    if (!this.manualActivation) {
      this.activate()
    }
  }

  disconnect() {
    this.observer?.disconnect()
  }

  async activate() {
    await delay(DELAY_BEFORE_OBSERVING)

    if (this.paginateOnIntersectionValue) {
      this.observer = new IntersectionObserver(this.#intersect, { rootMargin: "300px", threshold: 1 })
    }
  }

  async paginationLinkTargetConnected(linkElement) {
    if (this.paginateOnIntersectionValue) {
      await delay(DELAY_BEFORE_OBSERVING)
      this.observer?.observe(linkElement)
    }
  }

  // Actions

  loadPage({ target }) {
    this.#loadPaginationLink(target)
  }

  // Private

  #intersect = ([ entry ]) => {
    if (entry?.isIntersecting && entry.intersectionRatio === 1) {
      this.#loadPaginationLink(entry.target)
    }
  }

  #loadPaginationLink(linkElement) {
    this.observer?.unobserve(linkElement)

    keepingScrollPosition(this.#closestSiblingTo(linkElement) || linkElement.parentNode, this.#expandPaginationLink(linkElement))
  }

  #closestSiblingTo(element) {
    return element.nextElementSibling || element.previousElementSibling
  }

  async #expandPaginationLink(linkElement) {
    linkElement.setAttribute("aria-busy", "true")

    if (this.discardFrameValue) {
      await this.#replacePaginationLinkWithFrameContents(linkElement)
    } else {
      await this.#replacePaginationLinkWithFrame(linkElement)
    }

    linkElement.removeAttribute("aria-busy")
  }

  async #replacePaginationLinkWithFrameContents(linkElement) {
    linkElement.outerHTML = await this.#loadHtmlFrom(linkElement)
  }

  async #loadHtmlFrom(linkElement) {
    const response = await get(linkElement.href, { responseKind: "html" })
    const html = await response.text
    const doc = new DOMParser().parseFromString(html, "text/html")
    const element = doc.querySelector(`turbo-frame#${linkElement.dataset.frame}`)
    return element ? element.innerHTML.trim() : ""
  }

  #replacePaginationLinkWithFrame(linkElement) {
    const turboFrame = this.#buildTurboFrameFor(linkElement)
    this.#insertTurboFrameAtPosition(linkElement, turboFrame)
  }

  #buildTurboFrameFor(linkElement) {
    const turboFrame = createElement("turbo-frame", {
      id: linkElement.dataset.frame,
      src: linkElement.href,
      refresh: "morph",
      target: "_top"
    })

    this.#keepScrollPositionOnFrameRender(turboFrame, linkElement)

    return turboFrame
  }

  async #keepScrollPositionOnFrameRender(turboFrame, linkElement) {
    await nextEvent(turboFrame, "turbo:before-frame-render")

    keepingScrollPosition(linkElement, nextEvent(turboFrame, "turbo:frame-render"))
  }

  #insertTurboFrameAtPosition(linkElement, turboFrame) {
    const container = linkElement.parentNode.parentNode

    if (linkElement.parentNode.firstElementChild === linkElement) {
      container.prepend(turboFrame)
    } else {
      container.append(turboFrame)
    }
  }
}
