import {div} from "@bokehjs/core/dom"
import type * as p from "@bokehjs/core/properties"
import {ModelEvent} from "@bokehjs/core/bokeh_events"
import {isArray} from "@bokehjs/core/util/types"
import {LayoutDOM, LayoutDOMView} from "@bokehjs/models/layouts/layout_dom"
import type {Attrs} from "@bokehjs/core/types"

import {set_size} from "./layout"

import {debounce} from  "debounce"

export class VegaEvent extends ModelEvent {
  constructor(readonly data: any) {
    super()
  }

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

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

export class VegaPlotView extends LayoutDOMView {
  declare model: VegaPlot

  vega_view: any
  container: HTMLDivElement
  _callbacks: string[]
  _connected: string[]
  _replot: any
  _resize: any
  _rendered: boolean = false

  override connect_signals(): void {
    super.connect_signals()
    const {data, show_actions, theme, data_sources, events} = this.model.properties
    this._replot = debounce(() => this._plot(), 20)
    this.on_change([data, show_actions, theme], () => {
      this._replot()
    })
    this.on_change(data_sources, () => this._connect_sources())
    this.on_change(events, () => {
      for (const event of this.model.events) {
        if (this._callbacks.indexOf(event) > -1) {
          continue
        }
        this._callbacks.push(event)
        const callback = (name: string, value: any) => this._dispatch_event(name, value)
        const timeout = this.model.throttle[event] || 20
        this.vega_view.addSignalListener(event, debounce(callback, timeout, false))
      }
    })
    this._connected = []
    this._connect_sources()
  }

  _connect_sources(): void {
    for (const ds in this.model.data_sources) {
      const cds = this.model.data_sources[ds]
      if (this._connected.indexOf(ds) < 0) {
        this.connect(cds.properties.data.change, () => this._replot())
        this._connected.push(ds)
      }
    }
  }

  override remove(): void {
    if (this.vega_view) {
      this.vega_view.finalize()
    }
    super.remove()
  }

  _dispatch_event(name: string, value: any): void {
    if ("vlPoint" in value && value.vlPoint.or != null) {
      const indexes = []
      for (const index of value.vlPoint.or) {
        if (index._vgsid_ !== undefined) {  // If "_vgsid_" property exists
          indexes.push(index._vgsid_)
        } else {  // If "_vgsid_" property doesn't exist
          // Iterate through all properties in the "index" object
          for (const key in index) {
            if (index.hasOwnProperty(key)) {  // To ensure key comes from "index" object itself, not its prototype
              indexes.push({[key]: index[key]})  // Push a new object with this key-value pair into the array
            }
          }
        }
      }
      value = indexes
    }
    this.model.trigger_event(new VegaEvent({type: name, value}))
  }

  _fetch_datasets() {
    const datasets: any = {}
    for (const ds in this.model.data_sources) {
      const cds = this.model.data_sources[ds]
      const data: any = []
      const columns = cds.columns()
      for (let i = 0; i < cds.get_length(); i++) {
        const item: any = {}
        for (const column of columns) {
          item[column] = cds.data[column][i]
        }
        data.push(item)
      }
      datasets[ds] = data
    }
    return datasets
  }

  get child_models(): LayoutDOM[] {
    return []
  }

  override render(): void {
    super.render()
    this._rendered = false
    this.container = div()
    set_size(this.container, this.model)
    this._callbacks = []
    this._plot()
    this.shadow_el.append(this.container)
  }

  _plot(): void {
    const data = this.model.data
    if ((data == null) || !(window as any).vegaEmbed) {
      return
    }
    if (this.model.data_sources && (Object.keys(this.model.data_sources).length > 0)) {
      const datasets = this._fetch_datasets()
      if ("data" in datasets) {
        data.data.values = datasets.data
        delete datasets.data
      }
      if (data.data != null) {
        const data_objs = isArray(data.data) ? data.data : [data.data]
        for (const d of data_objs) {
          if (d.name in datasets) {
            d.values = datasets[d.name]
            delete datasets[d.name]
          }
        }
      }
      this.model.data.datasets = datasets
    }
    const config: any = {actions: this.model.show_actions, theme: this.model.theme};

    (window as any).vegaEmbed(this.container, this.model.data, config).then((result: any) => {
      this.vega_view = result.view
      this._resize = debounce(() => this.resize_view(result.view), 50)
      const callback = (name: string, value: any) => this._dispatch_event(name, value)
      for (const event of this.model.events) {
        this._callbacks.push(event)
        const timeout = this.model.throttle[event] || 20
        this.vega_view.addSignalListener(event, debounce(callback, timeout, false))
      }
    })
  }

  override after_layout(): void {
    super.after_layout()
    if (this.vega_view != null) {
      this._resize()
    }
  }

  resize_view(view: any): void {
    const canvas = view._renderer.canvas()
    if (!this._rendered && canvas !== null) {
      for (const listener of view._eventListeners) {
        if (listener.type === "resize") {
          listener.handler(new Event("resize"))
        }
      }
      this._rendered = true
    }
  }
}

export namespace VegaPlot {
  export type Attrs = p.AttrsOf<Props>
  export type Props = LayoutDOM.Props & {
    data: p.Property<any>
    data_sources: p.Property<any>
    events: p.Property<string[]>
    show_actions: p.Property<boolean>
    theme: p.Property<string | null>
    throttle: p.Property<any>
  }
}

export interface VegaPlot extends VegaPlot.Attrs {}

export class VegaPlot extends LayoutDOM {
  declare properties: VegaPlot.Props

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

  static override __module__ = "panel.models.vega"

  static {
    this.prototype.default_view = VegaPlotView

    this.define<VegaPlot.Props>(({Any, List, Bool, Nullable, Str}) => ({
      data:         [ Any,                {} ],
      data_sources: [ Any,                {} ],
      events:       [ List(Str),      [] ],
      show_actions: [ Bool,         false ],
      theme:        [ Nullable(Str), null ],
      throttle:     [ Any,                {} ],
    }))
  }
}
