import {ModelEvent} from "@bokehjs/core/bokeh_events"
import type {StyleSheetLike} from "@bokehjs/core/dom"
import {div} from "@bokehjs/core/dom"
import type * as p from "@bokehjs/core/properties"
import {ColumnDataSource} from "@bokehjs/models/sources/column_data_source"
import {HTMLBox, HTMLBoxView, set_size} from "./layout"
import type {Attrs} from "@bokehjs/core/types"

import perspective_css from "styles/models/perspective.css"

const THEMES: any = {
  "pro-dark": "Pro Dark",
  pro: "Pro Light",
  vaporwave: "Vaporwave",
  solarized: "Solarized",
  "solarized-dark": "Solarized Dark",
  monokai: "Monokai",
}

const PLUGINS: any = {
  datagrid: "Datagrid",
  d3_x_bar: "X Bar",
  d3_y_bar: "Y Bar",
  d3_xy_line: "X/Y Line",
  d3_y_line: "Y Line",
  d3_y_area: "Y Area",
  d3_y_scatter: "Y Scatter",
  d3_xy_scatter: "X/Y Scatter",
  d3_treemap: "Treemap",
  d3_candlestick: "Candlestick",
  d3_sunburst: "Sunburst",
  d3_heatmap: "Heatmap",
  d3_ohlc: "OHLC",
}

function objectFlip(obj: any) {
  const ret: any = {}
  Object.keys(obj).forEach(key => {
    ret[obj[key]] = key
  })
  return ret
}

const PLUGINS_REVERSE = objectFlip(PLUGINS)
const THEMES_REVERSE = objectFlip(THEMES)

export class PerspectiveClickEvent extends ModelEvent {
  constructor(readonly config: any, readonly column_names: string[], readonly row: any[]) {
    super()
  }

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

  static {
    this.prototype.event_name = "perspective-click"
  }
}

export class PerspectiveView extends HTMLBoxView {
  declare model: Perspective

  perspective_element: any
  table: any
  worker: any
  _updating: boolean = false
  _config_listener: any = null
  _current_config: any = null
  _current_plugin: string | null = null
  _loaded: boolean = false
  _plugin_configs: any = new Map()

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

    this.connect(this.model.source.properties.data.change, () => this.setData())
    this.connect(this.model.source.streaming, () => this.stream())
    this.connect(this.model.source.patching, () => this.patch())

    const {
      schema, columns, expressions, split_by, group_by,
      aggregates, filters, sort, plugin, selectable, editable, theme,
      title, settings,
    } = this.model.properties

    const not_updating = (fn: () => void) => {
      return () => {
        if (this._updating) {
          return
        }
        fn()
      }
    }

    this.on_change(schema, () => {
      this.worker.table(this.model.schema).then((table: any) => {
        this.table = table
        this.table.update(this.data)
        this.perspective_element.load(this.table)
      })
    })
    this.on_change(columns, not_updating(() => {
      this.perspective_element.restore({columns: this.model.columns})
    }))
    this.on_change(expressions, not_updating(() => {
      this.perspective_element.restore({expressions: this.model.expressions})
    }))
    this.on_change(split_by, not_updating(() => {
      this.perspective_element.restore({split_by: this.model.split_by})
    }))
    this.on_change(group_by, not_updating(() => {
      this.perspective_element.restore({group_by: this.model.group_by})
    }))
    this.on_change(aggregates, not_updating(() => {
      this.perspective_element.restore({aggregates: this.model.aggregates})
    }))
    this.on_change(filters, not_updating(() => {
      this.perspective_element.restore({filter: this.model.filters})
    }))
    this.on_change(settings, not_updating(() => {
      this.perspective_element.restore({settings: this.model.settings})
    }))
    this.on_change(title, not_updating(() => {
      this.perspective_element.restore({title: this.model.title})
    }))
    this.on_change(sort, not_updating(() => {
      this.perspective_element.restore({sort: this.model.sort})
    }))
    this.on_change(plugin, not_updating(() => {
      this.perspective_element.restore({plugin: PLUGINS[this.model.plugin], ...settings})
    }))
    this.on_change(selectable, not_updating(() => {
      this.perspective_element.restore({plugin_config: {...this._current_config, selectable: this.model.selectable}})
    }))
    this.on_change(editable, not_updating(() => {
      this.perspective_element.restore({plugin_config: {...this._current_config, editable: this.model.editable}})
    }))
    this.on_change(theme, not_updating(() => {
      this.perspective_element.restore({theme: THEMES[this.model.theme as string]}).catch(() => {})
    }))
  }

  override disconnect_signals(): void {
    if (this._config_listener != null) {
      this.perspective_element.removeEventListener("perspective-config-update", this._config_listener)
    }
    this._config_listener = null
    super.disconnect_signals()
  }

  override remove(): void {
    if (this.perspective_element) {
      this.perspective_element.delete(() => this.worker.terminate())
    }
    super.remove()
  }

  override stylesheets(): StyleSheetLike[] {
    return [...super.stylesheets(), perspective_css]
  }

  override render(): void {
    super.render()
    const container = div({
      class: "pnx-perspective-viewer",
      style: {
        zIndex: "0",
      },
    })
    this._current_plugin = this.model.plugin

    set_size(container, this.model)
    this.shadow_el.appendChild(container)

    new Promise((resolve, reject) => {
      const start = Date.now()
      const timeout = 5000
      const interval = 100

      const check = () => {
        if ((window as any).perspective_viewer !== undefined) {
          resolve(null)
        } else if (Date.now() - start > timeout) {
          reject(new Error("Timed out waiting for window.perspective_viewer"))
        } else {
          setTimeout(check, interval)
        }
      }
      check()
    }).then(() => {
      (window as any).perspective.worker().then((worker: any) => {
        this.worker = worker
        worker.table(this.model.schema).then((table: any) => {
          this.table = table
          this.table.update(this.data)
          container.innerHTML = "<perspective-viewer style='height:100%; width:100%;'></perspective-viewer>"
          this.perspective_element = container.children[0]

          const themesArray = Object.values(THEMES)
          const filteredThemes = themesArray.filter(t => t !== this.model.theme)
          const orderedThemes = [this.model.theme, ...filteredThemes]
          this.perspective_element.resetThemes(orderedThemes).catch(() => {})

          this.perspective_element.load(this.table)
          const plugin_config = {
            ...this.model.plugin_config,
            editable: this.model.editable,
            selectable: this.model.selectable,
          }

          this.perspective_element.restore({
            aggregates: this.model.aggregates,
            columns: this.model.columns,
            columns_config: this.model.columns_config,
            expressions: this.model.expressions,
            filter: this.model.filters,
            split_by: this.model.split_by,
            group_by: this.model.group_by,
            plugin: PLUGINS[this.model.plugin],
            plugin_config,
            settings: this.model.settings,
            sort: this.model.sort,
            theme: THEMES[this.model.theme ],
            title: this.model.title,
          }).catch(() => {})

          this.perspective_element.save().then((config: any) => {
            this._current_config = config
          })
          this._config_listener = () => this.sync_config()
          this.perspective_element.addEventListener("perspective-config-update", this._config_listener)
          this.perspective_element.addEventListener("perspective-click", (event: any) => {
            this.model.trigger_event(new PerspectiveClickEvent(event.detail.config, event.detail.column_names, event.detail.row))
          })
          this._loaded = true
        })
      })
    })
  }

  sync_config(): boolean {
    if (this._updating) {
      return true
    }
    this.perspective_element.save().then((config: any) => {
      if (config.plugin !== this._current_plugin) {
        this._plugin_configs.set(this._current_plugin, {
          columns: this._current_config.columns,
          columns_config: this._current_config.columns_config,
          plugin_config: this._current_config.plugin_config,
        })
        if (this._plugin_configs.has(config.plugin)) {
          const overrides = this._plugin_configs.get(config.plugin)
          this.perspective_element.restore(overrides)
          config = {...config, ...overrides}
        }
      }
      this._current_config = config
      this._current_plugin = config.plugin
      const props: any =  {}
      for (let option in config) {
        let value = config[option]
        if (value === undefined || (option == "plugin" && value === "debug") || option == "version" || this.model.properties.hasOwnProperty(option) === undefined) {
          continue
        }
        if (option === "filter") {
          option = "filters"
        } else if (option === "plugin") {
          value = PLUGINS_REVERSE[value ]
        } else if (option === "theme") {
          value = THEMES_REVERSE[value ]
        }
        props[option] = value
      }
      this._updating = true
      this.model.setv(props)
      this._updating = false
    })
    return true
  }

  get data(): any {
    const data: any = {}
    for (const column of this.model.source.columns()) {
      let array = this.model.source.get_array(column)
      if (this.model.schema[column] == "datetime" && array.includes(-9223372036854776)) {
        array = array.map((v) => v === -9223372036854776 ? null : v)
      }
      data[column] = array
    }
    return data
  }

  setData(): void {
    if (!this._loaded) {
      return
    }
    for (const col of this.model.source.columns()) {
      if (!(col in this.model.schema)) {
        return
      }
    }
    this.table.replace(this.data)
  }

  stream(): void {
    if (this._loaded) {
      this.table.replace(this.data)
    }
  }

  patch(): void {
    if (this._loaded) {
      this.table.replace(this.data)
    }
  }
}

export namespace Perspective {
  export type Attrs = p.AttrsOf<Props>
  export type Props = HTMLBox.Props & {
    aggregates: p.Property<any>
    split_by: p.Property<any[] | null>
    columns: p.Property<any[]>
    columns_config: p.Property<any>
    expressions: p.Property<any>
    editable: p.Property<boolean | null>
    filters: p.Property<any[] | null>
    group_by: p.Property<any[] | null>
    plugin: p.Property<any>
    plugin_config: p.Property<any>
    selectable: p.Property<boolean | null>
    schema: p.Property<any>
    settings: p.Property<boolean>
    sort: p.Property<any[] | null>
    source: p.Property<ColumnDataSource>
    theme: p.Property<any>
    title: p.Property<string | null>
  }
}

export interface Perspective extends Perspective.Attrs { }

export class Perspective extends HTMLBox {
  declare properties: Perspective.Props

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

  static override __module__ = "panel.models.perspective"

  static {
    this.prototype.default_view = PerspectiveView

    this.define<Perspective.Props>(({Any, List, Bool, Ref, Nullable, Str}) => ({
      aggregates:       [ Any,                         {} ],
      columns:          [ List(Nullable(Str)),         [] ],
      columns_config:   [ Any,                         {} ],
      expressions:      [ Any,                         {} ],
      split_by:         [ Nullable(List(Str)),       null ],
      editable:         [ Bool,                      true ],
      filters:          [ Nullable(List(Any)),       null ],
      group_by:         [ Nullable(List(Str)),       null ],
      plugin:           [ Str                             ],
      plugin_config:    [ Any,                         {} ],
      selectable:       [ Bool,                      true ],
      settings:         [ Bool,                      true ],
      schema:           [ Any,                         {} ],
      sort:             [ Nullable(List(List(Str))), null ],
      source:           [ Ref(ColumnDataSource)           ],
      theme:            [ Str,                      "pro" ],
      title:            [ Nullable(Str),             null ],
    }))
  }
}
