import type {IterViews} from "@bokehjs/core/build_views"
import {build_view} from "@bokehjs/core/build_views"
import {ButtonType} from "@bokehjs/core/enums"
import type * as p from "@bokehjs/core/properties"

import {InputWidget, InputWidgetView} from "@bokehjs/models/widgets/input_widget"
import type {IconView} from "@bokehjs/models/ui/icons/icon"
import {Icon} from "@bokehjs/models/ui/icons/icon"

import buttons_css, * as buttons from "@bokehjs/styles/buttons.css"
import type {StyleSheetLike} from "@bokehjs/core/dom"
import {nbsp, text, button, input} from "@bokehjs/core/dom"

function dataURItoBlob(dataURI: string) {
  // convert base64 to raw binary data held in a string

  const byteString = atob(dataURI.split(",")[1])

  // separate out the mime component
  const mimeString = dataURI.split(",")[0].split(":")[1].split(";")[0]

  // write the bytes of the string to an ArrayBuffer
  const ab = new ArrayBuffer(byteString.length)
  const ia = new Uint8Array(ab)
  for (let i = 0; i < byteString.length; i++) {
    ia[i] = byteString.charCodeAt(i)
  }

  // write the ArrayBuffer to a blob, and you're done
  const bb = new Blob([ab], {type: mimeString})
  return bb
}

export class FileDownloadView extends InputWidgetView {
  declare model: FileDownload

  protected icon_view?: IconView

  anchor_el: HTMLAnchorElement
  button_el: HTMLButtonElement
  label_el: Text
  declare input_el: HTMLInputElement  // HACK: So this.input_el.id = "input" can be set in Bokeh 3.4
  _downloadable: boolean = false
  _click_listener: any
  _prev_href: string | null = ""
  _prev_download: string | null = ""

  override *children(): IterViews {
    yield* super.children()
    if (this.icon_view != null) {
      yield this.icon_view
    }
  }

  override *controls() {
    yield (this.anchor_el as any)
    yield (this.button_el as any)
  }

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

    const {button_type, filename, _transfers, label} = this.model.properties
    this.on_change(button_type, () => this._update_button_style())
    this.on_change(filename, () => this._update_download())
    this.on_change(_transfers, () => this._handle_click())
    this.on_change(label, () => this._update_label())
  }

  override remove(): void {
    if (this.icon_view != null) {
      this.icon_view.remove()
    }
    super.remove()
  }

  override async lazy_initialize(): Promise<void> {
    await super.lazy_initialize()
    const {icon} = this.model
    if (icon != null) {
      this.icon_view = await build_view(icon, {parent: this})
    }
  }

  _render_input(): HTMLElement {
    // Create an anchor HTML element that is styled as a bokeh button.
    // When its 'href' and 'download' attributes are set, it's a downloadable link:
    // * A click triggers a download
    // * A right click allows to "Save as" the file

    // There are three main cases:
    // 1. embed=True: The widget is a download link
    // 2. auto=False: The widget is first a button and becomes a download link after the first click
    // 3. auto=True: The widget is a button, i.e right click to "Save as..." won't work
    this.anchor_el = document.createElement("a")
    this.button_el = button({
      disabled: this.model.disabled,
    })
    if (this.icon_view != null) {
      const separator = this.model.label != "" ? nbsp() : text("")
      this.anchor_el.appendChild(this.icon_view.el)
      this.anchor_el.appendChild(separator)
      this.icon_view.render()
    }
    this.label_el = document.createTextNode(this.model.label)
    this.anchor_el.appendChild(this.label_el)
    this._update_button_style()

    // Changing the disabled property calls render() so it needs to be handled here.
    // This callback is inherited from ControlView in bokehjs.
    if (this.model.disabled) {
      this.anchor_el.setAttribute("disabled", "")
      this._downloadable = false
    } else {
      this.anchor_el.removeAttribute("disabled")
      // auto=False + toggle Disabled ==> Needs to reset the link as it was.
      if (this._prev_download) {
        this.anchor_el.download = this._prev_download
      }
      if (this._prev_href) {
        this.anchor_el.href = this._prev_href
      }
      if (this.anchor_el.download && this.anchor_el.download) {
        this._downloadable = true
      }
    }

    // If embedded the button is just a download link.
    // Otherwise clicks will be handled by the code itself, allowing for more interactivity.
    if (this.model.embed) {
      this._make_link_downloadable()
    } else {
      // Add a "click" listener, note that it's not going to
      // handle right clicks (they won't increment 'clicks')
      this._click_listener = this._increment_clicks.bind(this)
      this.anchor_el.addEventListener("click", this._click_listener)
    }
    this.button_el.appendChild(this.anchor_el)
    this.input_el = input() // HACK: So this.input_el.id = "input" can be set in Bokeh 3.4
    return this.button_el
  }

  override render(): void {
    super.render()
    this.group_el.style.display = "flex"
    this.group_el.style.alignItems = "stretch"
  }

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

  _increment_clicks(): void {
    this.model.clicks = this.model.clicks + 1
  }

  _handle_click(): void {
    // When auto=False the button becomes a link which no longer
    // requires being updated.
    if ((!this.model.auto && this._downloadable) || this.anchor_el.hasAttribute("disabled")) {
      return
    }

    this._make_link_downloadable()

    if (!this.model.embed && this.model.auto) {
      // Temporarily removing the event listener to emulate a click
      // event on the anchor link which will trigger a download.
      this.anchor_el.removeEventListener("click", this._click_listener)
      this.anchor_el.click()

      // In this case #3 the widget is not a link so these attributes are removed.
      this.anchor_el.removeAttribute("href")
      this.anchor_el.removeAttribute("download")

      this.anchor_el.addEventListener("click", this._click_listener)
    }

    // Store the current state for handling changes of the disabled property.
    this._prev_href = this.anchor_el.getAttribute("href")
    this._prev_download = this.anchor_el.getAttribute("download")
  }

  _make_link_downloadable(): void {
    this._update_href()
    this._update_download()
    if (this.anchor_el.download && this.anchor_el.href) {
      this._downloadable = true
    }
  }

  _update_href(): void {
    if (this.model.data) {
      const blob = dataURItoBlob(this.model.data)
      this.anchor_el.href = (URL as any).createObjectURL(blob)
    }
  }

  _update_download(): void {
    if (this.model.filename) {
      this.anchor_el.download = this.model.filename
    }
  }

  _update_label(): void {
    this.label_el.data = this.model.label
  }

  _update_button_style(): void {
    const btn_type = buttons[`btn_${this.model.button_type}` as const]
    if (!this.button_el.hasAttribute("class")) { // When the widget is rendered.
      this.button_el.classList.add(buttons.btn)
      this.button_el.classList.add(btn_type)
    } else {  // When the button type is changed.
      const prev_button_type = this.anchor_el.classList.item(1)
      if (prev_button_type) {
        this.button_el.classList.replace(prev_button_type, btn_type)
      }
    }
  }
}

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

  export type Props = InputWidget.Props & {
    auto: p.Property<boolean>
    button_type: p.Property<ButtonType>
    clicks: p.Property<number>
    data: p.Property<string | null>
    embed: p.Property<boolean>
    icon: p.Property<Icon | null>
    label: p.Property<string>
    filename: p.Property<string | null>
    _transfers: p.Property<number>
  }
}

export interface FileDownload extends FileDownload.Attrs {}

export class FileDownload extends InputWidget {
  declare properties: FileDownload.Props

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

  static override __module__ = "panel.models.widgets"

  static {
    this.prototype.default_view = FileDownloadView

    this.define<FileDownload.Props>(({Bool, Int, Nullable, Ref, Str}) => ({
      auto:         [ Bool,          false ],
      clicks:       [ Int,                  0 ],
      data:         [ Nullable(Str),  null ],
      embed:        [ Bool,          false ],
      icon:        [ Nullable(Ref(Icon)), null ],
      label:        [ Str,      "Download" ],
      filename:     [ Nullable(Str),  null ],
      button_type:  [ ButtonType,   "default" ], // TODO (bev)
      _transfers:   [ Int,                  0 ],
    }))

    this.override<FileDownload.Props>({
      title: "",
    })
  }
}
