import {
  Disposable,
  Dom,
  disposable,
  FunctionExt,
  type KeyValue,
} from '../common'
import { FLAG_INSERT, FLAG_REMOVE } from '../constants'
import type { Rectangle } from '../geometry'
import type { Graph } from '../graph'
import type { Cell, ModelEventArgs } from '../model'
import { CellView, EdgeView, NodeView, type View } from '../view'
import type { FlagManagerAction } from '../view/flag'
import { JOB_PRIORITY, JobQueue } from './queueJob'

export enum SchedulerViewState {
  CREATED,
  MOUNTED,
  WAITING,
}

export interface SchedulerView {
  view: CellView
  flag: number
  options: KeyValue
  state: SchedulerViewState
}

export interface SchedulerEventArgs {
  'view:mounted': { view: CellView }
  'view:unmounted': { view: CellView }
  'render:done': null
}
export class Scheduler extends Disposable {
  public views: KeyValue<SchedulerView> = {}
  public willRemoveViews: KeyValue<SchedulerView> = {}
  protected zPivots: KeyValue<Comment>
  private graph: Graph
  private renderArea?: Rectangle
  private queue: JobQueue

  get model() {
    return this.graph.model
  }

  get container() {
    return this.graph.view.stage
  }

  constructor(graph: Graph) {
    super()
    this.queue = new JobQueue()
    this.graph = graph
    this.init()
  }

  protected init() {
    this.startListening()
    this.renderViews(this.model.getCells())
  }

  protected startListening() {
    this.model.on('reseted', this.onModelReseted, this)
    this.model.on('cell:added', this.onCellAdded, this)
    this.model.on('cell:removed', this.onCellRemoved, this)
    this.model.on('cell:change:zIndex', this.onCellZIndexChanged, this)
    this.model.on('cell:change:visible', this.onCellVisibleChanged, this)
  }

  protected stopListening() {
    this.model.off('reseted', this.onModelReseted, this)
    this.model.off('cell:added', this.onCellAdded, this)
    this.model.off('cell:removed', this.onCellRemoved, this)
    this.model.off('cell:change:zIndex', this.onCellZIndexChanged, this)
    this.model.off('cell:change:visible', this.onCellVisibleChanged, this)
  }

  protected onModelReseted({ options, previous }: ModelEventArgs['reseted']) {
    let cells = this.model.getCells()
    if (!options?.diff) {
      this.queue.clearJobs()
      this.removeZPivots()
      this.resetViews()
    } else {
      const previousSet = new Set(previous)
      cells = cells.filter((cell) => !previousSet.has(cell))
    }
    this.renderViews(cells, { ...options, queue: cells.map((cell) => cell.id) })
  }

  protected onCellAdded({ cell, options }: ModelEventArgs['cell:added']) {
    this.renderViews([cell], options)
  }

  protected onCellRemoved({ cell }: ModelEventArgs['cell:removed']) {
    this.removeViews([cell])
  }

  protected onCellZIndexChanged({
    cell,
    options,
  }: ModelEventArgs['cell:change:zIndex']) {
    const viewItem = this.views[cell.id]
    if (viewItem) {
      this.requestViewUpdate(
        viewItem.view,
        FLAG_INSERT,
        options,
        JOB_PRIORITY.Update,
        true,
      )
    }
  }

  protected onCellVisibleChanged({
    cell,
    current,
  }: ModelEventArgs['cell:change:visible']) {
    this.toggleVisible(cell, !!current)
  }

  requestViewUpdate(
    view: CellView,
    flag: number,
    options: KeyValue = {},
    priority: JOB_PRIORITY = JOB_PRIORITY.Update,
    flush = true,
  ) {
    const id = view.cell.id
    const viewItem = this.views[id]

    if (!viewItem) {
      return
    }

    const nextFlag = viewItem.flag | flag
    viewItem.flag = nextFlag
    const prevOptions = viewItem.options || {}
    const nextOptions = options || {}
    if (prevOptions.queue && nextOptions.queue == null) {
      nextOptions.queue = prevOptions.queue
    }
    if (prevOptions.async === false || nextOptions.async === false) {
      nextOptions.async = false
    }
    viewItem.options = nextOptions

    const priorAction = view.hasAction(flag, ['translate', 'resize', 'rotate'])
    if (priorAction || nextOptions.async === false) {
      priority = JOB_PRIORITY.PRIOR // eslint-disable-line
      flush = false // eslint-disable-line
    }

    this.queue.queueJob({
      id,
      priority,
      cb: () => {
        const current = this.views[id]
        if (!current) return

        const currentOptions = current.options || {}
        this.renderViewInArea(current.view, current.flag, currentOptions)
        const queue = currentOptions.queue
        if (queue) {
          const index = queue.indexOf(current.view.cell.id)
          if (index >= 0) {
            queue.splice(index, 1)
          }
          if (queue.length === 0) {
            this.graph.trigger('render:done')
          }
        }
      },
    })

    const effectedEdges = this.getEffectedEdges(view)
    effectedEdges.forEach((edge) => {
      this.requestViewUpdate(edge.view, edge.flag, options, priority, false)
    })

    if (flush) {
      this.flush()
    }
  }

  setRenderArea(area?: Rectangle) {
    this.renderArea = area

    // 当可视渲染区域变化时，卸载不在区域内且已挂载的视图
    Object.values(this.views).forEach((viewItem) => {
      if (!viewItem) return
      const { view } = viewItem
      if (viewItem.state === SchedulerViewState.MOUNTED) {
        if (!this.isUpdatable(view)) {
          // 卸载 DOM
          view.remove()
          this.graph.trigger('view:unmounted', { view })
          // 切换到 WAITING 状态，等待重新进入区域时再插入
          viewItem.state = SchedulerViewState.WAITING
          // 确保重新进入可视区域后会重新插入，并执行视图的 render 等动作，让 react 等节点能重新展示
          viewItem.flag |= FLAG_INSERT | view.getBootstrapFlag()
        }
      }
    })

    this.flushWaitingViews()
  }

  isViewMounted(view: CellView) {
    if (view == null) {
      return false
    }

    const viewItem = this.views[view.cell.id]

    if (!viewItem) {
      return false
    }

    return viewItem.state === SchedulerViewState.MOUNTED
  }

  protected renderViews(cells: Cell[], options: any = {}) {
    cells.sort((c1, c2) => {
      if (c1.isNode() && c2.isEdge()) {
        return -1
      }
      return 0
    })

    cells.forEach((cell) => {
      const id = cell.id
      const views = this.views
      let flag = 0
      let viewItem = views[id]

      if (viewItem) {
        flag = FLAG_INSERT
      } else {
        const cellView = this.createCellView(cell)
        if (cellView) {
          cellView.graph = this.graph
          flag = FLAG_INSERT | cellView.getBootstrapFlag()
          viewItem = {
            view: cellView,
            flag,
            options,
            state: SchedulerViewState.CREATED,
          }
          this.views[id] = viewItem
        }
      }

      if (viewItem) {
        this.requestViewUpdate(
          viewItem.view,
          flag,
          options,
          this.getRenderPriority(viewItem.view),
          false,
        )
      }
    })

    this.flush()
  }

  protected renderViewInArea(view: CellView, flag: number, options: any = {}) {
    const cell = view.cell
    const id = cell.id
    const viewItem = this.views[id]

    if (!viewItem) {
      return
    }

    let result = 0
    if (this.isUpdatable(view)) {
      result = this.updateView(view, flag, options)
      viewItem.flag = result
    } else {
      // 视图不在当前可渲染区域内
      if (viewItem.state === SchedulerViewState.MOUNTED) {
        // 将已挂载但不在可视区域的视图从 DOM 中卸载
        view.remove()
        this.graph.trigger('view:unmounted', { view })
        result = 0
      }
      // 标记为 WAITING 状态，以便在可视区域变化时重新渲染
      viewItem.state = SchedulerViewState.WAITING
      // 确保重新进入可视区域时能够重新插入到 DOM
      viewItem.flag = flag | FLAG_INSERT | view.getBootstrapFlag()
    }

    if (result) {
      if (
        cell.isEdge() &&
        (result & view.getFlag(['source', 'target'])) === 0
      ) {
        this.queue.queueJob({
          id,
          priority: JOB_PRIORITY.RenderEdge,
          cb: () => {
            this.updateView(view, flag, options)
          },
        })
      }
    }
  }

  protected removeViews(cells: Cell[]) {
    cells.forEach((cell) => {
      const id = cell.id
      const viewItem = this.views[id]

      if (viewItem) {
        this.willRemoveViews[id] = viewItem
        delete this.views[id]

        this.queue.queueJob({
          id,
          priority: this.getRenderPriority(viewItem.view),
          cb: () => {
            this.removeView(viewItem.view)
          },
        })
      }
    })

    this.flush()
  }

  protected flush() {
    this.graph.options.async
      ? this.queue.queueFlush()
      : this.queue.queueFlushSync()
  }

  protected flushWaitingViews() {
    Object.values(this.views).forEach((viewItem) => {
      if (viewItem && viewItem.state === SchedulerViewState.WAITING) {
        const { view, flag, options } = viewItem
        this.requestViewUpdate(
          view,
          flag,
          options,
          this.getRenderPriority(view),
          false,
        )
      }
    })

    this.flush()
  }

  protected updateView(view: View, flag: number, options: KeyValue = {}) {
    if (view == null) {
      return 0
    }

    if (CellView.isCellView(view)) {
      if (flag & FLAG_REMOVE) {
        this.removeView(view)
        return 0
      }

      if (flag & FLAG_INSERT) {
        this.insertView(view)
        flag ^= FLAG_INSERT // eslint-disable-line
      }
    }

    if (!flag) {
      return 0
    }

    return view.confirmUpdate(flag, options)
  }

  protected insertView(view: CellView) {
    const viewItem = this.views[view.cell.id]
    if (viewItem) {
      const zIndex = view.cell.getZIndex()
      const pivot = this.addZPivot(zIndex)
      this.container.insertBefore(view.container, pivot)

      if (!view.cell.isVisible()) {
        this.toggleVisible(view.cell, false)
      }

      viewItem.state = SchedulerViewState.MOUNTED
      this.graph.trigger('view:mounted', { view })
    }
  }

  protected resetViews() {
    this.willRemoveViews = { ...this.views, ...this.willRemoveViews }
    Object.values(this.willRemoveViews).forEach((viewItem) => {
      if (viewItem) {
        this.removeView(viewItem.view)
      }
    })
    this.views = {}
    this.willRemoveViews = {}
  }

  protected removeView(view: CellView) {
    const cell = view.cell
    const viewItem = this.willRemoveViews[cell.id]
    if (viewItem && view) {
      viewItem.view.remove()
      delete this.willRemoveViews[cell.id]
      this.graph.trigger('view:unmounted', { view })
    }
  }

  protected toggleVisible(cell: Cell, visible: boolean) {
    const edges = this.model.getConnectedEdges(cell)

    for (let i = 0, len = edges.length; i < len; i += 1) {
      const edge = edges[i]
      if (visible) {
        const source = edge.getSourceCell()
        const target = edge.getTargetCell()
        if (
          (source && !source.isVisible()) ||
          (target && !target.isVisible())
        ) {
          continue
        }
        this.toggleVisible(edge, true)
      } else {
        this.toggleVisible(edge, false)
      }
    }

    const viewItem = this.views[cell.id]
    if (viewItem) {
      Dom.css(viewItem.view.container, {
        display: visible ? 'unset' : 'none',
      })
    }
  }

  protected addZPivot(zIndex = 0) {
    if (this.zPivots == null) {
      this.zPivots = {}
    }

    const pivots = this.zPivots
    let pivot = pivots[zIndex]
    if (pivot) {
      return pivot
    }

    pivot = pivots[zIndex] = document.createComment(`z-index:${zIndex + 1}`)
    let neighborZ = -Infinity
    // eslint-disable-next-line
    for (const key in pivots) {
      const currentZ = +key
      if (currentZ < zIndex && currentZ > neighborZ) {
        neighborZ = currentZ
        if (neighborZ === zIndex - 1) {
        }
      }
    }

    const layer = this.container
    if (neighborZ !== -Infinity) {
      const neighborPivot = pivots[neighborZ]
      layer.insertBefore(pivot, neighborPivot.nextSibling)
    } else {
      layer.insertBefore(pivot, layer.firstChild)
    }
    return pivot
  }

  protected removeZPivots() {
    if (this.zPivots) {
      Object.values(this.zPivots).forEach((elem) => {
        if (elem && elem.parentNode) {
          elem.parentNode.removeChild(elem)
        }
      })
    }
    this.zPivots = {}
  }

  protected createCellView(cell: Cell) {
    const options = { graph: this.graph }

    const createViewHook = this.graph.options.createCellView
    if (createViewHook) {
      const ret = FunctionExt.call(createViewHook, this.graph, cell)
      if (ret) {
        return new ret(cell, options) // eslint-disable-line new-cap
      }
      if (ret === null) {
        // null means not render
        return null
      }
    }

    const view = cell.view

    if (view != null && typeof view === 'string') {
      const def = CellView.registry.get(view)
      if (def) {
        return new def(cell, options) // eslint-disable-line new-cap
      }
      return CellView.registry.onNotFound(view)
    }

    if (cell.isNode()) {
      return new NodeView(cell, options)
    }

    if (cell.isEdge()) {
      return new EdgeView(cell, options)
    }

    return null
  }

  protected getEffectedEdges(view: CellView) {
    const effectedEdges: { id: string; view: CellView; flag: number }[] = []
    const cell = view.cell
    const edges = this.model.getConnectedEdges(cell)

    for (let i = 0, n = edges.length; i < n; i += 1) {
      const edge = edges[i]
      const viewItem = this.views[edge.id]

      if (!viewItem) {
        continue
      }

      const edgeView = viewItem.view
      if (!this.isViewMounted(edgeView)) {
        continue
      }

      const flagLabels: FlagManagerAction[] = ['update']
      if (edge.getTargetCell() === cell) {
        flagLabels.push('target')
      }
      if (edge.getSourceCell() === cell) {
        flagLabels.push('source')
      }
      effectedEdges.push({
        id: edge.id,
        view: edgeView,
        flag: edgeView.getFlag(flagLabels),
      })
    }

    return effectedEdges
  }

  protected isUpdatable(view: CellView) {
    if (view.isNodeView()) {
      if (this.renderArea) {
        return this.renderArea.isIntersectWithRect(view.cell.getBBox())
      }
      return true
    }

    if (view.isEdgeView()) {
      const edge = view.cell
      const intersects = this.renderArea
        ? this.renderArea.isIntersectWithRect(edge.getBBox())
        : true
      if (this.graph.virtualRender.isVirtualEnabled()) {
        return intersects
      }

      const sourceCell = edge.getSourceCell()
      const targetCell = edge.getTargetCell()
      if (this.renderArea && sourceCell && targetCell) {
        return (
          this.renderArea.isIntersectWithRect(sourceCell.getBBox()) ||
          this.renderArea.isIntersectWithRect(targetCell.getBBox())
        )
      }
    }

    return true
  }

  protected getRenderPriority(view: CellView) {
    return view.cell.isNode()
      ? JOB_PRIORITY.RenderNode
      : JOB_PRIORITY.RenderEdge
  }

  @disposable()
  dispose() {
    this.stopListening()
    // clear views
    Object.keys(this.views).forEach((id) => {
      this.views[id].view.dispose()
    })
    this.views = {}
  }
}
