import { generateUUID, getArrowPoints, getObjById, getOffsetLT, setAttributes } from './utils/index'
import LinkDragMoveHelper from './utils/LinkDragMoveHelper'
import { createSvgGroup, createSvgText, editSvgText, svgNS } from './utils/svg'
import type { CustomSvg, Topic } from './types/dom'
import type { MindElixirInstance, Uid } from './index'

const highlightColor = '#4dc4ff'

export interface Arrow {
  id: string
  /**
   * label of arrow
   */
  label: string
  /**
   * id of start node
   */
  from: Uid
  /**
   * id of end node
   */
  to: Uid
  /**
   *  offset of control point from start point
   */
  delta1: {
    x: number
    y: number
  }
  /**
   * offset of control point from end point
   */
  delta2: {
    x: number
    y: number
  }
  /**
   * whether the arrow is bidirectional
   */
  bidirectional?: boolean
  /**
   * style properties for the arrow
   */
  style?: {
    /**
     * stroke color of the arrow
     */
    stroke?: string
    /**
     * stroke width of the arrow
     */
    strokeWidth?: string | number
    /**
     * stroke dash array for dashed lines
     */
    strokeDasharray?: string
    /**
     * stroke line cap style
     */
    strokeLinecap?: 'butt' | 'round' | 'square'
    /**
     * opacity of the arrow
     */
    opacity?: string | number
    /**
     * color of the arrow label
     */
    labelColor?: string
  }
}
export type DivData = {
  cx: number // center x
  cy: number // center y
  w: number // div width
  h: number // div height
  ctrlX: number // control point x
  ctrlY: number // control point y
}
export type ArrowOptions = {
  bidirectional?: boolean
  style?: {
    stroke?: string
    strokeWidth?: string | number
    strokeDasharray?: string
    strokeLinecap?: 'butt' | 'round' | 'square'
    opacity?: string | number
    labelColor?: string
  }
}

/**
 * Calculate bezier curve midpoint position
 */
function calcBezierMidPoint(p1x: number, p1y: number, p2x: number, p2y: number, p3x: number, p3y: number, p4x: number, p4y: number) {
  return {
    x: p1x / 8 + (p2x * 3) / 8 + (p3x * 3) / 8 + p4x / 8,
    y: p1y / 8 + (p2y * 3) / 8 + (p3y * 3) / 8 + p4y / 8,
  }
}

/**
 * Update arrow label position
 */
function updateArrowLabel(label: SVGTextElement, x: number, y: number) {
  setAttributes(label, {
    x: x + '',
    y: y + '',
  })
}

/**
 * Update control line position
 */
function updateControlLine(line: SVGElement, x1: number, y1: number, x2: number, y2: number) {
  setAttributes(line, {
    x1: x1 + '',
    y1: y1 + '',
    x2: x2 + '',
    y2: y2 + '',
  })
}

/**
 * Update arrow path and related elements
 */
function updateArrowPath(
  arrow: CustomSvg,
  p1x: number,
  p1y: number,
  p2x: number,
  p2y: number,
  p3x: number,
  p3y: number,
  p4x: number,
  p4y: number,
  linkItem: Arrow
) {
  const mainPath = `M ${p1x} ${p1y} C ${p2x} ${p2y} ${p3x} ${p3y} ${p4x} ${p4y}`

  // Update main path
  arrow.line.setAttribute('d', mainPath)

  // Apply styles to the main line if they exist
  if (linkItem.style) {
    const style = linkItem.style
    if (style.stroke) arrow.line.setAttribute('stroke', style.stroke)
    if (style.strokeWidth) arrow.line.setAttribute('stroke-width', String(style.strokeWidth))
    if (style.strokeDasharray) arrow.line.setAttribute('stroke-dasharray', style.strokeDasharray)
    if (style.strokeLinecap) arrow.line.setAttribute('stroke-linecap', style.strokeLinecap)
    if (style.opacity !== undefined) arrow.line.setAttribute('opacity', String(style.opacity))
  }

  // Update hotzone for main path (find the first hotzone path which corresponds to the main line)
  const hotzones = arrow.querySelectorAll('path[stroke="transparent"]')
  if (hotzones.length > 0) {
    hotzones[0].setAttribute('d', mainPath)
  }

  // Update arrow head
  const arrowPoint = getArrowPoints(p3x, p3y, p4x, p4y)
  if (arrowPoint) {
    const arrowPath1 = `M ${arrowPoint.x1} ${arrowPoint.y1} L ${p4x} ${p4y} L ${arrowPoint.x2} ${arrowPoint.y2}`
    arrow.arrow1.setAttribute('d', arrowPath1)

    // Update hotzone for arrow1
    if (hotzones.length > 1) {
      hotzones[1].setAttribute('d', arrowPath1)
    }

    // Apply styles to arrow head
    if (linkItem.style) {
      const style = linkItem.style
      if (style.stroke) arrow.arrow1.setAttribute('stroke', style.stroke)
      if (style.strokeWidth) arrow.arrow1.setAttribute('stroke-width', String(style.strokeWidth))
      if (style.strokeLinecap) arrow.arrow1.setAttribute('stroke-linecap', style.strokeLinecap)
      if (style.opacity !== undefined) arrow.arrow1.setAttribute('opacity', String(style.opacity))
    }
  }

  // Update start arrow if bidirectional
  if (linkItem.bidirectional) {
    const arrowPointStart = getArrowPoints(p2x, p2y, p1x, p1y)
    if (arrowPointStart) {
      const arrowPath2 = `M ${arrowPointStart.x1} ${arrowPointStart.y1} L ${p1x} ${p1y} L ${arrowPointStart.x2} ${arrowPointStart.y2}`
      arrow.arrow2.setAttribute('d', arrowPath2)

      // Update hotzone for arrow2
      if (hotzones.length > 2) {
        hotzones[2].setAttribute('d', arrowPath2)
      }

      // Apply styles to start arrow head
      if (linkItem.style) {
        const style = linkItem.style
        if (style.stroke) arrow.arrow2.setAttribute('stroke', style.stroke)
        if (style.strokeWidth) arrow.arrow2.setAttribute('stroke-width', String(style.strokeWidth))
        if (style.strokeLinecap) arrow.arrow2.setAttribute('stroke-linecap', style.strokeLinecap)
        if (style.opacity !== undefined) arrow.arrow2.setAttribute('opacity', String(style.opacity))
      }
    }
  }

  // Update label position and color
  const { x: halfx, y: halfy } = calcBezierMidPoint(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y)
  updateArrowLabel(arrow.label, halfx, halfy)

  // Apply label color if specified
  if (linkItem.style?.labelColor) {
    arrow.label.setAttribute('fill', linkItem.style.labelColor)
  }

  // Update highlight layer
  updateArrowHighlight(arrow)
}

/**
 * calc control point, center point and div size
 */
function calcCtrlP(mei: MindElixirInstance, tpc: Topic, delta: { x: number; y: number }) {
  const { offsetLeft: x, offsetTop: y } = getOffsetLT(mei.nodes, tpc)
  const w = tpc.offsetWidth
  const h = tpc.offsetHeight
  const cx = x + w / 2
  const cy = y + h / 2
  const ctrlX = cx + delta.x
  const ctrlY = cy + delta.y
  return {
    w,
    h,
    cx,
    cy,
    ctrlX,
    ctrlY,
  }
}

/**
 * calc start and end point using control point and div status
 */
function calcP(data: DivData) {
  let x, y
  const k = (data.cy - data.ctrlY) / (data.ctrlX - data.cx)
  if (k > data.h / data.w || k < -data.h / data.w) {
    if (data.cy - data.ctrlY < 0) {
      x = data.cx - data.h / 2 / k
      y = data.cy + data.h / 2
    } else {
      x = data.cx + data.h / 2 / k
      y = data.cy - data.h / 2
    }
  } else {
    if (data.cx - data.ctrlX < 0) {
      x = data.cx + data.w / 2
      y = data.cy - (data.w * k) / 2
    } else {
      x = data.cx - data.w / 2
      y = data.cy + (data.w * k) / 2
    }
  }
  return {
    x,
    y,
  }
}

/**
 * FYI
 * p1: start point
 * p2: control point of start point
 * p3: control point of end point
 * p4: end point
 */
const drawArrow = function (mei: MindElixirInstance, from: Topic, to: Topic, obj: Arrow, isInitPaint?: boolean) {
  if (!from || !to) {
    return // not expand
  }

  const fromData = calcCtrlP(mei, from, obj.delta1)
  const toData = calcCtrlP(mei, to, obj.delta2)

  const { x: p1x, y: p1y } = calcP(fromData)
  const { ctrlX: p2x, ctrlY: p2y } = fromData
  const { ctrlX: p3x, ctrlY: p3y } = toData
  const { x: p4x, y: p4y } = calcP(toData)

  const arrowT = getArrowPoints(p3x, p3y, p4x, p4y)
  if (!arrowT) return

  const toArrow = `M ${arrowT.x1} ${arrowT.y1} L ${p4x} ${p4y} L ${arrowT.x2} ${arrowT.y2}`
  let fromArrow = ''
  if (obj.bidirectional) {
    const arrowF = getArrowPoints(p2x, p2y, p1x, p1y)
    if (!arrowF) return
    fromArrow = `M ${arrowF.x1} ${arrowF.y1} L ${p1x} ${p1y} L ${arrowF.x2} ${arrowF.y2}`
  }
  const newSvgGroup = createSvgGroup(`M ${p1x} ${p1y} C ${p2x} ${p2y} ${p3x} ${p3y} ${p4x} ${p4y}`, toArrow, fromArrow, obj.style)

  // Use extracted common function to calculate midpoint
  const { x: halfx, y: halfy } = calcBezierMidPoint(p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y)
  const labelColor = obj.style?.labelColor
  const label = createSvgText(obj.label, halfx, halfy, {
    anchor: 'middle',
    color: labelColor,
    dataType: 'custom-link',
  })
  newSvgGroup.appendChild(label)
  newSvgGroup.label = label

  newSvgGroup.arrowObj = obj
  newSvgGroup.dataset.linkid = obj.id
  mei.linkSvgGroup.appendChild(newSvgGroup)
  if (!isInitPaint) {
    mei.arrows.push(obj)
    mei.currentArrow = newSvgGroup
    showLinkController(mei, obj, fromData, toData)
  }
}

export const createArrow = function (this: MindElixirInstance, from: Topic, to: Topic, options: ArrowOptions = {}) {
  const arrowObj = {
    id: generateUUID(),
    label: 'Custom Link',
    from: from.nodeObj.id,
    to: to.nodeObj.id,
    delta1: {
      x: from.offsetWidth / 2 + 100,
      y: 0,
    },
    delta2: {
      x: to.offsetWidth / 2 + 100,
      y: 0,
    },
    ...options,
  }
  drawArrow(this, from, to, arrowObj)

  this.bus.fire('operation', {
    name: 'createArrow',
    obj: arrowObj,
  })
}

export const createArrowFrom = function (this: MindElixirInstance, arrow: Omit<Arrow, 'id'>) {
  hideLinkController(this)
  const arrowObj = { ...arrow, id: generateUUID() }
  drawArrow(this, this.findEle(arrowObj.from), this.findEle(arrowObj.to), arrowObj)

  this.bus.fire('operation', {
    name: 'createArrow',
    obj: arrowObj,
  })
}

export const removeArrow = function (this: MindElixirInstance, linkSvg?: CustomSvg) {
  let link
  if (linkSvg) {
    link = linkSvg
  } else {
    link = this.currentArrow
  }
  if (!link) return
  hideLinkController(this)
  const id = link.arrowObj!.id
  this.arrows = this.arrows.filter(arrow => arrow.id !== id)
  link.remove()
  this.bus.fire('operation', {
    name: 'removeArrow',
    obj: {
      id,
    },
  })
}

export const selectArrow = function (this: MindElixirInstance, link: CustomSvg) {
  this.currentArrow = link
  const obj = link.arrowObj

  const from = this.findEle(obj.from)
  const to = this.findEle(obj.to)

  const fromData = calcCtrlP(this, from, obj.delta1)
  const toData = calcCtrlP(this, to, obj.delta2)

  showLinkController(this, obj, fromData, toData)
}

export const unselectArrow = function (this: MindElixirInstance) {
  hideLinkController(this)
  this.currentArrow = null
}

/**
 * Create a highlight path element with common attributes
 */
const createHighlightPath = function (d: string, highlightColor: string): SVGPathElement {
  const path = document.createElementNS(svgNS, 'path')
  setAttributes(path, {
    d,
    stroke: highlightColor,
    fill: 'none',
    'stroke-width': '6',
    'stroke-linecap': 'round',
    'stroke-linejoin': 'round',
  })
  return path
}

const addArrowHighlight = function (arrow: CustomSvg, highlightColor: string) {
  const highlightGroup = document.createElementNS(svgNS, 'g')
  highlightGroup.setAttribute('class', 'arrow-highlight')
  highlightGroup.setAttribute('opacity', '0.45')

  const highlightLine = createHighlightPath(arrow.line.getAttribute('d')!, highlightColor)
  highlightGroup.appendChild(highlightLine)

  const highlightArrow1 = createHighlightPath(arrow.arrow1.getAttribute('d')!, highlightColor)
  highlightGroup.appendChild(highlightArrow1)

  if (arrow.arrow2.getAttribute('d')) {
    const highlightArrow2 = createHighlightPath(arrow.arrow2.getAttribute('d')!, highlightColor)
    highlightGroup.appendChild(highlightArrow2)
  }

  arrow.insertBefore(highlightGroup, arrow.firstChild)
}

const removeArrowHighlight = function (arrow: CustomSvg) {
  const highlightGroup = arrow.querySelector('.arrow-highlight')
  if (highlightGroup) {
    highlightGroup.remove()
  }
}

const updateArrowHighlight = function (arrow: CustomSvg) {
  const highlightGroup = arrow.querySelector('.arrow-highlight')
  if (!highlightGroup) return

  const highlightPaths = highlightGroup.querySelectorAll('path')
  if (highlightPaths.length >= 1) {
    highlightPaths[0].setAttribute('d', arrow.line.getAttribute('d')!)
  }
  if (highlightPaths.length >= 2) {
    highlightPaths[1].setAttribute('d', arrow.arrow1.getAttribute('d')!)
  }
  if (highlightPaths.length >= 3 && arrow.arrow2.getAttribute('d')) {
    highlightPaths[2].setAttribute('d', arrow.arrow2.getAttribute('d')!)
  }
}

const hideLinkController = function (mei: MindElixirInstance) {
  mei.helper1?.destroy!()
  mei.helper2?.destroy!()
  mei.linkController.style.display = 'none'
  mei.P2.style.display = 'none'
  mei.P3.style.display = 'none'
  if (mei.currentArrow) {
    removeArrowHighlight(mei.currentArrow)
  }
}

const showLinkController = function (mei: MindElixirInstance, linkItem: Arrow, fromData: DivData, toData: DivData) {
  const { linkController, P2, P3, line1, line2, nodes, map, currentArrow, bus } = mei
  if (!currentArrow) return
  linkController.style.display = 'initial'
  P2.style.display = 'initial'
  P3.style.display = 'initial'
  nodes.appendChild(linkController)
  nodes.appendChild(P2)
  nodes.appendChild(P3)

  addArrowHighlight(currentArrow, highlightColor)

  // init points
  let { x: p1x, y: p1y } = calcP(fromData)
  let { ctrlX: p2x, ctrlY: p2y } = fromData
  let { ctrlX: p3x, ctrlY: p3y } = toData
  let { x: p4x, y: p4y } = calcP(toData)

  P2.style.cssText = `top:${p2y}px;left:${p2x}px;`
  P3.style.cssText = `top:${p3y}px;left:${p3x}px;`
  updateControlLine(line1, p1x, p1y, p2x, p2y)
  updateControlLine(line2, p3x, p3y, p4x, p4y)

  mei.helper1 = LinkDragMoveHelper.create(P2)
  mei.helper2 = LinkDragMoveHelper.create(P3)

  mei.helper1.init(map, (deltaX, deltaY) => {
    // recalc key points
    p2x = p2x + deltaX / mei.scaleVal // scale should keep the latest value
    p2y = p2y + deltaY / mei.scaleVal
    const p1 = calcP({ ...fromData, ctrlX: p2x, ctrlY: p2y })
    p1x = p1.x
    p1y = p1.y

    // update dom position
    P2.style.top = p2y + 'px'
    P2.style.left = p2x + 'px'

    // Use extracted common function to update arrow
    updateArrowPath(currentArrow, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, linkItem)
    updateControlLine(line1, p1x, p1y, p2x, p2y)

    linkItem.delta1.x = p2x - fromData.cx
    linkItem.delta1.y = p2y - fromData.cy

    bus.fire('updateArrowDelta', linkItem)
  })

  mei.helper2.init(map, (deltaX, deltaY) => {
    p3x = p3x + deltaX / mei.scaleVal
    p3y = p3y + deltaY / mei.scaleVal
    const p4 = calcP({ ...toData, ctrlX: p3x, ctrlY: p3y })
    p4x = p4.x
    p4y = p4.y

    P3.style.top = p3y + 'px'
    P3.style.left = p3x + 'px'

    // Use extracted common function to update arrow
    updateArrowPath(currentArrow, p1x, p1y, p2x, p2y, p3x, p3y, p4x, p4y, linkItem)
    updateControlLine(line2, p3x, p3y, p4x, p4y)

    linkItem.delta2.x = p3x - toData.cx
    linkItem.delta2.y = p3y - toData.cy

    bus.fire('updateArrowDelta', linkItem)
  })
}

export function renderArrow(this: MindElixirInstance) {
  this.linkSvgGroup.innerHTML = ''
  for (let i = 0; i < this.arrows.length; i++) {
    const link = this.arrows[i]
    try {
      drawArrow(this, this.findEle(link.from), this.findEle(link.to), link, true)
    } catch (e) {
      console.warn('Node may not be expanded')
    }
  }
  this.nodes.appendChild(this.linkSvgGroup)
}

export function editArrowLabel(this: MindElixirInstance, el: CustomSvg) {
  hideLinkController(this)
  console.time('editSummary')
  if (!el) return
  const textEl = el.label
  editSvgText(this, textEl, el.arrowObj)
  console.timeEnd('editSummary')
}

export function tidyArrow(this: MindElixirInstance) {
  this.arrows = this.arrows.filter(arrow => {
    return getObjById(arrow.from, this.nodeData) && getObjById(arrow.to, this.nodeData)
  })
}
