<template>
  <div class="trace-flow-wrapper tree-mode">
    <div class="main-content">
      <Button
        class="absolute top-[0px] right-[16px] z-10"
        variant="outline"
        @click="(fitGraph(), WT1iser.resetView(ViewMode.Tree))"
      >
        Reset view
      </Button>
      <div ref="cyContainer" class="cy-container"></div>
      <div v-if="tooltip.visible" :style="tooltipAnchorStyle as any">
        <div v-html="tooltip.html"></div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import cytoscape from 'cytoscape'
import { onMounted, ref, watch, onBeforeUnmount, computed } from 'vue'

import { ViewMode } from '@/common/views'
import { WT1iser } from '@/common/wt1'
import { Button } from '@/components/ui/button'
import { useTracesStore } from '@/stores/traces'
import { dateToTimeShort } from '@/utils/formatDate'
import { getGroupBackground, getGroupColor, getGroupShape } from '@/utils/nodeGroups'
import { traceToFullHierarchy } from '@/utils/traceToGraph'

function parseTimeValue(str: string) {
  if (!str) return NaN
  const t = new Date(str).getTime()
  return isNaN(t) ? NaN : t
}

function computeDepths(nodes: any[], edges: any[]) {
  const childrenMap: any = {}
  edges.forEach((edge: any) => {
    const parentId = edge.data.source
    const childId = edge.data.target
    if (!childrenMap[parentId]) {
      childrenMap[parentId] = []
    }
    childrenMap[parentId].push(childId)
  })
  const allIds = new Set(nodes.map((n: any) => n.data.id))
  const childIds = new Set(edges.map((e: any) => e.data.target))
  const rootIds = [...allIds].filter((id: any) => !childIds.has(id))
  const depthMap: any = {}

  rootIds.forEach((rootId: any) => {
    depthMap[rootId] = 0
    const queue = [rootId]
    while (queue.length) {
      const current = queue.shift()
      const currentDepth = depthMap[current]
      const children = childrenMap[current] || []
      children.forEach((childId: any) => {
        if (depthMap[childId] == null) {
          depthMap[childId] = currentDepth + 1
          queue.push(childId)
        }
      })
    }
  })

  nodes.forEach((n: any) => {
    if (depthMap[n.data.id] == null) {
      depthMap[n.data.id] = 0
    }
  })

  return depthMap
}

function buildParentAndChildrenMaps(edges: any[]) {
  const parentMap: any = {}
  const childrenMap: any = {}
  edges.forEach((e: any) => {
    const p = e.data.source
    const c = e.data.target
    parentMap[c] = p
    if (!childrenMap[p]) {
      childrenMap[p] = []
    }
    childrenMap[p].push(c)
  })
  return { parentMap, childrenMap }
}

function createPositions(nodes: any[], depthMap: any, edges: any[]) {
  const positions: any = {}
  const TIME_SPACING = 70
  const Y_SPACING = 150
  const NO_TIME_SHIFT = 50

  const { parentMap } = buildParentAndChildrenMaps(edges)

  const hasTime = []
  const noTime = []

  for (const n of nodes) {
    const tVal = parseTimeValue(n.data.begin)
    if (isNaN(tVal)) {
      noTime.push(n)
    } else {
      hasTime.push(n)
    }
  }

  hasTime.sort((a, b) => {
    const aBegin = parseTimeValue(a.data.begin) || 0
    const bBegin = parseTimeValue(b.data.begin) || 0
    if (aBegin !== bBegin) {
      return aBegin - bBegin
    }
    const aDepth = depthMap[a.data.id] || 0
    const bDepth = depthMap[b.data.id] || 0
    if (aDepth !== bDepth) {
      return aDepth - bDepth
    }
    const aLabel = a.data.fullLabel || a.data.label || ''
    const bLabel = b.data.fullLabel || b.data.label || ''
    return aLabel.localeCompare(bLabel)
  })

  let timeIndex = 0
  for (const node of hasTime) {
    const id = node.data.id
    const depth = depthMap[id] || 0
    const x = timeIndex * TIME_SPACING
    const y = depth * Y_SPACING
    positions[id] = { x, y }
    timeIndex++
  }

  noTime.sort((a, b) => {
    const aDepth = depthMap[a.data.id] || 0
    const bDepth = depthMap[b.data.id] || 0
    if (aDepth !== bDepth) {
      return aDepth - bDepth
    }
    const aLabel = a.data.fullLabel || a.data.label || ''
    const bLabel = b.data.fullLabel || b.data.label || ''
    return aLabel.localeCompare(bLabel)
  })

  const parentNoTimeCount: any = {}
  for (const node of noTime) {
    const id = node.data.id
    const pId = parentMap[id]
    const depth = depthMap[id] || 0
    const y = depth * Y_SPACING
    let x = 0

    if (pId && positions[pId]) {
      if (!parentNoTimeCount[pId]) {
        parentNoTimeCount[pId] = 0
      }
      const siblingIndex = parentNoTimeCount[pId]
      let offset = 0
      if (siblingIndex > 0) {
        const sign = siblingIndex % 2 === 0 ? -1 : 1
        const magnitude = Math.ceil(siblingIndex / 2)
        offset = sign * magnitude * NO_TIME_SHIFT
      }
      x = positions[pId].x + offset
      parentNoTimeCount[pId]++
    }

    positions[id] = { x, y }
  }

  return positions
}

const tracesStore = useTracesStore()
const cyContainer = ref(null)
const cyInstance = ref<any>(null)
const tooltip = ref({ visible: false, html: '', x: 0, y: 0 })

let lastTapTime = 0
let lastTappedNodeId: any = null
const doubleClickThreshold = 300
let singleClickTimer: any = null
const savedPositions: any = {}
const tooltipAnchorStyle = computed(() => ({
  position: 'fixed',
  left: tooltip.value.x + 'px',
  top: tooltip.value.y + 'px',
  width: '0',
  height: '0'
}))

onMounted(() => {
  renderGraph()
})

onBeforeUnmount(() => {
  if (cyInstance.value) {
    cyInstance.value.destroy()
    cyInstance.value = null
  }
})

watch(
  () => tracesStore.selectedNodeId,
  (selectedNodeId) => {
    highlightSelectedNode(selectedNodeId)
  },
  { immediate: true }
)

watch(
  () => tracesStore.selectedTrace,
  (newVal, oldVal) => {
    if (newVal && newVal !== oldVal) {
      renderGraph()
    } else if (!newVal) {
      if (cyInstance.value) {
        cyInstance.value.destroy()
        cyInstance.value = null
      }
    }
  },
  { deep: true }
)

function highlightSelectedNode(nodeId: any) {
  if (!cyInstance.value) return
  cyInstance.value.$('node').removeClass('selected-node')
  if (!nodeId) return
  const node = cyInstance.value.$(`node[id="${nodeId}"]`)
  if (node.nonempty()) {
    node.addClass('selected-node')
  }
}

function renderGraph() {
  if (!tracesStore.selectedTrace?.parentNode || !cyContainer.value) return

  const { nodes, edges, minDuration, maxDuration } = traceToFullHierarchy(
    tracesStore.selectedTrace?.parentNode
  )
  const depthMap = computeDepths(nodes, edges)
  const positions = createPositions(nodes, depthMap, edges)

  if (cyInstance.value) {
    cyInstance.value.destroy()
  }

  cyInstance.value = cytoscape({
    container: cyContainer.value,
    elements: [...nodes, ...edges],
    style: getStyle(minDuration, maxDuration) as any,
    zoom: 1,
    minZoom: 0.15, // to avoid zooming out too far
    layout: {
      name: 'preset',
      positions: (ele: any) => positions[ele.id()],
      fit: true,
      padding: 50
    }
  })

  const cy = cyInstance.value

  cy.on('mouseover', 'node', (evt: any) => {
    cy.container().style.cursor = 'pointer'
    buildAndShowTooltip(evt, evt.target.data())
  })

  cy.on('mouseout', 'node', () => {
    cy.container().style.cursor = ''
    hideTooltip()
  })

  cy.on('tap', 'node', handleNodeTap)
  cy.on('tapstart', () => hideTooltip())

  if (tracesStore.selectedNodeId) {
    highlightSelectedNode(tracesStore.selectedNodeId)
  }
}

function fitGraph() {
  if (cyInstance.value) {
    cyInstance.value.fit(50)
    tracesStore.resetNodeFilters()
  }
}

function handleNodeTap(evt: any) {
  const node = evt.target
  const now = Date.now()
  const isEmptyNode = node.data().emptyNode
  const nodeId = node.id() || null

  if (singleClickTimer) {
    clearTimeout(singleClickTimer)
    singleClickTimer = null
  }

  if (now - lastTapTime < doubleClickThreshold && lastTappedNodeId === nodeId) {
    toggleCollapseExpand(node)
  } else {
    singleClickTimer = setTimeout(() => {
      if (!isEmptyNode && nodeId) {
        tracesStore.selectedNodeId = nodeId
      }
    }, doubleClickThreshold)
  }

  lastTapTime = now
  lastTappedNodeId = nodeId
}

function toggleCollapseExpand(node: any) {
  const data = node.data()
  if (!data.expandable) return
  const cy = cyInstance.value
  if (!cy) return

  if (data.collapsed === false) {
    storeChildrenPositions(data._childElements?.nodes || [])
    if (data._childElements && data._childElements.nodes) {
      const allIds = data._childElements.nodes
        .map((n: any) => '#' + n.data.id)
        .concat(data._childElements.edges.map((e: any) => '#' + e.data.id))
      cy.remove(allIds.join(','))
    }
    data.collapsed = true
    node.data(data)
  } else {
    if (data._childElements && data._childElements.nodes) {
      cy.add(data._childElements.nodes)
      cy.add(data._childElements.edges)
      data.collapsed = false
      node.data(data)
      restoreChildrenPositions(data._childElements.nodes)
    }
  }
  cy.resize()
  cy.fit({ padding: 50 })
}

function storeChildrenPositions(childNodes: any[]) {
  childNodes.forEach((child) => {
    const n = cyInstance.value.$('#' + child.data.id)
    if (n.nonempty()) {
      savedPositions[child.data.id] = {
        x: n.position('x'),
        y: n.position('y')
      }
    }
  })
}

function restoreChildrenPositions(childNodes: any[]) {
  childNodes.forEach((child) => {
    const pos = savedPositions[child.data.id]
    if (pos) {
      const n = cyInstance.value.$('#' + child.data.id)
      if (n.nonempty()) {
        n.position({ x: pos.x, y: pos.y })
      }
    }
  })
}

function buildAndShowTooltip(evt: any, data: any) {
  const beginVal = findAttributeVal(data.details?.attributes, 'Begin')
  const endVal = findAttributeVal(data.details?.attributes, 'End')
  const durationVal = findAttributeVal(data.details?.attributes, 'Duration')
  const name = data.fullLabel || removeChildCount(data.label) || '(No name)'
  const emptyNode = data.emptyNode

  let tooltipHtml = `<div class="inline-block p-2 px-3 bg-secondary text-xs rounded-sm">
    <span class="font-semibold">${name || '(no name)'}</span>`

  if (beginVal) {
    const beginDate = new Date(beginVal)
    const beginFormatted = beginDate ? dateToTimeShort(beginDate) : beginVal
    tooltipHtml += `<div class="flex justify-between text-sm mt-2">
      Start: <span class="font-mono text-xs relative top-[3.5px] ml-5">${beginFormatted}</span>
    </div>`
  }

  if (endVal) {
    const endDate = new Date(endVal)
    const endFormatted = endDate ? dateToTimeShort(endDate) : endVal
    tooltipHtml += `<div class="flex justify-between text-sm">
      End: <span class="font-mono text-xs relative top-[3.5px] ml-5">${endFormatted}</span>
    </div>`
  }

  if (durationVal) {
    tooltipHtml += `<div class="flex justify-between text-sm">
      Duration: <span class="font-mono text-xs relative top-[3.5px] ml-5">${durationVal}</span>
    </div>`
  }

  if (emptyNode) {
    tooltipHtml += `<div class="flex justify-between text-sm mt-2">
      <span class="font-mono text-xs">
        <strong>No additional details available</strong>
      </span>
    </div>`
  }

  tooltipHtml += `</div>`
  showTooltip(evt.originalEvent.clientX + 10, evt.originalEvent.clientY + 10, tooltipHtml)
}

function findAttributeVal(arr: any[], key: string) {
  if (!arr) return null

  const found = arr.find((x) => x.key === key)
  return found ? found.value : null
}

function removeChildCount(label: string) {
  return label.replace(/\(\d+\)$/, '').trim()
}

function showTooltip(x: number, y: number, html: string) {
  tooltip.value = {
    visible: true,
    html,
    x,
    y
  }
}

function hideTooltip() {
  tooltip.value.visible = false
}

function getStyle(minDur: number, maxDur: number) {
  return [
    {
      selector: 'node',
      style: {
        'background-image': (ele: any) => {
          const g = ele.data().group
          const empty = ele.data().emptyNode
          return getGroupBackground(g, empty)
        },
        shape: (ele: any) => {
          const g = ele.data().group
          return getGroupShape(g)
        },
        width: (ele: any) => scaleDuration(ele.data().rawDuration, minDur, maxDur),
        height: (ele: any) => scaleDuration(ele.data().rawDuration, minDur, maxDur),
        label: 'data(label)',
        color: '#333333',
        'font-size': '12px',
        'text-wrap': 'wrap',
        'text-max-width': '200px',
        'text-valign': 'top',
        'text-halign': 'center',
        'text-overflow-wrap': 'anywhere',
        'line-height': 1.3,
        'text-background-color': '#f0f4ff',
        'text-background-opacity': 0.85,
        'text-background-padding': '5px',
        'text-background-shape': 'roundrectangle',
        'text-outline-color': '#fff',
        'text-outline-width': 1,
        'text-margin-y': '-10px',

        'border-color': (ele: any) => {
          const g = ele.data().group
          return getGroupColor(g)
        },
        'border-width': (ele: any) => (ele.data().emptyNode ? 0.8 : 0),
        'border-style': (ele: any) => (ele.data().emptyNode ? 'dotted' : 'solid'),
        padding: '6px'
      }
    },
    {
      selector: 'edge',
      style: {
        width: 1,
        'line-color': '#333',
        'target-arrow-color': '#333',
        'target-arrow-shape': 'chevron',
        'arrow-scale': 1,
        'curve-style': 'taxi',
        'taxi-direction': 'downward',
        'taxi-turn': 20,
        'taxi-turn-min-distance': 10
      }
    },
    {
      selector: ':selected',
      style: {
        'border-width': (ele: any) => (ele.data().emptyNode ? 1 : 5),
        'border-color': '#4578fc',
        'border-style': 'solid'
      }
    },
    {
      selector: '.selected-node',
      style: {
        'border-width': (ele: any) => (ele.data().emptyNode ? 1 : 5),
        'border-color': '#4578fc',
        'border-style': 'solid'
      }
    }
  ]
}

function scaleDuration(durationMs: number, minD: number, maxD: number) {
  const minSize = 10
  const maxSize = 80
  if (!durationMs || maxD <= minD) {
    return minSize
  }
  const clamped = Math.max(durationMs, minD)
  const ratio = (clamped - minD) / (maxD - minD)
  const size = minSize + ratio * (maxSize - minSize)
  return Math.min(Math.max(size, minSize), maxSize)
}
</script>

<style scoped>
.trace-flow-wrapper {
  position: relative;
  background: #ffffff;
  padding: 0px;
  color: #333e48;
  overflow: visible;
}

.trace-flow-wrapper.tree-mode {
  height: 100%;
}

.main-content {
  position: relative;
  height: 100%;
  overflow: visible;
}

.cy-container {
  width: 100%;
  height: 100%;
  background: #ffffff;
  position: relative;
  overflow: visible;
}
</style>
