<script setup lang="ts">
import { remove } from 'es-toolkit'
import { computed, provide, ref, toValue } from 'vue'
import type { MaybeRef } from 'vue'
import { useI18n } from 'vue-i18n'

import DraggableList from '@/components/common/DraggableList.vue'
import IoItem from '@/components/builder/IoItem.vue'
import PropertiesAccordionItem from '@/components/rightSidePanel/layout/PropertiesAccordionItem.vue'
import WidgetItem from '@/components/rightSidePanel/parameters/WidgetItem.vue'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import { TitleMode } from '@/lib/litegraph/src/types/globalEnums'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { BaseWidget } from '@/lib/litegraph/src/widgets/BaseWidget'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'
import { app } from '@/scripts/app'
import { DOMWidgetImpl } from '@/scripts/domWidget'
import { useDialogService } from '@/services/dialogService'
import { useAppMode } from '@/composables/useAppMode'
import { useAppModeStore } from '@/stores/appModeStore'
import { resolveNode } from '@/utils/litegraphUtil'
import { cn } from '@/utils/tailwindUtil'
import { HideLayoutFieldKey } from '@/types/widgetTypes'

type BoundStyle = { top: string; left: string; width: string; height: string }

const appModeStore = useAppModeStore()
const canvasInteractions = useCanvasInteractions()
const canvasStore = useCanvasStore()
const settingStore = useSettingStore()
const workflowStore = useWorkflowStore()
const { t } = useI18n()
const canvas: LGraphCanvas = canvasStore.getCanvas()

const { isSelectMode, isSelectInputsMode, isSelectOutputsMode, isArrangeMode } =
  useAppMode()
const hoveringSelectable = ref(false)

provide(HideLayoutFieldKey, true)

workflowStore.activeWorkflow?.changeTracker?.reset()

const arrangeInputs = computed(() =>
  appModeStore.selectedInputs
    .map(([nodeId, widgetName]) => {
      const node = resolveNode(nodeId)
      if (!node) return null
      const widget = node.widgets?.find((w) => w.name === widgetName)
      return { nodeId, widgetName, node, widget }
    })
    .filter((item): item is NonNullable<typeof item> => item !== null)
)

const inputsWithState = computed(() =>
  appModeStore.selectedInputs.map(([nodeId, widgetName]) => {
    const node = resolveNode(nodeId)
    const widget = node?.widgets?.find((w) => w.name === widgetName)
    if (!node || !widget) {
      return {
        nodeId,
        widgetName,
        subLabel: t('linearMode.builder.unknownWidget')
      }
    }

    const input = node.inputs.find((i) => i.widget?.name === widget.name)
    const rename = input && (() => renameWidget(widget, input))

    return {
      nodeId,
      widgetName,
      label: widget.label,
      subLabel: node.title,
      rename
    }
  })
)
const outputsWithState = computed<[NodeId, string][]>(() =>
  appModeStore.selectedOutputs.map((nodeId) => [
    nodeId,
    app.rootGraph.getNodeById(nodeId)?.title ?? String(nodeId)
  ])
)

async function renameWidget(widget: IBaseWidget, input: INodeInputSlot) {
  const newLabel = await useDialogService().prompt({
    title: t('g.rename'),
    message: t('g.enterNewNamePrompt'),
    defaultValue: widget.label,
    placeholder: widget.name
  })
  if (newLabel === null) return
  widget.label = newLabel || undefined
  input.label = newLabel || undefined
  widget.callback?.(widget.value)
  useCanvasStore().canvas?.setDirty(true)
}

function getHovered(
  e: MouseEvent
): undefined | [LGraphNode, undefined] | [LGraphNode, IBaseWidget] {
  const { graph } = canvas
  if (!canvas || !graph) return

  if (settingStore.get('Comfy.VueNodes.Enabled')) return undefined
  if (!e) return

  canvas.adjustMouseEvent(e)
  const node = graph.getNodeOnPos(e.canvasX, e.canvasY)
  if (!node) return

  const widget = node.getWidgetOnPos(e.canvasX, e.canvasY, false)

  if (widget || node.constructor.nodeData?.output_node) return [node, widget]
}

function getBounding(nodeId: NodeId, widgetName?: string) {
  if (settingStore.get('Comfy.VueNodes.Enabled')) return undefined
  const node = app.rootGraph.getNodeById(nodeId)
  if (!node) return

  const titleOffset =
    node.title_mode === TitleMode.NORMAL_TITLE ? LiteGraph.NODE_TITLE_HEIGHT : 0

  if (!widgetName)
    return {
      width: `${node.size[0]}px`,
      height: `${node.size[1] + titleOffset}px`,
      left: `${node.pos[0]}px`,
      top: `${node.pos[1] - titleOffset}px`
    }
  const widget = node.widgets?.find((w) => w.name === widgetName)
  if (!widget) return

  const margin = widget instanceof DOMWidgetImpl ? widget.margin : undefined
  const marginX = margin ?? BaseWidget.margin
  const height =
    (widget.computedHeight !== undefined
      ? widget.computedHeight - 4
      : LiteGraph.NODE_WIDGET_HEIGHT) - (margin ? 2 * margin - 4 : 0)
  return {
    width: `${node.size[0] - marginX * 2}px`,
    height: `${height}px`,
    left: `${node.pos[0] + marginX}px`,
    top: `${node.pos[1] + widget.y + (margin ?? 0)}px`
  }
}

function handleDown(e: MouseEvent) {
  const [node] = getHovered(e) ?? []
  if (!node || e.button > 0) canvasInteractions.forwardEventToCanvas(e)
}
function handleClick(e: MouseEvent) {
  const [node, widget] = getHovered(e) ?? []
  if (!node) return canvasInteractions.forwardEventToCanvas(e)

  if (!widget) {
    if (!isSelectOutputsMode.value) return
    if (!node.constructor.nodeData?.output_node)
      return canvasInteractions.forwardEventToCanvas(e)
    const index = appModeStore.selectedOutputs.findIndex((id) => id == node.id)
    if (index === -1) appModeStore.selectedOutputs.push(node.id)
    else appModeStore.selectedOutputs.splice(index, 1)
    return
  }
  if (!isSelectInputsMode.value) return

  const index = appModeStore.selectedInputs.findIndex(
    ([nodeId, widgetName]) => node.id == nodeId && widget.name === widgetName
  )
  if (index === -1) appModeStore.selectedInputs.push([node.id, widget.name])
  else appModeStore.selectedInputs.splice(index, 1)
}

function nodeToDisplayTuple(
  n: LGraphNode
): [NodeId, MaybeRef<BoundStyle> | undefined, boolean] {
  return [
    n.id,
    getBounding(n.id),
    appModeStore.selectedOutputs.some((id) => n.id === id)
  ]
}

const renderedOutputs = computed(() => {
  void appModeStore.selectedOutputs.length
  return canvas
    .graph!.nodes.filter((n) => n.constructor.nodeData?.output_node)
    .map(nodeToDisplayTuple)
})
const renderedInputs = computed<[string, MaybeRef<BoundStyle> | undefined][]>(
  () =>
    appModeStore.selectedInputs.map(([nodeId, widgetName]) => [
      `${nodeId}: ${widgetName}`,
      getBounding(nodeId, widgetName)
    ])
)
</script>
<template>
  <div class="flex h-full flex-col">
    <div class="flex items-center border-b border-border-subtle p-2 font-bold">
      {{
        isArrangeMode ? t('nodeHelpPage.inputs') : t('linearMode.builder.title')
      }}
    </div>
    <div class="min-h-0 flex-1 overflow-y-auto">
      <DraggableList
        v-if="isArrangeMode"
        v-slot="{ dragClass }"
        v-model="appModeStore.selectedInputs"
      >
        <div
          v-for="{ nodeId, widgetName, node, widget } in arrangeInputs"
          :key="`${nodeId}: ${widgetName}`"
          :class="cn(dragClass, 'pointer-events-auto my-2 p-2')"
          :aria-label="`${widget?.label ?? widgetName} — ${node.title}`"
        >
          <div v-if="widget" class="pointer-events-none" inert>
            <WidgetItem
              :widget="widget"
              :node="node"
              show-node-name
              hidden-widget-actions
            />
          </div>
          <div
            v-else
            class="pointer-events-none p-1 text-sm text-muted-foreground"
          >
            {{ widgetName }}
            <p class="text-xs italic">
              ({{ t('linearMode.builder.unknownWidget') }})
            </p>
          </div>
        </div>
      </DraggableList>
      <PropertiesAccordionItem
        v-if="isSelectInputsMode"
        :label="t('nodeHelpPage.inputs')"
        enable-empty-state
        :disabled="!appModeStore.selectedInputs.length"
        class="border-b border-border-subtle"
        :tooltip="`${t('linearMode.builder.inputsDesc')}\n${t('linearMode.builder.inputsExample')}`"
        :tooltip-delay="100"
      >
        <template #label>
          <div class="flex gap-3">
            {{ t('nodeHelpPage.inputs') }}
            <i class="icon-[lucide--circle-alert] bg-muted-foreground" />
          </div>
        </template>
        <template #empty>
          <div
            class="w-full p-4 pt-2 text-muted-foreground"
            v-text="t('linearMode.builder.promptAddInputs')"
          />
        </template>
        <div
          class="w-full p-4 pt-2 text-muted-foreground"
          v-text="t('linearMode.builder.promptAddInputs')"
        />
        <DraggableList
          v-slot="{ dragClass }"
          v-model="appModeStore.selectedInputs"
        >
          <IoItem
            v-for="{
              nodeId,
              widgetName,
              label,
              subLabel,
              rename
            } in inputsWithState"
            :key="`${nodeId}: ${widgetName}`"
            :class="
              cn(dragClass, 'my-2 rounded-lg bg-primary-background/30 p-2')
            "
            :title="label ?? widgetName"
            :sub-title="subLabel"
            :rename
            :remove="
              () =>
                remove(
                  appModeStore.selectedInputs,
                  ([id, name]) => nodeId == id && widgetName === name
                )
            "
          />
        </DraggableList>
      </PropertiesAccordionItem>
      <PropertiesAccordionItem
        v-if="isSelectOutputsMode"
        :label="t('nodeHelpPage.outputs')"
        enable-empty-state
        :disabled="!appModeStore.selectedOutputs.length"
        :tooltip="`${t('linearMode.builder.outputsDesc')}\n${t('linearMode.builder.outputsExample')}`"
        :tooltip-delay="100"
      >
        <template #label>
          <div class="flex gap-3">
            {{ t('nodeHelpPage.outputs') }}
            <i class="icon-[lucide--circle-alert] bg-muted-foreground" />
          </div>
        </template>
        <template #empty>
          <div
            class="w-full p-4 pt-2 text-muted-foreground"
            v-text="t('linearMode.builder.promptAddOutputs')"
          />
        </template>
        <div
          class="w-full p-4 pt-2 text-muted-foreground"
          v-text="t('linearMode.builder.promptAddOutputs')"
        />
        <DraggableList
          v-slot="{ dragClass }"
          v-model="appModeStore.selectedOutputs"
        >
          <IoItem
            v-for="([key, title], index) in outputsWithState"
            :key
            :class="
              cn(
                dragClass,
                'my-2 rounded-lg bg-warning-background/40 p-2',
                index === 0 && 'ring-2 ring-warning-background'
              )
            "
            :title
            :sub-title="String(key)"
            :remove="
              () => remove(appModeStore.selectedOutputs, (k) => k == key)
            "
          />
        </DraggableList>
      </PropertiesAccordionItem>
    </div>
  </div>

  <Teleport
    v-if="isSelectMode && !settingStore.get('Comfy.VueNodes.Enabled')"
    to="body"
  >
    <div
      :class="
        cn(
          'pointer-events-auto absolute size-full',
          hoveringSelectable ? 'cursor-pointer' : 'cursor-grab'
        )
      "
      @pointerdown="handleDown"
      @pointermove="hoveringSelectable = !!getHovered($event)"
      @click="handleClick"
      @wheel="canvasInteractions.forwardEventToCanvas"
    >
      <TransformPane :canvas="canvasStore.getCanvas()">
        <template v-if="isSelectInputsMode">
          <div
            v-for="[key, style] in renderedInputs"
            :key
            :style="toValue(style)"
            class="fixed rounded-lg bg-primary-background/30"
          />
        </template>
        <template v-else>
          <div
            v-for="[key, style, isSelected] in renderedOutputs"
            :key
            :style="toValue(style)"
            :class="
              cn(
                'fixed rounded-2xl ring-5 ring-warning-background',
                !isSelected && 'ring-warning-background/50'
              )
            "
          >
            <div class="absolute top-0 right-0 size-8">
              <div
                v-if="isSelected"
                class="pointer-events-auto absolute -top-1/2 -right-1/2 size-full cursor-pointer rounded-lg bg-warning-background p-2"
                @click.stop="
                  remove(appModeStore.selectedOutputs, (k) => k == key)
                "
                @pointerdown.stop
              >
                <i class="bg-text-foreground icon-[lucide--check] size-full" />
              </div>
              <div
                v-else
                class="pointer-events-auto absolute -top-1/2 -right-1/2 size-full cursor-pointer rounded-lg bg-component-node-background ring-4 ring-warning-background/50 ring-inset"
                @click.stop="appModeStore.selectedOutputs.push(key)"
                @pointerdown.stop
              />
            </div>
          </div>
        </template>
      </TransformPane>
    </div>
  </Teleport>
</template>
