import { Dom, FunctionExt, NumberExt, ObjectExt, Util } from '../../common'
import { Point } from '../../geometry'
import type { Cell, Edge } from '../../model'
import type { CellView, EdgeView, NodeView } from '../../view'
import { ToolItem, type ToolItemOptions } from '../../view/tool'
import { createViewElement } from '../../view/view/util'

export class CellEditor extends ToolItem<
  NodeView | EdgeView,
  CellEditorOptions & { event: Dom.EventObject }
> {
  public static defaults: CellEditorOptions = {
    ...ToolItem.getDefaults(),
    tagName: 'div',
    isSVGElement: false,
    events: {
      mousedown: 'onMouseDown',
      touchstart: 'onMouseDown',
    },
    documentEvents: {
      mouseup: 'onDocumentMouseUp',
      touchend: 'onDocumentMouseUp',
      touchcancel: 'onDocumentMouseUp',
    },
  }
  private editor: HTMLDivElement | null
  private labelIndex = -1
  private distance = 0.5
  private event: Dom.DoubleClickEvent
  private dblClick = this.onCellDblClick.bind(this)

  onRender() {
    const cellView = this.cellView as CellView
    if (cellView) {
      cellView.on('cell:dblclick', this.dblClick)
    }
  }

  createElement() {
    const classNames = [
      this.prefixClassName(
        `${this.cell.isEdge() ? 'edge' : 'node'}-tool-editor`,
      ),
      this.prefixClassName('cell-tool-editor'),
    ]
    this.editor = createViewElement('div', false) as HTMLDivElement
    this.addClass(classNames, this.editor)
    this.editor.contentEditable = 'true'
    this.container.appendChild(this.editor)
  }

  removeElement() {
    this.undelegateDocumentEvents()
    if (this.editor) {
      this.container.removeChild(this.editor)
      this.editor = null
    }
  }

  updateEditor() {
    const { cell, editor } = this

    if (!editor) {
      return
    }

    const { style } = editor

    if (cell.isNode()) {
      this.updateNodeEditorTransform()
    } else if (cell.isEdge()) {
      this.updateEdgeEditorTransform()
    }

    // set font style
    const { attrs } = this.options
    style.fontSize = `${attrs.fontSize}px`
    style.fontFamily = attrs.fontFamily
    style.color = attrs.color
    style.backgroundColor = attrs.backgroundColor

    // set init value
    const text = this.getCellText() || ''
    editor.innerText = text
    this.setCellText('') // clear display value when edit status because char ghosting.

    return this
  }

  updateNodeEditorTransform() {
    const { graph, cell, editor } = this

    if (!editor) {
      return
    }

    let pos = Point.create()
    let minWidth = 20
    let translate = ''
    let { x, y } = this.options
    const { width, height } = this.options

    if (typeof x !== 'undefined' && typeof y !== 'undefined') {
      const bbox = cell.getBBox()
      x = NumberExt.normalizePercentage(x, bbox.width)
      y = NumberExt.normalizePercentage(y, bbox.height)
      pos = bbox.topLeft.translate(x, y)
      minWidth = bbox.width - x * 2
    } else {
      const bbox = cell.getBBox()
      pos = bbox.center
      minWidth = bbox.width - 4
      translate = 'translate(-50%, -50%)'
    }

    const scale = graph.scale()
    const { style } = editor
    pos = graph.localToGraph(pos)
    style.left = `${pos.x}px`
    style.top = `${pos.y}px`
    style.transform = `scale(${scale.sx}, ${scale.sy}) ${translate}`
    style.minWidth = `${minWidth}px`

    if (typeof width === 'number') {
      style.width = `${width}px`
    }
    if (typeof height === 'number') {
      style.height = `${height}px`
    }
  }

  updateEdgeEditorTransform() {
    if (!this.event) {
      return
    }

    const { graph, editor } = this
    if (!editor) {
      return
    }

    let pos = Point.create()
    let minWidth = 20
    const { style } = editor
    const target = this.event.target
    const parent = target.parentElement
    const isEdgeLabel =
      parent && Dom.hasClass(parent, this.prefixClassName('edge-label'))
    if (isEdgeLabel) {
      const index = parent.getAttribute('data-index') || '0'
      this.labelIndex = parseInt(index, 10)
      const matrix = parent.getAttribute('transform')
      const { translation } = Dom.parseTransformString(matrix)
      pos = new Point(translation.tx, translation.ty)
      minWidth = Util.getBBox(target).width
    } else {
      if (!this.options.labelAddable) {
        return this
      }
      pos = graph.clientToLocal(
        Point.create(this.event.clientX, this.event.clientY),
      )
      const view = this.cellView as EdgeView
      const d = view.path.closestPointLength(pos)
      this.distance = d
      this.labelIndex = -1
    }

    pos = graph.localToGraph(pos)
    const scale = graph.scale()
    style.left = `${pos.x}px`
    style.top = `${pos.y}px`
    style.minWidth = `${minWidth}px`
    style.transform = `scale(${scale.sx}, ${scale.sy}) translate(-50%, -50%)`
  }

  updateCell() {
    const value = this.editor.innerText.replace(/\n$/, '') || ''
    // set value, when value is null, we will remove label in edge
    this.setCellText(value !== '' ? value : null)
    // remove tool
    this.removeElement()
  }

  onDocumentMouseUp(e: Dom.MouseDownEvent) {
    if (this.editor && e.target !== this.editor) {
      this.updateCell()
    }
  }

  onCellDblClick({ e }: { e: Dom.DoubleClickEvent }) {
    if (!this.editor) {
      e.stopPropagation()
      this.removeElement()
      this.event = e
      this.createElement()
      this.updateEditor()
      this.autoFocus()
      const documentEvents = this.options.documentEvents
      if (documentEvents) {
        this.delegateDocumentEvents(documentEvents)
      }
    }
  }

  onMouseDown(e: Dom.MouseDownEvent) {
    e.stopPropagation()
  }

  autoFocus() {
    setTimeout(() => {
      if (this.editor) {
        this.editor.focus()
        this.selectText()
      }
    })
  }

  selectText() {
    if (window.getSelection && this.editor) {
      const range = document.createRange()
      range.selectNodeContents(this.editor)
      const selection = window.getSelection()
      selection?.removeAllRanges()
      selection?.addRange(range)
    }
  }

  getCellText() {
    const { getText } = this.options
    if (typeof getText === 'function') {
      return FunctionExt.call(getText, this.cellView, {
        cell: this.cell,
        index: this.labelIndex,
      })
    }
    if (typeof getText === 'string') {
      if (this.cell.isNode()) {
        return this.cell.attr(getText)
      }
      if (this.cell.isEdge()) {
        if (this.labelIndex !== -1) {
          return this.cell.prop(`labels/${this.labelIndex}/attrs/${getText}`)
        }
      }
    }
  }

  setCellText(value: string | null) {
    const setText = this.options.setText
    if (typeof setText === 'function') {
      FunctionExt.call(setText, this.cellView, {
        cell: this.cell,
        value,
        index: this.labelIndex,
        distance: this.distance,
      })
      return
    }
    if (typeof setText === 'string') {
      if (this.cell.isNode()) {
        this.cell.attr(setText, value === null ? '' : value)
        return
      }
      if (this.cell.isEdge()) {
        const edge = this.cell as Edge
        if (this.labelIndex === -1) {
          if (value) {
            const newLabel = {
              position: {
                distance: this.distance,
              },
              attrs: {},
            }
            ObjectExt.setByPath(newLabel, `attrs/${setText}`, value)
            edge.appendLabel(newLabel)
          }
        } else {
          if (value !== null) {
            edge.prop(`labels/${this.labelIndex}/attrs/${setText}`, value)
          } else {
            edge.removeLabelAt(this.labelIndex)
          }
        }
      }
    }
  }

  protected onRemove() {
    const cellView = this.cellView as CellView
    if (cellView) {
      cellView.off('cell:dblclick', this.dblClick)
    }
    this.removeElement()
  }
}

interface CellEditorOptions extends ToolItemOptions {
  x?: number | string
  y?: number | string
  width?: number
  height?: number
  attrs: {
    fontSize: number
    fontFamily: string
    color: string
    backgroundColor: string
  }
  labelAddable?: boolean
  getText:
    | ((
        this: CellView,
        args: {
          cell: Cell
          index?: number
        },
      ) => string)
    | string
  setText:
    | ((
        this: CellView,
        args: {
          cell: Cell
          value: string | null
          index?: number
          distance?: number
        },
      ) => void)
    | string
}

export class NodeEditor extends CellEditor {
  public static defaults: CellEditorOptions = ObjectExt.merge(
    {},
    CellEditor.defaults,
    {
      attrs: {
        fontSize: 14,
        fontFamily: 'Arial, helvetica, sans-serif',
        color: '#000',
        backgroundColor: '#fff',
      },
      getText: 'text/text',
      setText: 'text/text',
    },
  )
}

export class EdgeEditor extends CellEditor {
  public static defaults: CellEditorOptions = ObjectExt.merge(
    {},
    CellEditor.defaults,
    {
      attrs: {
        fontSize: 14,
        fontFamily: 'Arial, helvetica, sans-serif',
        color: '#000',
        backgroundColor: '#fff',
      },
      labelAddable: true,
      getText: 'label/text',
      setText: 'label/text',
    },
  )
}
