import {ModelEvent} from "@bokehjs/core/bokeh_events"
import type * as p from "@bokehjs/core/properties"
import type {Attrs} from "@bokehjs/core/types"
import {Markup} from "@bokehjs/models/widgets/markup"
import {PanelMarkupView} from "./layout"
import {serializeEvent} from "./event-to-object"

export class DOMEvent extends ModelEvent {
  constructor(readonly node: string, readonly data: any) {
    super()
  }

  protected override get event_values(): Attrs {
    return {model: this.origin, node: this.node, data: this.data}
  }

  static {
    this.prototype.event_name = "dom_event"
  }
}

export function htmlDecode(input: string): string | null {
  const doc = new DOMParser().parseFromString(input, "text/html")
  return doc.documentElement.textContent
}

export function runScripts(node: any): void {
  Array.from(node.querySelectorAll("script")).forEach((oldScript: any) => {
    const newScript = document.createElement("script")
    Array.from(oldScript.attributes)
      .forEach((attr: any) => newScript.setAttribute(attr.name, attr.value))
    newScript.appendChild(document.createTextNode(oldScript.innerHTML))
    if (oldScript.parentNode) {
      oldScript.parentNode.replaceChild(newScript, oldScript)
    }
  })
}

export class HTMLView extends PanelMarkupView {
  declare model: HTML

  _event_listeners: any = {}

  override connect_signals(): void {
    super.connect_signals()

    const {text, visible, events} = this.model.properties
    this.on_change(text, () => {
      const html = this.process_tex()
      this.set_html(html)
    })
    this.on_change(visible, () => {
      if (this.model.visible) {
        this.container.style.visibility = "visible"
      }
    })
    this.on_change(events, () => {
      this._remove_event_listeners()
      this._setup_event_listeners()
    })
  }

  protected rerender() {
    this.render()
    this.invalidate_layout()
  }

  set_html(html: string | null): void {
    if (html !== null) {
      this.container.innerHTML = html
      if (this.model.run_scripts) {
        runScripts(this.container)
      }
      this._setup_event_listeners()
    }
  }

  override render(): void {
    super.render()
    this.container.style.visibility = "hidden"
    this.shadow_el.appendChild(this.container)

    if (this.provider.status == "failed" || this.provider.status == "loaded") {
      this._has_finished = true
    }

    const html = this.process_tex()
    this.watch_stylesheets()
    this.set_html(html)
  }

  override style_redraw(): void {
    if (this.model.visible) {
      this.container.style.visibility = "visible"
    }
  }

  override process_tex(): string {
    const decoded = htmlDecode(this.model.text)
    const text = decoded || this.model.text
    if (this.model.disable_math || !this.contains_tex(text)) {
      return text
    }

    const tex_parts = this.provider.MathJax.find_tex(text)
    const processed_text: string[] = []

    let last_index: number | undefined = 0
    for (const part of tex_parts) {
      processed_text.push(text.slice(last_index, part.start.n))
      processed_text.push(this.provider.MathJax.tex2svg(part.math, {display: part.display}).outerHTML)

      last_index = part.end.n
    }

    if (last_index! < text.length) {
      processed_text.push(text.slice(last_index))
    }

    return processed_text.join("")
  }

  private contains_tex(html: string): boolean {
    if (!this.provider.MathJax) {
      return false
    }

    return this.provider.MathJax.find_tex(html).length > 0
  }

  private _remove_event_listeners(): void {
    for (const node in this._event_listeners) {
      const el: any = document.getElementById(node)
      if (el == null) {
        console.warn(`DOM node '${node}' could not be found. Cannot subscribe to DOM events.`)
        continue
      }
      for (const event_name in this._event_listeners[node]) {
        const event_callback = this._event_listeners[node][event_name]
        el.removeEventListener(event_name, event_callback)
      }
    }
    this._event_listeners = {}
  }

  private _setup_event_listeners(): void {
    for (const node in this.model.events) {
      const el: any = document.getElementById(node)
      if (el == null) {
        console.warn(`DOM node '${node}' could not be found. Cannot subscribe to DOM events.`)
        continue
      }
      for (const event_name of this.model.events[node]) {
        const callback = (event: any) => {
          this.model.trigger_event(new DOMEvent(node, serializeEvent(event)))
        }
        el.addEventListener(event_name, callback)
        if (!(node in this._event_listeners)) {
          this._event_listeners[node] = {}
        }
        this._event_listeners[node][event_name] = callback
      }
    }
  }
}

export namespace HTML {
  export type Attrs = p.AttrsOf<Props>

  export type Props = Markup.Props & {
    events: p.Property<any>
    run_scripts: p.Property<boolean>
  }
}

export interface HTML extends HTML.Attrs {}

export class HTML extends Markup {
  declare properties: HTML.Props

  constructor(attrs?: Partial<HTML.Attrs>) {
    super(attrs)
  }

  static override __module__ = "panel.models.markup"

  static {
    this.prototype.default_view = HTMLView
    this.define<HTML.Props>(({Any, Bool}) => ({
      events: [ Any, {} ],
      run_scripts: [ Bool, true ],
    }))
  }
}
