"use client"

import {
  useCallback,
  useEffect,
  useMemo,
  useReducer,
  useRef,
  useState,
} from "react"
import ScaleComponent from "./components/ScaleComponent"

import { Box, Button, Flex, LoadingOverlay, Stack } from "@mantine/core"
import { notifications, showNotification } from "@mantine/notifications"
import paper from "paper"
import { getLabelResult, getServerImage } from "./api/label"
import {
  Comment,
  ImageObjects,
  LabelResult,
  WorkLoad,
} from "./api/label/typing"
import { getProjectDetail } from "./api/project"
import { Project } from "./api/project/typing"
import { getTaskList, getTaskWorkFlow } from "./api/task"
import { Task } from "./api/task/typing"
import BottomTools from "./components/BottomTools"
import ConfirmModal from "./components/ConfirmModal"
import { splitWord } from "./components/EditorContainer"
import PaperContainer from "./components/PaperContainer"
import RightDescTools from "./components/RightDescTools"
import RightGroupTools from "./components/RightGroupTools"
import RightObjectTools from "./components/RightObjectTools"
import RightQATools from "./components/RightQATools"
import RightTaskTools from "./components/RightTaskTools"
import ScaleToolContainer from "./components/ScaleToolContainer"
import TopTools from "./components/TopTools"
import { processVideo } from "./ffmpeg/processVideo"
import {
  useImagesStore,
  useKeyEventStore,
  useLabelStore,
  useObjectStore,
  useVideoFrameStore,
} from "./store"
import { usePermissionStore } from "./store/auth"
import { useBottomToolsStore } from "./useBottomToolsStore"
import { useDescToolsStore } from "./useDescToolsStore"
import { useKeyboardStore } from "./useKeyBoardStore"
import { usePaperStore } from "./usePaperStore"
import { useRightToolsStore } from "./useRightToolsStore"
import {
  useIntervalStore,
  useLabelTimeStore,
  useTimerStore,
} from "./useTimerStore"
import { useTopToolsStore } from "./useTopToolsStore"
import { findGroupKey } from "./util"
import { safeClone } from "./utils/clone"
import { labelTypeMap } from "./utils/constants"
import { toggleObjectHideByShortcut } from "./utils/objectVisibility"

// Splitter component - will need custom implementation or alternative

// 定义状态的类型
export type LabelState = Map<string, string[]>

// 定义操作的类型
type Action =
  | {
      type: "INIT_IMAGES"
      imageIds: string[]
      objectOperations: Project.LabelSchemaList[]
    }
  | { type: "REMOVE_IMAGE"; imageId: string }

// 定义初始状态
const initialState: LabelState = new Map()

const operationTypeList = [101, 102, 103, 104]
let latestProjectDetailRequestSeq = 0
let latestTaskDetailRequestSeq = 0
const RIGHT_PANEL_TRANSITION =
  "width 160ms ease, min-width 160ms ease, opacity 160ms ease"
const RIGHT_PANEL_WILL_CHANGE = "width, min-width, opacity"

// utils
const getRectPoints = ([sx, sy, w, h]: number[]) => {
  return [
    [sx, sy + h],
    [sx, sy],
    [sx + w, sy],
    [sx + w, sy + h],
  ]
}

const getSegmentPoints = (arr: Array<number[]>) => {
  return arr.map((item) => [item[0], item[1]])
}

const getRightPanelViewportStyle = (width: number) => {
  const stableWidth = Math.max(0, Math.round(width))
  return {
    position: "absolute" as const,
    top: 0,
    left: 0,
    width: stableWidth,
    minWidth: stableWidth,
    height: "100%",
    contain: "layout paint",
  }
}

const videoExtPattern = /\.(mp4|mov|avi|mkv|webm|h264|264|ts|m4v)$/i
const GLOBAL_LOADING_Z_INDEX = 900
const videoMimeByExt: Record<string, string> = {
  mp4: "video/mp4",
  mov: "video/quicktime",
  webm: "video/webm",
  mkv: "video/x-matroska",
  avi: "video/x-msvideo",
  h264: "video/h264",
  "264": "video/h264",
  ts: "video/mp2t",
  m4v: "video/x-m4v",
}
const streamingFrameArgs = [
  "-vf",
  "fps=1,scale='min(640,iw)':-2:flags=bilinear,format=yuv420p",
  "-q:v",
  "5",
  "frame_%03d.jpg",
]

const getFileExt = (fileName: string) => {
  const segments = fileName.split(".")
  if (segments.length <= 1) return ""
  return segments[segments.length - 1].toLowerCase()
}

const normalizeBase64 = (text: string) => {
  if (!text) return ""
  const raw = text.includes(",") ? text.split(",").slice(1).join(",") : text
  return raw.replace(/\s+/g, "")
}

const normalizeNamePrefix = (name: string) => {
  const value = name
    .replace(/[^a-zA-Z0-9_-]+/g, "_")
    .replace(/_+/g, "_")
    .replace(/^_|_$/g, "")
  return value || "video"
}

const isVideoTaskDetail = (detail: Task.DataProps) => {
  const sourceNames = detail.data_name
    .map((item) => item?.name?.[0] || "")
    .filter((name) => !!name)
  if (!sourceNames.length) return false
  const taskDataType = Number(
    (detail as { [key: string]: any })?.data_type ??
      (detail as { [key: string]: any })?.task_data_type ??
      -1
  )
  return (
    taskDataType === 2 ||
    taskDataType === 7 ||
    sourceNames.some((name) => videoExtPattern.test(name))
  )
}

interface ResultImageEntry {
  frameName: string
  imageObjects: ImageObjects
}

const expandLabelResultImages = (item: LabelResult): ResultImageEntry[] => {
  if (item.image_objects) {
    return [
      {
        frameName: item.data_name,
        imageObjects: item.image_objects,
      },
    ]
  }
  if (!item.video_objects) return []
  return Object.entries(item.video_objects).map(([frameName, imageObjects]) => {
    return {
      frameName,
      imageObjects,
    }
  })
}

interface LabelProps {
  headerHeight: number
  leftWidth: number
  project_id: number
  task_id: number
}

const LabelPage = ({
  headerHeight = 0,
  leftWidth = 0,
  project_id = -1,
  task_id = -1,
}: LabelProps) => {
  const projectId = useMemo(() => {
    return project_id
  }, [project_id])
  const taskId = useMemo(() => {
    return task_id
  }, [task_id])
  //   const searchParams = useSearchParams()
  //   const id1 = searchParams.get("project_id")
  //   const projectId =
  //     typeof id1 === "string" && !isNaN(Number(id1)) ? Number(id1) : 1
  //   const id2 = searchParams.get("task_id")
  //   const taskId =
  //     typeof id2 === "string" && !isNaN(Number(id2)) ? Number(id2) : 2

  const user_id = usePermissionStore.getState().user_id
  const [projectDetail, setProjectDetail] = useState<Project.DetailResponse>()
  const [taskDetail, setTaskDetail] = useState<Task.DataProps>()
  const [oldWorkLoad, setOldWorkLoad] = useState<WorkLoad | null>(null)
  // const [objectOperations, setObjectOperations] = useState<
  //   Project.LabelSchemaList[]
  // >([]);
  const topRef = useRef<any>(null)
  const autoSaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const autoSaveInProgressRef = useRef(false)
  const paperContainerRef = useRef<any>(null)
  const qaToolContainerRef = useRef<any>(null)
  const [isConfirmSave, setIsConfirmSave] = useState(false)
  const [loading, setLoading] = useState(false)
  const [sizes, setSizes] = useState<number[]>([])
  const [videoFetchPendingCount, setVideoFetchPendingCount] = useState(0)
  const [hasReadyVideoFrame, setHasReadyVideoFrame] = useState(false)
  const [sourceVideoNames, setSourceVideoNames] = useState<string[]>([])
  const [downloadingVideoName, setDownloadingVideoName] = useState<
    string | null
  >(null)

  const labelData = useLabelStore((state) => state.label)
  const setLabel = useLabelStore((state) => state.setLabel)
  const setStateStack = useLabelStore((state) => state.setStateStack)
  const selectedPathMap = useObjectStore((state) => state.selectedPath)
  const setLabelTime = useLabelTimeStore((state) => state.setLabelTime)
  const isAutoSave = useIntervalStore((state) => state.isAutoSave)
  const autoSaveGap = useIntervalStore((state) => state.autoSaveGap)

  const {
    editMode,
    setEditMode,
    pressA,
    setPressA,
    activeOperation,
    setActiveOperation,
    setDrawOption,
    showObjectList,
    showGroupList,
    showTaskList,
    showDescList,
    showTags,
    showTagsConfigs,
    showMultiFrame,
    setShowMultiFrame,
    isView,
    setIsView,
    renderMode,
    scale,
    setScale,
    objectOperations,
    setObjectOperations,
  } = useTopToolsStore()
  const { activeImage } = useBottomToolsStore()
  const {
    setDescOperations,
    metaOperation,
    setMetaOperation,
    setQaOperations,
    setQaData,
    resetData,
  } = useDescToolsStore()

  const asyncGetProjectDetail = useCallback(async () => {
    if (projectId) {
      const requestSeq = ++latestProjectDetailRequestSeq
      try {
        const res = await getProjectDetail(projectId)
        if (requestSeq !== latestProjectDetailRequestSeq) {
          console.warn("[LabelNossr] ignore stale project detail response", {
            projectId,
            requestSeq,
            latestRequestSeq: latestProjectDetailRequestSeq,
          })
          return
        }
        setProjectDetail(res)
        let operations: Project.LabelSchemaList[] = []
        let textOperations: Project.LabelSchemaList[] = []
        let metaOperation: Project.LabelSchemaList | null = null
        res.label_schema_list?.forEach((schema) => {
          if (operationTypeList.includes(schema.label_type)) {
            operations.push(schema)
          } else if ([201].includes(schema.label_type)) {
            if (schema.label_class === "元操作序列") metaOperation = schema
            else textOperations.push(schema)
          }
        })
        setObjectOperations(operations)
        if (res.label_type === 5) {
          setDescOperations(textOperations)
          if (metaOperation) setMetaOperation(metaOperation)
        } else {
          setDescOperations([])
          setMetaOperation(null)
        }
        if (res.label_type === 6) {
          let obj: any = {}
          textOperations.forEach((item) => {
            let arr = item.sub_attributes_describe.map((sub) => {
              return {
                [sub.chinese_name]: {
                  value: "",
                  id: sub.round_id,
                  is_pre: true,
                  tag: 0,
                  uid: 0,
                  create_timestamp: 0,
                  modify_uid: 0,
                  modify_timestamp: 0,
                  comment: "",
                  flag: 0,
                },
              }
            })
            obj[item.label_class] = arr
          })
          let map = new Map()
          map.set("init", obj)
          console.log(map)
          setQaData(map)
          setQaOperations(textOperations)
        } else {
          setQaOperations([])
        }
        resetData()
      } catch {}
    }
  }, [
    projectId,
    resetData,
    setDescOperations,
    setMetaOperation,
    setObjectOperations,
    setQaData,
    setQaOperations,
  ])

  const normalizeVideoTaskData = useCallback(
    async (detail: Task.DataProps, requestSeq: number) => {
      const isStaleRequest = () => requestSeq !== latestTaskDetailRequestSeq

      // 后端任务可能是视频（单条 data_name），这里统一展开为可渲染的“帧列表任务”。
      const sourceNames = detail.data_name
        .map((item) => item?.name?.[0] || "")
        .filter((name) => !!name)
      if (!sourceNames.length || !projectId || !taskId) return detail

      const isVideoTask = isVideoTaskDetail(detail)
      if (!isVideoTask) {
        setSourceVideoNames([])
        setHasReadyVideoFrame(true)
        return detail
      }
      setSourceVideoNames(sourceNames)

      const appendFramesToPage = (frameNames: string[]) => {
        if (isStaleRequest() || !frameNames.length) return

        const bottomStore = useBottomToolsStore.getState()
        const nextAllItems = [...bottomStore.allItems]
        const existedItems = new Set(nextAllItems)
        const appendNames: string[] = []

        frameNames.forEach((name) => {
          if (!name || existedItems.has(name)) return
          existedItems.add(name)
          nextAllItems.push(name)
          appendNames.push(name)
        })
        if (!appendNames.length) return

        bottomStore.setAllItems(nextAllItems)
        setTaskDetail((prev) => {
          const baseDetail = prev ?? { ...detail, data_name: [] }
          const existedFrameNames = new Set(
            baseDetail.data_name.map((item) => item?.name?.[0] || "")
          )
          const additions = appendNames
            .filter((name) => !existedFrameNames.has(name))
            .map((name) => ({
              name: [name] as [string],
              related_images: [] as [],
            }))
          if (!additions.length) {
            return baseDetail === prev ? prev : baseDetail
          }
          return {
            ...baseDetail,
            data_name: [...baseDetail.data_name, ...additions],
          }
        })
      }

      const imagesMap = new Map(useImagesStore.getState().images)
      const frameMapUpdates = new Map<string, string[]>()
      let imageMapUpdated = false
      const normalizedDataName: Task.DataProps["data_name"] = []

      for (const videoName of sourceNames) {
        // 任务级缓存键：相同 project/task/video 可复用历史抽帧结果，避免重复解码。
        const cacheKey = `${projectId}:${taskId}:${videoName}`
        let frameNames =
          useVideoFrameStore.getState().videoFrames.get(cacheKey) ?? []

        const hasAllCachedFrames =
          frameNames.length > 0 &&
          frameNames.every((frameName) => {
            const cache = imagesMap.get(frameName)
            return typeof cache === "string" && cache.length > 0
          })

        if (!hasAllCachedFrames) {
          let frameData: Awaited<ReturnType<typeof processVideo>> = []
          const noticeId = `video-process-${cacheKey}`
          const fetchStartedAt = Date.now()
          let decodeStartedAt: number | null = null
          let fetchNoticeTicker: ReturnType<typeof setInterval> | null = null
          let progress = 0
          let firstFrameDelaySec: string | null = null
          const streamFrameNames: string[] = []
          const streamFrameNameSet = new Set<string>()
          let lastNoticeUpdateAt = 0
          let noticeTicker: ReturnType<typeof setInterval> | null = null
          const getFetchSec = (anchorTs?: number) => {
            const endTs = decodeStartedAt ?? anchorTs ?? Date.now()
            return ((endTs - fetchStartedAt) / 1000).toFixed(1)
          }

          const updateProgressNotice = (force = false) => {
            if (isStaleRequest()) return
            const now = Date.now()
            if (!force && now - lastNoticeUpdateAt < 500) return
            lastNoticeUpdateAt = now
            const decodeElapsedSec = decodeStartedAt
              ? ((now - decodeStartedAt) / 1000).toFixed(1)
              : "0.0"
            const fetchSec = getFetchSec(now)
            const progressPct = Math.max(0, Math.min(100, progress * 100))
            const progressText =
              progressPct < 1
                ? `${progressPct.toFixed(1)}%`
                : `${Math.round(progressPct)}%`
            const firstFrameText = firstFrameDelaySec
              ? `，首帧解码 ${firstFrameDelaySec}s`
              : decodeStartedAt && now - decodeStartedAt >= 8000
                ? "，首帧生成中"
                : ""
            notifications.update({
              id: noticeId,
              loading: true,
              autoClose: false,
              withCloseButton: false,
              title: "视频抽帧中",
              message: `${videoName} ${progressText}，已生成 ${streamFrameNames.length} 帧，解码用时 ${decodeElapsedSec}s，拉取 ${fetchSec}s${firstFrameText}`,
            })
          }

          const applyFrameBatch = (
            batchFrames: Awaited<ReturnType<typeof processVideo>>
          ) => {
            if (isStaleRequest() || !batchFrames.length) return
            const appendNames: string[] = []
            batchFrames.forEach((frame) => {
              if (!frame?.name || streamFrameNameSet.has(frame.name)) return
              streamFrameNameSet.add(frame.name)
              streamFrameNames.push(frame.name)
              appendNames.push(frame.name)
              imagesMap.set(frame.name, frame.dataUrl)
            })
            if (!appendNames.length) return

            if (!firstFrameDelaySec) {
              const anchor = decodeStartedAt ?? Date.now()
              const firstDelayMs = Date.now() - anchor
              firstFrameDelaySec = (firstDelayMs / 1000).toFixed(1)
              progress = Math.max(progress, 0.01)
              setHasReadyVideoFrame(true)
            }
            imageMapUpdated = true
            useImagesStore.getState().setImages(new Map(imagesMap))
            useVideoFrameStore
              .getState()
              .setVideoFrames(cacheKey, [...streamFrameNames])
            appendFramesToPage(appendNames)
            updateProgressNotice(true)
          }

          // 用计数器驱动全局 LoadingOverlay，仅在首帧未到达前阻塞页面。
          setVideoFetchPendingCount((prev) => prev + 1)
          notifications.show({
            id: noticeId,
            loading: true,
            autoClose: false,
            withCloseButton: false,
            title: "视频加载中",
            message: `正在获取 ${videoName}，拉取用时 0.0s`,
          })
          fetchNoticeTicker = setInterval(() => {
            if (isStaleRequest()) return
            notifications.update({
              id: noticeId,
              loading: true,
              autoClose: false,
              withCloseButton: false,
              title: "视频加载中",
              message: `正在获取 ${videoName}，拉取用时 ${getFetchSec()}s`,
            })
          }, 1000)
          try {
            const base64Video = await getServerImage({
              data_names: [videoName],
              data_type: 2,
              project_id: projectId,
            })
            if (isStaleRequest()) return detail
            decodeStartedAt = Date.now()
            const fetchSecAtDecode = getFetchSec(decodeStartedAt)
            if (fetchNoticeTicker) {
              clearInterval(fetchNoticeTicker)
              fetchNoticeTicker = null
            }

            notifications.update({
              id: noticeId,
              loading: true,
              autoClose: false,
              withCloseButton: false,
              title: "视频抽帧中",
              message: `正在快速提取并持续抽帧 ${videoName}，拉取 ${fetchSecAtDecode}s`,
            })
            noticeTicker = setInterval(() => {
              updateProgressNotice(true)
            }, 1000)

            frameData = await processVideo({
              base64Data: String(base64Video || ""),
              fileName: videoName,
              // 输出帧名带 project/task 前缀，降低跨任务重名覆盖风险。
              namePrefix: normalizeNamePrefix(
                `${projectId}_${taskId}_${videoName.replace(/\.[^.]+$/, "")}`
              ),
              args: streamingFrameArgs,
              quickFirstFrame: true,
              continueAfterQuickFirstFrame: true,
              onProgress: (value) => {
                if (isStaleRequest()) return
                progress = Math.max(0, Math.min(1, Number(value) || 0))
                updateProgressNotice(false)
              },
              onFrames: (batchFrames) => {
                applyFrameBatch(batchFrames)
              },
            })
            if (isStaleRequest()) return detail

            const totalSec = ((Date.now() - fetchStartedAt) / 1000).toFixed(1)
            const fetchSecFinal = decodeStartedAt
              ? ((decodeStartedAt - fetchStartedAt) / 1000).toFixed(1)
              : "未统计"
            notifications.update({
              id: noticeId,
              loading: false,
              color: "teal",
              title: "视频处理完成",
              message: `${videoName} 已生成 ${frameData.length} 帧（首帧解码 ${firstFrameDelaySec || "未统计"}s，总耗时 ${totalSec}s，拉取 ${fetchSecFinal}s）`,
              autoClose: 2600,
              withCloseButton: true,
            })
          } catch (error) {
            const message = error instanceof Error ? error.message : "未知错误"
            const fetchSec = getFetchSec()
            notifications.update({
              id: noticeId,
              loading: false,
              color: "red",
              title: "视频处理失败",
              message: `${videoName}（${message}，拉取 ${fetchSec}s）`,
              autoClose: 5000,
              withCloseButton: true,
            })
            throw new Error(`视频帧生成失败：${videoName}（${message}）`)
          } finally {
            if (fetchNoticeTicker) {
              clearInterval(fetchNoticeTicker)
              fetchNoticeTicker = null
            }
            if (noticeTicker) {
              clearInterval(noticeTicker)
              noticeTicker = null
            }
            setVideoFetchPendingCount((prev) => Math.max(prev - 1, 0))
          }
          frameNames = frameData.map((frame) => frame.name)
          if (!frameNames.length) {
            throw new Error(`视频帧生成失败：${videoName}`)
          }
          frameData.forEach((frame) => {
            if (!imagesMap.has(frame.name)) {
              imagesMap.set(frame.name, frame.dataUrl)
            }
          })
          imageMapUpdated = true
          frameMapUpdates.set(cacheKey, frameNames)
          appendFramesToPage(frameNames)
        } else {
          // 已命中缓存时也立即解锁页面并展示帧列表。
          setHasReadyVideoFrame(true)
          appendFramesToPage(frameNames)
        }

        if (!frameNames.length) {
          throw new Error(`视频帧映射为空：${videoName}`)
        }

        frameNames.forEach((frameName) => {
          // 将视频帧映射回 Task.DataProps 结构，复用现有图片标注渲染逻辑。
          normalizedDataName.push({
            name: [frameName] as [string],
            related_images: [],
          })
        })
      }

      if (!normalizedDataName.length) {
        throw new Error("未生成可用视频帧")
      }

      if (imageMapUpdated) {
        // 批量写入，避免每帧 setState 触发频繁重渲染。
        useImagesStore.getState().setImages(imagesMap)
      }
      frameMapUpdates.forEach((frameNames, cacheKey) => {
        useVideoFrameStore.getState().setVideoFrames(cacheKey, frameNames)
      })
      setHasReadyVideoFrame(true)

      return {
        ...detail,
        data_name: normalizedDataName,
      }
    },
    [projectId, taskId]
  )

  const downloadVideoToLocal = useCallback(
    (base64Video: string, name: string) => {
      // 调试入口：把服务端 base64 视频还原成本地文件，便于和抽帧结果对比。
      const payload = normalizeBase64(String(base64Video || ""))
      if (!payload) throw new Error("视频数据为空")
      const binary = atob(payload)
      const bytes = new Uint8Array(binary.length)
      for (let i = 0; i < binary.length; i += 1) {
        bytes[i] = binary.charCodeAt(i)
      }
      const ext = getFileExt(name)
      const blob = new Blob([bytes], {
        type: videoMimeByExt[ext] || "video/mp4",
      })
      const url = URL.createObjectURL(blob)
      const anchor = document.createElement("a")
      anchor.href = url
      anchor.download = name
      document.body.appendChild(anchor)
      anchor.click()
      document.body.removeChild(anchor)
      URL.revokeObjectURL(url)
    },
    []
  )

  const handleDownloadSourceVideo = useCallback(async () => {
    const videoName = sourceVideoNames[0]
    if (!videoName || !projectId) return
    const noticeId = `video-download-${projectId}-${taskId || 0}-${videoName}`
    const fetchStartedAt = Date.now()
    let fetchNoticeTicker: ReturnType<typeof setInterval> | null = null
    setDownloadingVideoName(videoName)
    setVideoFetchPendingCount((prev) => prev + 1)
    notifications.show({
      id: noticeId,
      loading: true,
      autoClose: false,
      withCloseButton: false,
      title: "准备下载视频",
      message: `正在获取 ${videoName}，拉取用时 0.0s`,
    })
    fetchNoticeTicker = setInterval(() => {
      const fetchSec = ((Date.now() - fetchStartedAt) / 1000).toFixed(1)
      notifications.update({
        id: noticeId,
        loading: true,
        autoClose: false,
        withCloseButton: false,
        title: "准备下载视频",
        message: `正在获取 ${videoName}，拉取用时 ${fetchSec}s`,
      })
    }, 1000)
    try {
      const base64Video = await getServerImage({
        data_names: [videoName],
        data_type: 2,
        project_id: projectId,
      })
      if (fetchNoticeTicker) {
        clearInterval(fetchNoticeTicker)
        fetchNoticeTicker = null
      }
      const fetchSec = ((Date.now() - fetchStartedAt) / 1000).toFixed(1)
      downloadVideoToLocal(String(base64Video || ""), videoName)
      notifications.update({
        id: noticeId,
        loading: false,
        color: "teal",
        title: "下载已开始",
        message:
          sourceVideoNames.length > 1
            ? `${videoName}（当前任务共 ${sourceVideoNames.length} 个视频，拉取 ${fetchSec}s）`
            : `${videoName}（拉取 ${fetchSec}s）`,
        autoClose: 2500,
        withCloseButton: true,
      })
    } catch (error) {
      const message = error instanceof Error ? error.message : "未知错误"
      const fetchSec = ((Date.now() - fetchStartedAt) / 1000).toFixed(1)
      notifications.update({
        id: noticeId,
        loading: false,
        color: "red",
        title: "下载失败",
        message: `${videoName}（${message}，拉取 ${fetchSec}s）`,
        autoClose: 5000,
        withCloseButton: true,
      })
    } finally {
      if (fetchNoticeTicker) {
        clearInterval(fetchNoticeTicker)
        fetchNoticeTicker = null
      }
      setDownloadingVideoName(null)
      setVideoFetchPendingCount((prev) => Math.max(prev - 1, 0))
    }
  }, [downloadVideoToLocal, projectId, sourceVideoNames, taskId])

  const asyncGetTaskDetail = useCallback(async () => {
    if (taskId) {
      const requestSeq = ++latestTaskDetailRequestSeq
      try {
        usePaperStore.getState().setLoadingData(true)
        usePaperStore.getState().resetRasterScale()
        setLabel(new Map())
        setStateStack([])
        useRightToolsStore.getState().setPathGroupMap(new Map())
        useObjectStore.getState().resetPathAndOperationStatus()
        setHasReadyVideoFrame(false)
        setSourceVideoNames([])
        // 获取当前任务全量数据
        const res = await getTaskList({
          id: [taskId],
          get_data: true,
          page_number: 1,
          page_size: 1,
        })
        if (requestSeq !== latestTaskDetailRequestSeq) {
          console.warn("[LabelNossr] ignore stale task list response", {
            taskId,
            requestSeq,
            latestRequestSeq: latestTaskDetailRequestSeq,
          })
          return
        }
        let detail = res.task_list[0]
        if (isVideoTaskDetail(detail)) {
          // 视频任务先清空帧列表占位，首帧到达后再逐步追加并可立即开始操作。
          setTaskDetail({
            ...detail,
            data_name: [],
          })
          useBottomToolsStore.getState().setAllItems([])
        } else {
          setHasReadyVideoFrame(true)
        }
        detail = await normalizeVideoTaskData(detail, requestSeq)
        if (requestSeq !== latestTaskDetailRequestSeq) {
          console.warn("[LabelNossr] ignore stale normalized task response", {
            taskId,
            requestSeq,
            latestRequestSeq: latestTaskDetailRequestSeq,
          })
          return
        }
        setTaskDetail(detail)
        // 帧列表
        useBottomToolsStore
          .getState()
          .setAllItems(detail.data_name.map((item) => item.name[0] || ""))
        // 获取当前任务流转记录
        let flowCommentArr: Comment[] = []
        let turn1time = 1
        let turn2time = 1
        const flowRes = await getTaskWorkFlow(taskId)
        if (requestSeq !== latestTaskDetailRequestSeq) {
          console.warn("[LabelNossr] ignore stale workflow response", {
            taskId,
            requestSeq,
            latestRequestSeq: latestTaskDetailRequestSeq,
          })
          return
        }
        flowRes.forEach((item) => {
          if (item.task_status_src !== 4 && item.task_status_dst === 4) {
            flowCommentArr.push({
              uid: item.new_uid,
              turn: 1,
              times: turn1time,
              check_box_comments: [],
              text_comments: [],
              comment_type: 0,
              date: 0,
              scene_info: {
                type: 0,
              },
            })
            turn1time++
          } else if (item.task_status_src !== 6 && item.task_status_dst === 6) {
            flowCommentArr.push({
              uid: item.new_uid,
              turn: 2,
              times: turn2time,
              check_box_comments: [],
              text_comments: [],
              comment_type: 0,
              date: 0,
              scene_info: {
                type: 0,
              },
            })
            turn2time++
          }
        })
        console.log(flowCommentArr)
        useLabelStore.getState().setLabelDefaultComments(flowCommentArr)
        const { work_time, label_status } = detail
        setLabelTime({
          label: work_time.label_work_time,
          review1: work_time.first_review_work_time,
          review2: work_time.second_review_work_time,
        })
        // 获取并处理当前任务标注结果数据
        const dataRes = await getLabelResult(taskId)
        if (requestSeq !== latestTaskDetailRequestSeq) {
          console.warn("[LabelNossr] ignore stale label result response", {
            taskId,
            requestSeq,
            latestRequestSeq: latestTaskDetailRequestSeq,
          })
          return
        }
        const { results } = dataRes
        const resultImageEntries = results.flatMap((item) =>
          expandLabelResultImages(item)
        )

        let currentWorkTime = 0
        if (label_status === 2) {
          currentWorkTime = work_time.label_work_time
        } else if (label_status === 4) {
          currentWorkTime = work_time.first_review_work_time
        } else if (label_status === 6) {
          currentWorkTime = work_time.second_review_work_time
        }

        // 生成当前workload
        const workLoad: WorkLoad = {
          object_size: {},
          prelabel_modify_size: {},
          review_size: 0,
          comment_size: 0,
          work_time: currentWorkTime,
          dynamic_size: {},
          static_size: {},
          review_dynamic_size: {},
          review_static_size: {},
          review_question_size: {},
          key_frame_size: {},
          review_key_frame_size: {},
          question_size: {},
        }
        resultImageEntries.forEach(({ frameName, imageObjects }) => {
          const { annotations, images, key_frame, desc } = imageObjects
          const rasterName = frameName || images.file_name
          if (rasterName) {
            usePaperStore
              .getState()
              .setRasterSize(rasterName, [images.width, images.height])
          }
          annotations.forEach((annotation) => {
            let reviewed = false
            const { comment, prelabel, uid } = annotation
            if (prelabel)
              workLoad.prelabel_modify_size[uid]
                ? workLoad.prelabel_modify_size[uid]++
                : (workLoad.prelabel_modify_size[uid] = 1)
            else
              workLoad.object_size[uid]
                ? workLoad.object_size[uid]++
                : (workLoad.object_size[uid] = 1)
            if (comment) {
              comment.forEach((c) => {
                if (c.comment_type > 0 && c.uid === user_id) reviewed = true
                if (c.comment_type === 2 && c.uid === user_id)
                  workLoad.comment_size++
              })
            }
            if (reviewed) workLoad.review_size++
          })
          if (key_frame) {
            if (label_status === 2) {
              let id = imageObjects.label1_uid!
              if (id)
                workLoad.key_frame_size[id]
                  ? workLoad.key_frame_size[id]++
                  : (workLoad.key_frame_size[id] = 1)
            } else if (label_status === 4) {
              let id = imageObjects.review1_uid!
              if (id)
                workLoad.review_key_frame_size[id]
                  ? workLoad.review_key_frame_size[id]++
                  : (workLoad.review_key_frame_size[id] = 1)
            } else if (label_status === 6) {
              let id = imageObjects.review2_uid!
              if (id)
                workLoad.review_key_frame_size[id]
                  ? workLoad.review_key_frame_size[id]++
                  : (workLoad.review_key_frame_size[id] = 1)
            }
          }
          if (useDescToolsStore.getState().qaOperations.length) {
            if (desc) {
              const { other_desc } = desc
              const descData = other_desc ? JSON.parse(other_desc) : {}
              Object.values(descData).forEach((questionArr: any) => {
                questionArr.forEach((q: any) => {
                  const v: any = Object.values(q)[0]
                  if (v.uid) {
                    workLoad.question_size[v.uid]
                      ? workLoad.question_size[v.uid]++
                      : (workLoad.question_size[v.uid] = 1)
                  }
                  if (v.tag) {
                    const uid = usePermissionStore.getState().user_id
                    workLoad.review_question_size[uid]
                      ? workLoad.review_question_size[uid]++
                      : (workLoad.review_question_size[uid] = 1)
                  }
                })
              })
            }
          }
        })
        console.log("获取时计算的工作量统计", workLoad)
        setOldWorkLoad(workLoad)
        const taskLabelData = new Map()
        const pathGroupData = new Map()
        // 重置 PathIds
        useRightToolsStore.getState().setPathIds([])
        // 重置 PathGroupIds
        useRightToolsStore.getState().setPathGroupIds([])
        // 生成标注结果
        resultImageEntries.forEach(({ frameName, imageObjects }) => {
          const key = frameName
          if (!key) return
          const categoryMap = new Map()
          const { annotations, image_groups, desc } = imageObjects
          // 赋值用过的 id
          annotations.map((annotation) => {
            useRightToolsStore.getState().pushPathId(annotation.id)
          })
          // 赋值用过的 groupId
          image_groups &&
            Object.keys(image_groups)?.map((groupId) => {
              useRightToolsStore.getState().pushPathGroupId(+groupId)
            })
          annotations.forEach((annotation) => {
            const { id, category_id, comment } = annotation
            let current_comment: Comment[] = []
            if (comment) {
              current_comment = flowCommentArr.map((c) => {
                const { turn, times } = c
                let modified_comment = null
                comment.forEach((item) => {
                  if (item.turn === turn && item.times === times)
                    modified_comment = item
                })
                return modified_comment ? modified_comment : c
              })
            } else {
              current_comment = [...flowCommentArr]
            }
            let detail: any = {
              ...annotation,
              comment: current_comment,
            }
            // group info
            if (image_groups) {
              const key = findGroupKey(image_groups as any, id)
              if (key !== -1) detail.parentGroupId = key
            }
            const categoryKey = `${category_id}`
            let arr = categoryMap.has(categoryKey)
              ? categoryMap.get(categoryKey)
              : []
            if (annotation.rect) {
              // arr.push([id, getRectPoints(annotation.rect), detail, []]);
              arr.push([id, [getRectPoints(annotation.rect)], detail, []])
            }

            if (annotation.line_segment) {
              arr.push([
                id,
                annotation.line_segment.map((item) => getSegmentPoints(item)),
                detail,
                [],
              ])
            }

            if (annotation.segmentation) {
              arr.push([
                id,
                // getSegmentPoints(annotation.segmentation[0]),
                annotation.segmentation.map((item) => getSegmentPoints(item)),
                detail,
                annotation.hollow_segmentation?.map((item) =>
                  getSegmentPoints(item)
                ) || [],
              ])
            }
            if (annotation.key_points) {
              arr.push([
                id,
                // getSegmentPoints(annotation.segmentation[0]),
                [
                  getSegmentPoints(
                    annotation.key_points.map((item) => item.point)
                  ),
                ],
                Object.assign(detail, {
                  circles: annotation.key_points,
                }),
                [],
              ])
            }

            categoryMap.set(categoryKey, arr)
          })
          taskLabelData.set(key, categoryMap)
          // 关键帧信息
          const keyFrame = {
            key_frame: imageObjects.key_frame || false,
            label1_ts: imageObjects.label1_ts || 0,
            label1_uid: imageObjects.label1_uid || 0,
            review1_ts: imageObjects.review1_ts || 0,
            review1_uid: imageObjects.review1_uid || 0,
            review2_ts: imageObjects.review2_ts || 0,
            review2_uid: imageObjects.review2_uid || 0,
          }
          useBottomToolsStore.getState().setKeyFrameData(key, keyFrame)
          // 大语言模型 两种数据情况
          if (useDescToolsStore.getState().qaOperations.length) {
            if (desc) {
              const { other_desc } = desc
              let current_data: any = {}
              const descData = other_desc ? JSON.parse(other_desc) : {}
              Object.entries(descData).forEach(
                ([label_class, questionArr]: any) => {
                  let arr = questionArr.map((question: any) => {
                    let [name, val]: any = Object.entries(question)[0]
                    return {
                      [name]: {
                        ...val,
                        value: val.value
                          ? `${val.value}${splitWord}${val.value}`
                          : undefined,
                        comment: val.comment
                          ? `${val.comment}${splitWord}${val.comment}`
                          : undefined,
                      },
                    }
                  })
                  current_data[label_class] = arr
                }
              )
              const qaData = useDescToolsStore.getState().qaData
              // 无文本描述时 赋予预设值
              if (JSON.stringify(current_data) === "{}") {
                current_data = qaData.get("init")
              }
              const qaMap = safeClone(qaData)
              qaMap.set(key, current_data)
              useDescToolsStore.getState().setQaData(qaMap)
            }
          } else if (useDescToolsStore.getState().descOperations.length) {
            if (desc) {
              const { meta_operation, other_desc } = desc
              const currentMetaMap = safeClone(
                useDescToolsStore.getState().metaData
              )
              currentMetaMap.set(key, meta_operation || [])
              useDescToolsStore.getState().setMetaData(currentMetaMap)
              const currentDescMap = safeClone(
                useDescToolsStore.getState().descData
              )
              let descMap = new Map()
              const descData = other_desc ? JSON.parse(other_desc) : {}
              console.log(descData)
              Object.entries(descData).forEach(
                ([label_class, { value, is_correct, comment }]: any) => {
                  descMap.set(label_class, {
                    value:
                      (typeof value === "string"
                        ? `${value}${splitWord}${value}`
                        : value) || undefined,
                    is_correct: is_correct || undefined,
                    comment: comment || undefined,
                  })
                }
              )
              currentDescMap.set(key, descMap)
              useDescToolsStore.getState().setDescData(currentDescMap)
            }
          }
          image_groups &&
            pathGroupData.set(
              key,
              new Map(
                Object.entries(image_groups).map(([key, value]: any) => [
                  key * 1,
                  value,
                ])
              )
            )
        })
        setLabel(taskLabelData)
        // 获取任务详情时设置状态栈初始值
        setStateStack([taskLabelData])
        useDescToolsStore.getState().updateFlag(true)
        useRightToolsStore.getState().setPathGroupMap(pathGroupData)
      } catch (err) {
        if (requestSeq !== latestTaskDetailRequestSeq) return
        console.log(err)
        setSourceVideoNames([])
        setOldWorkLoad(null)
        setTaskDetail(undefined)
        useBottomToolsStore.getState().setAllItems([])
        setLabel(new Map())
        setStateStack([])
        useRightToolsStore.getState().setPathGroupMap(new Map())
        if (err instanceof Error && err.message.includes("视频")) {
          showNotification({
            color: "red",
            title: "视频解码失败",
            message: err.message,
          })
        }
      } finally {
        if (requestSeq === latestTaskDetailRequestSeq) {
          usePaperStore.getState().setLoadingData(false)
        }
      }
    }
  }, [
    normalizeVideoTaskData,
    setLabel,
    setLabelTime,
    setStateStack,
    taskId,
    user_id,
  ])

  // 保存时更新当前workload
  const updateOldWorkLoad = (data: WorkLoad) => {
    setOldWorkLoad(data)
  }

  useEffect(() => {
    asyncGetProjectDetail()
    asyncGetTaskDetail()
  }, [asyncGetProjectDetail, asyncGetTaskDetail])

  // 定义 reducer 函数
  const reducer = useCallback(
    (state: LabelState, action: Action): LabelState => {
      switch (action.type) {
        case "INIT_IMAGES":
          const newState = new Map() // 创建状态的副本
          action.imageIds.forEach((imageId) => {
            newState.set(
              imageId,
              action.objectOperations.map((item) => item.category_id.toString())
            ) // 将操作列表添加到新状态中
          })
          return newState

        case "REMOVE_IMAGE":
          const stateAfterRemoval = new Map(state)
          if (stateAfterRemoval.has(action.imageId)) {
            stateAfterRemoval.delete(action.imageId) // 删除该图片的条目
          }
          return stateAfterRemoval

        default:
          return state // 返回当前状态
      }
    },
    []
  )

  const [labelState, dispatch] = useReducer(reducer, initialState)

  const initImages = (
    imageIds: string[],
    objectOperations: Project.LabelSchemaList[]
  ) => {
    dispatch({
      type: "INIT_IMAGES",
      imageIds,
      objectOperations,
    })
  }

  const [isLabelTask, setIsLabelTask] = useState<boolean>(false)

  useEffect(() => {
    if (!taskDetail || !projectDetail) {
      setIsLabelTask(false)
      setIsView(true)
      setEditMode(false)
      return
    }
    if (taskDetail) {
      if (
        ![201, 202, 203, 500].includes(projectDetail!.status) ||
        ![2, 4, 6, 7].includes(taskDetail.label_status) ||
        taskDetail.current_uid !== user_id
      ) {
        // 如果不是能标注的任务
        setIsLabelTask(false)
        setIsView(true)
        setEditMode(false)
      } else {
        setIsLabelTask(true)
        setIsView(false)
      }

      initImages(
        taskDetail.data_name.map((item) => item.name?.[0]),
        objectOperations
      )

      if (taskDetail.data_name?.length) setShowMultiFrame(true)
    }
  }, [
    objectOperations,
    setIsView,
    setShowMultiFrame,
    user_id,
    taskDetail,
    projectDetail,
    setEditMode,
  ])

  const { setPaperMode, setToolOption, setGroupScale } = usePaperStore()

  // @ts-expect-error  to_do
  const forceUpdate = useReducer((bool) => !bool)[1]

  const getOperationSchema = useCallback(
    (operationId: string) => {
      return (
        projectDetail?.label_schema_list?.find(
          (item) => item.category_id === +operationId && item.label_type !== 201
        ) || null
      )
    },
    [projectDetail?.label_schema_list]
  )

  const setDrawType = useCallback(() => {
    if (isView || renderMode) {
      setPaperMode(null)
    } else {
      let operationSchema = getOperationSchema(activeOperation)
      if (operationSchema) {
        let strokeColor = `rgb(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]})`
        let fillColor = `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.3)`
        let blankColor = `rgba(${operationSchema.color[0]},${operationSchema.color[1]},${operationSchema.color[2]},0.01)`

        if (editMode) {
          switch (
            operationSchema &&
            labelTypeMap.get(operationSchema.label_type)
          ) {
            case "多边形":
              setToolOption({
                strokeColor: strokeColor,
                strokeWidth: 2,
                fillColor: fillColor,
                blankColor: blankColor,
              })
              setPaperMode("polygon")
              return
            case "关键点":
              setToolOption({
                strokeColor: strokeColor,
                strokeWidth: 2,
                fillColor: fillColor,
                blankColor: blankColor,
              })
              setPaperMode("point")
              return
            case "多线段":
              setToolOption({
                strokeColor: strokeColor,
                strokeWidth: 2,
                fillColor: fillColor,
                blankColor: blankColor,
              })
              setPaperMode("brush")
              return
            case "2D框":
              setToolOption({
                strokeColor: strokeColor,
                strokeWidth: 2,
                fillColor: fillColor,
                blankColor: blankColor,
              })
              setPaperMode("rectangle")
              return
            default:
              setPaperMode("polygon")
              return
          }
        } else {
          setPaperMode("pan")
        }
      }
    }
  }, [
    activeOperation,
    editMode,
    getOperationSchema,
    isView,
    renderMode,
    setPaperMode,
    setToolOption,
  ])

  useEffect(() => {
    setDrawType()
  }, [editMode, setDrawType])

  const { setShift, focusInput } = useKeyEventStore()
  const drawAction = useKeyboardStore((state) => state.drawAction)
  const drawShortcutSnapshotRef = useRef<{
    activeOperation: string
    drawOption: "default" | "intersect" | "unite"
    editMode: boolean
  } | null>(null)

  const handleShortcut = useCallback(
    (key: string, ctrl: boolean) => {
      let checkOperation = getOperationSchema(key)
      const mode = usePaperStore.getState().mode
      // 标注方案里面存在的非文本类型标注才能切换
      if (checkOperation) {
        // 辅助标注模式下非多边形类型不能切换
        if (mode === "support" && checkOperation.label_type !== 102) return
        if (ctrl) {
          setActiveOperation((+key + 10).toString())
        } else {
          setActiveOperation(key)
        }
      }
    },
    [getOperationSchema, setActiveOperation]
  )

  const resolveSelectedObjectContext = useCallback(() => {
    const imageId = useBottomToolsStore.getState().activeImage
    const selectedIds = useObjectStore.getState().selectedPath[imageId] || []
    if (selectedIds.length !== 1) return null

    const objectId = selectedIds[0]
    const imageLabel = useLabelStore.getState().label.get(imageId)
    if (!imageLabel) return null

    let operationId = ""
    for (const [categoryId, paths] of imageLabel.entries()) {
      if (paths.some((path) => path[0] === objectId)) {
        operationId = categoryId
        break
      }
    }
    if (!operationId) return null

    const selectedItem = usePaperStore.getState().getItemById(objectId)
    if (!selectedItem?.data?.type) return null

    return {
      imageId,
      objectId,
      operationId,
      objectType: selectedItem.data.type as string,
    }
  }, [])

  const restoreDrawShortcutSnapshot = useCallback(() => {
    useKeyboardStore.getState().setDrawAction("none")

    const snapshot = drawShortcutSnapshotRef.current
    if (!snapshot) return

    setActiveOperation(snapshot.activeOperation)
    setDrawOption(snapshot.drawOption)
    setEditMode(snapshot.editMode)
    drawShortcutSnapshotRef.current = null
  }, [setActiveOperation, setDrawOption, setEditMode])

  const enterSelectedObjectDrawMode = useCallback(
    (mode: "add" | "subtract") => {
      const selectedContext = resolveSelectedObjectContext()
      if (!selectedContext) {
        notifications.show({
          color: "yellow",
          message: "请先选中单个对象",
        })
        return
      }

      if (
        mode === "add" &&
        !["polygon", "brush"].includes(selectedContext.objectType)
      ) {
        notifications.show({
          color: "yellow",
          message: "新增区域仅支持 polygon/brush 类型对象",
        })
        return
      }

      if (mode === "subtract" && selectedContext.objectType !== "polygon") {
        notifications.show({
          color: "yellow",
          message: "删除区域仅支持 polygon 类型对象",
        })
        return
      }

      if (!drawShortcutSnapshotRef.current) {
        const topState = useTopToolsStore.getState()
        drawShortcutSnapshotRef.current = {
          activeOperation: topState.activeOperation,
          drawOption: topState.drawOption,
          editMode: topState.editMode,
        }
      }

      setActiveOperation(selectedContext.operationId)
      usePaperStore.getState().setContinuePolygonTarget(null)
      setEditMode(true)
      setDrawOption("default")
      useKeyboardStore.getState().setDrawAction(mode)
      setPressA(true)

      if (!useTopToolsStore.getState().pressA) {
        notifications.show({
          color: "yellow",
          message: "当前对象无法进入区域编辑模式",
        })
        restoreDrawShortcutSnapshot()
      }
    },
    [
      resolveSelectedObjectContext,
      restoreDrawShortcutSnapshot,
      setActiveOperation,
      setDrawOption,
      setEditMode,
      setPressA,
    ]
  )

  const handleToggleHideSelectedObject = useCallback(() => {
    const selectedContext = resolveSelectedObjectContext()
    if (!selectedContext) {
      notifications.show({
        color: "yellow",
        message: "请先选中单个对象",
      })
      return
    }

    const currentHideStatus =
      useObjectStore.getState().pathStatus[
        selectedContext.imageId + selectedContext.objectId
      ]?.HIDE || false
    toggleObjectHideByShortcut(
      selectedContext.imageId,
      selectedContext.operationId,
      selectedContext.objectId,
      !currentHideStatus
    )
  }, [resolveSelectedObjectContext])

  const rerenderActiveImageByLabel = useCallback(
    (
      imageId: string,
      imageLabel: Map<string, [number, any[], any, any[]][]>,
      selectedIds: number[]
    ) => {
      const paperGroup = usePaperStore.getState().group
      if (paperGroup) {
        paperGroup
          .getItems({})
          .filter((item) => item.data.id)
          .forEach((item) => {
            item.remove()
          })

        for (const [categoryId] of imageLabel.entries()) {
          paperContainerRef.current?.renderPolygons?.(categoryId, imageLabel)
        }
      }

      useObjectStore.setState((state) => ({
        ...state,
        selectedPath: {
          ...state.selectedPath,
          [imageId]: selectedIds,
        },
      }))
    },
    []
  )

  const removeObjectFromGroupMap = useCallback(
    (imageId: string, id: number) => {
      const nextPathGroupMap = safeClone(
        useRightToolsStore.getState().pathGroupMap
      )
      const imageGroupMap = nextPathGroupMap.get(imageId)
      if (!imageGroupMap) return

      for (let [groupId, pathIds] of imageGroupMap.entries()) {
        const nextPathIds = pathIds.filter((pathId) => pathId !== id)
        if (nextPathIds.length > 1) {
          imageGroupMap.set(groupId, nextPathIds)
        } else {
          imageGroupMap.delete(groupId)
        }
      }

      if (!imageGroupMap.size) nextPathGroupMap.delete(imageId)
      useRightToolsStore.getState().setPathGroupMap(nextPathGroupMap)
    },
    []
  )

  const handleContinueSelectedPolygon = useCallback(() => {
    const selectedContext = resolveSelectedObjectContext()
    if (!selectedContext) {
      notifications.show({
        color: "yellow",
        message: "请先选中单个对象",
      })
      return
    }

    if (selectedContext.objectType !== "polygon") {
      notifications.show({
        color: "yellow",
        message: "继续标注快捷键仅支持 polygon 类型对象",
      })
      return
    }

    setEditMode(true)
    setDrawOption("default")
    setPressA(false)
    setActiveOperation(selectedContext.operationId)
    useKeyboardStore.getState().setDrawAction("none")
    usePaperStore.getState().setContinuePolygonTarget({
      imageId: selectedContext.imageId,
      operationId: selectedContext.operationId,
      objectId: selectedContext.objectId,
    })
    usePaperStore.getState().setPaperMode("polygon")
    notifications.show({
      color: "green",
      message: "已进入继续标注模式",
    })
  }, [
    resolveSelectedObjectContext,
    setActiveOperation,
    setDrawOption,
    setEditMode,
    setPressA,
  ])

  const hasShapeSegments = useCallback((shape: any) => {
    if (!shape) return false
    if (shape instanceof paper.CompoundPath) {
      return (shape.children as paper.Path[]).some(
        (child) => child.segments?.length
      )
    }
    if (shape instanceof paper.Path) {
      return !!shape.segments?.length
    }
    return false
  }, [])

  const createWorkingPolygonShape = useCallback((items: any[]) => {
    const compound = items.find((item) => item instanceof paper.CompoundPath)
    if (compound) {
      return (compound as paper.CompoundPath).clone({ insert: false }) as
        | paper.Path
        | paper.CompoundPath
    }

    const paths = items.filter(
      (item) => item instanceof paper.Path
    ) as paper.Path[]
    if (!paths.length) return null
    if (paths.length === 1) {
      return paths[0].clone({ insert: false }) as
        | paper.Path
        | paper.CompoundPath
    }

    const merged = new paper.CompoundPath({ insert: false })
    paths.forEach((path) => {
      merged.addChild(path.clone({ insert: false }))
    })
    return merged as paper.Path | paper.CompoundPath
  }, [])

  const collectPolygonPoints = useCallback(
    (shape: paper.Path | paper.CompoundPath) => {
      const polygonPoints: any[] = []
      const polygonHollowPoints: any[] = []

      const collectPath = (path: paper.Path) => {
        if (!path.segments?.length) return
        const pathData = JSON.parse(path.exportJSON({ precision: 20 }))
        if (path.clockwise) {
          polygonPoints.push(pathData[1]?.segments)
        } else {
          polygonHollowPoints.push(pathData[1]?.segments)
        }
      }

      if (shape instanceof paper.CompoundPath) {
        ;(shape.children as paper.Path[]).forEach((path) => collectPath(path))
      } else {
        collectPath(shape)
      }

      return { polygonPoints, polygonHollowPoints }
    },
    []
  )

  const applySelectedPolygonOverlap = useCallback(
    (mode: "subtract" | "unite") => {
      const selectedContext = resolveSelectedObjectContext()
      if (!selectedContext) {
        notifications.show({
          color: "yellow",
          message: "请先选中单个对象",
        })
        return
      }
      if (selectedContext.objectType !== "polygon") {
        notifications.show({
          color: "yellow",
          message:
            mode === "subtract"
              ? "镂空快捷键仅支持 polygon 类型对象"
              : "融合快捷键仅支持 polygon/brush 类型对象",
        })
        return
      }

      const group = usePaperStore.getState().group
      if (!group) return

      const targetItems = group
        .getItems({ data: { id: selectedContext.objectId } })
        .filter((item) => item.data?.type === "polygon")
      if (!targetItems.length) return

      let workingShape = createWorkingPolygonShape(targetItems)
      if (!workingShape || !hasShapeSegments(workingShape)) return

      const otherPolygonPaths: paper.Path[] = []
      group.children.forEach((item: any) => {
        if (
          !item.visible ||
          !item.data?.id ||
          item.data.id === selectedContext.objectId ||
          item.data.type !== "polygon"
        )
          return
        if (item instanceof paper.Path) {
          if (item.segments?.length) otherPolygonPaths.push(item)
        } else if (item instanceof paper.CompoundPath) {
          ;(item.children as paper.Path[]).forEach((child) => {
            if (child.segments?.length) otherPolygonPaths.push(child)
          })
        }
      })

      let changed = false

      for (const eachPath of otherPolygonPaths) {
        if (!workingShape || !hasShapeSegments(workingShape)) break

        const eachPathClone = eachPath.clone({ insert: false }) as paper.Path
        const intersectionPath = (workingShape as any).intersect(
          eachPathClone,
          {
            insert: false,
          }
        ) as paper.Path | paper.CompoundPath
        const hasIntersection = hasShapeSegments(intersectionPath)
        intersectionPath?.remove()
        if (!hasIntersection) {
          eachPathClone.remove()
          continue
        }

        const nextShape: paper.Path | paper.CompoundPath =
          mode === "subtract"
            ? ((workingShape as any).subtract(eachPathClone, {
                insert: false,
              }) as paper.Path | paper.CompoundPath)
            : ((workingShape as any).unite(eachPathClone, {
                insert: false,
              }) as paper.Path | paper.CompoundPath)
        eachPathClone.remove()
        workingShape?.remove()
        workingShape = nextShape
        changed = true
      }

      if (!changed) {
        workingShape?.remove()
        return
      }
      if (!workingShape) return

      const raster = usePaperStore.getState().rasterPath
      if (raster && hasShapeSegments(workingShape)) {
        const clippedShape = (workingShape as any).intersect(raster, {
          insert: false,
          trace: false,
        }) as paper.Path | paper.CompoundPath
        workingShape?.remove()
        workingShape = clippedShape
      }
      if (!workingShape) return

      const { polygonPoints, polygonHollowPoints } =
        collectPolygonPoints(workingShape)
      workingShape?.remove()

      const nextLabel = safeClone(useLabelStore.getState().label)
      const imageLabel = nextLabel.get(selectedContext.imageId)
      if (!imageLabel) return
      const operationPaths = safeClone(
        imageLabel.get(selectedContext.operationId) || []
      )
      const selectedIndex = operationPaths.findIndex(
        (item) => item[0] === selectedContext.objectId
      )
      if (selectedIndex === -1) return

      const currentTuple = operationPaths[selectedIndex]
      if (polygonPoints.length) {
        operationPaths[selectedIndex] = [
          selectedContext.objectId,
          polygonPoints,
          safeClone(currentTuple?.[2] || {}),
          polygonHollowPoints,
        ]
        imageLabel.set(selectedContext.operationId, operationPaths)
      } else {
        imageLabel.set(
          selectedContext.operationId,
          operationPaths.filter((item) => item[0] !== selectedContext.objectId)
        )
        removeObjectFromGroupMap(
          selectedContext.imageId,
          selectedContext.objectId
        )
      }

      useLabelStore.getState().setLabel(nextLabel)
      useLabelStore.getState().pushStateStack(nextLabel)

      rerenderActiveImageByLabel(
        selectedContext.imageId,
        imageLabel,
        polygonPoints.length ? [selectedContext.objectId] : []
      )
    },
    [
      collectPolygonPoints,
      createWorkingPolygonShape,
      hasShapeSegments,
      removeObjectFromGroupMap,
      rerenderActiveImageByLabel,
      resolveSelectedObjectContext,
    ]
  )

  const applySelectedBrushIntersectUnite = useCallback(() => {
    const selectedContext = resolveSelectedObjectContext()
    if (!selectedContext) {
      notifications.show({
        color: "yellow",
        message: "请先选中单个对象",
      })
      return
    }
    if (selectedContext.objectType !== "brush") {
      notifications.show({
        color: "yellow",
        message: "融合快捷键仅支持 polygon/brush 类型对象",
      })
      return
    }

    const group = usePaperStore.getState().group
    if (!group) return

    const selectedBrushPaths = group
      .getItems({ data: { id: selectedContext.objectId } })
      .filter((item) => item.data?.type === "brush")
      .flatMap((item) => {
        if (item instanceof paper.CompoundPath)
          return item.children as paper.Path[]
        if (item instanceof paper.Path) return [item]
        return []
      })
      .filter((path) => path.segments?.length)

    if (!selectedBrushPaths.length) return

    const otherBrushPathMap = new Map<number, paper.Path[]>()
    group.children.forEach((item: any) => {
      if (
        !item.visible ||
        !item.data?.id ||
        item.data.id === selectedContext.objectId ||
        item.data.type !== "brush"
      )
        return

      const targetId = Number(item.data.id)
      const currentPaths = otherBrushPathMap.get(targetId) || []
      if (item instanceof paper.CompoundPath) {
        otherBrushPathMap.set(targetId, [
          ...currentPaths,
          ...(item.children as paper.Path[]),
        ])
      } else if (item instanceof paper.Path) {
        otherBrushPathMap.set(targetId, [...currentPaths, item])
      }
    })

    const intersectingIds = new Set<number>()
    for (const [otherId, paths] of otherBrushPathMap.entries()) {
      const hasIntersection = paths.some((otherPath) => {
        return selectedBrushPaths.some((selectedPath) => {
          if (!selectedPath.bounds.intersects(otherPath.bounds)) return false
          return selectedPath.getIntersections(otherPath).length > 0
        })
      })
      if (hasIntersection) intersectingIds.add(otherId)
    }
    if (!intersectingIds.size) return

    const nextLabel = safeClone(useLabelStore.getState().label)
    const imageLabel = nextLabel.get(selectedContext.imageId)
    if (!imageLabel) return

    const selectedOperationPaths = safeClone(
      imageLabel.get(selectedContext.operationId) || []
    )
    const selectedIndex = selectedOperationPaths.findIndex(
      (item) => item[0] === selectedContext.objectId
    )
    if (selectedIndex === -1) return

    const selectedTuple = selectedOperationPaths[selectedIndex]
    const mergedPoints = safeClone(selectedTuple?.[1] || [])
    const pointSet = new Set(
      mergedPoints.map((pointItem) => JSON.stringify(pointItem))
    )

    let appendedCount = 0
    imageLabel.forEach((paths) => {
      paths.forEach((item) => {
        if (!intersectingIds.has(item[0])) return
        ;(item[1] || []).forEach((pathPoints: any) => {
          const key = JSON.stringify(pathPoints)
          if (pointSet.has(key)) return
          pointSet.add(key)
          mergedPoints.push(safeClone(pathPoints))
          appendedCount++
        })
      })
    })

    if (!appendedCount) return

    selectedOperationPaths[selectedIndex] = [
      selectedContext.objectId,
      mergedPoints,
      safeClone(selectedTuple?.[2] || {}),
      safeClone(selectedTuple?.[3] || []),
    ]
    imageLabel.set(selectedContext.operationId, selectedOperationPaths)

    useLabelStore.getState().setLabel(nextLabel)
    useLabelStore.getState().pushStateStack(nextLabel)

    rerenderActiveImageByLabel(selectedContext.imageId, imageLabel, [
      selectedContext.objectId,
    ])
  }, [rerenderActiveImageByLabel, resolveSelectedObjectContext])

  const handleMergeSelectedObjects = useCallback(() => {
    const activeImage = useBottomToolsStore.getState().activeImage
    const selectedIds =
      useObjectStore.getState().selectedPath[activeImage] || []
    if (selectedIds.length <= 1) {
      notifications.show({
        color: "yellow",
        message: "请先选中同类别且数量大于1的对象",
      })
      return
    }

    const nextLabel = safeClone(useLabelStore.getState().label)
    const imageLabel = nextLabel.get(activeImage)
    if (!imageLabel) return

    const selectedIdSet = new Set(selectedIds)
    const selectedByOperation = Array.from(imageLabel.entries())
      .map(([operationId, paths]) => {
        const selectedPaths = paths.filter((item) => selectedIdSet.has(item[0]))
        return { operationId, selectedPaths }
      })
      .filter((item) => item.selectedPaths.length > 0)

    if (
      selectedByOperation.length !== 1 ||
      selectedByOperation[0].selectedPaths.length <= 1 ||
      selectedByOperation[0].selectedPaths.length !== selectedIds.length
    ) {
      notifications.show({
        color: "yellow",
        message: "仅支持合并同类别且数量大于1的选中对象",
      })
      return
    }

    const { operationId, selectedPaths } = selectedByOperation[0]
    const selectedTypeList = selectedIds
      .map((id) => usePaperStore.getState().getItemById(id)?.data?.type)
      .filter((type): type is string => typeof type === "string")
    const selectedTypeSet = new Set(selectedTypeList)
    const selectedType = Array.from(selectedTypeSet)[0]
    const operationSchema = getOperationSchema(operationId)
    const operationType = operationSchema
      ? labelTypeMap.get(operationSchema.label_type)
      : ""

    if (
      selectedTypeList.length !== selectedIds.length ||
      selectedTypeSet.size !== 1 ||
      !["polygon", "brush"].includes(selectedType) ||
      !["多边形", "多线段"].includes(operationType || "")
    ) {
      notifications.show({
        color: "yellow",
        message: "仅支持合并 polygon/brush 类型对象",
      })
      return
    }

    const sortedSelectedPaths = [...selectedPaths].sort((a, b) => a[0] - b[0])
    const targetId = sortedSelectedPaths[0][0]
    const mergedPaths = sortedSelectedPaths.flatMap((item) =>
      safeClone(item[1] || [])
    )
    const mergedCompoundPaths = sortedSelectedPaths.flatMap((item) =>
      safeClone(item[3] || [])
    )
    const mergedDetail = {
      ...(safeClone(sortedSelectedPaths[0][2]) || {}),
      parentGroupId: null,
    }

    const mergedData: [number, any[], any, any[]] = [
      targetId,
      mergedPaths,
      mergedDetail,
      mergedCompoundPaths,
    ]

    const restPaths = (imageLabel.get(operationId) || []).filter(
      (item) => !selectedIdSet.has(item[0])
    )
    imageLabel.set(
      operationId,
      [...restPaths, mergedData].sort((a, b) => a[0] - b[0])
    )

    const nextPathGroupMap = safeClone(
      useRightToolsStore.getState().pathGroupMap
    )
    const imageGroupMap = nextPathGroupMap.get(activeImage)
    if (imageGroupMap) {
      for (let [groupId, pathIds] of imageGroupMap.entries()) {
        const nextPathIds = pathIds.filter((id) => !selectedIdSet.has(id))
        if (nextPathIds.length > 1) {
          imageGroupMap.set(groupId, nextPathIds)
        } else {
          imageGroupMap.delete(groupId)
        }
      }
      if (!imageGroupMap.size) nextPathGroupMap.delete(activeImage)
      useRightToolsStore.getState().setPathGroupMap(nextPathGroupMap)
    }

    useLabelStore.getState().setLabel(nextLabel)
    useLabelStore.getState().pushStateStack(nextLabel)
    useObjectStore.setState((state) => ({
      ...state,
      selectedPath: {
        ...state.selectedPath,
        [activeImage]: [targetId],
      },
    }))

    const paperGroup = usePaperStore.getState().group
    if (paperGroup) {
      paperGroup
        .getItems({})
        .filter((item) => item.data.id)
        .forEach((item) => {
          item.remove()
        })

      for (const [categoryId] of imageLabel.entries()) {
        paperContainerRef.current?.renderPolygons?.(categoryId, imageLabel)
      }
    }

    notifications.show({
      color: "green",
      message: `已合并${selectedIds.length}个对象`,
    })
  }, [getOperationSchema])

  useEffect(() => {
    const down = (e: KeyboardEvent) => {
      // console.log(e, e.key);
      if (focusInput) return

      console.log(e.key)
      const mode = usePaperStore.getState().mode
      const lowerKey = e.key.toLowerCase()
      const commandKey = e.ctrlKey || e.metaKey

      if (e.repeat && ["a", "b", "d", "h", "m", ",", "."].includes(lowerKey))
        return

      if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "m") {
        e.preventDefault()
        const topState = useTopToolsStore.getState()
        topState.setMagnetFlag(!topState.magnetFlag)
        return
      }

      if (isView || renderMode) return

      if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "h") {
        e.preventDefault()
        handleToggleHideSelectedObject()
        return
      }

      if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "a") {
        e.preventDefault()
        enterSelectedObjectDrawMode("add")
        return
      }

      if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "d") {
        e.preventDefault()
        enterSelectedObjectDrawMode("subtract")
        return
      }

      if (!e.ctrlKey && !e.metaKey && !e.altKey && lowerKey === "b") {
        e.preventDefault()
        handleContinueSelectedPolygon()
        return
      }

      if (
        !e.ctrlKey &&
        !e.metaKey &&
        !e.altKey &&
        [",", "，"].includes(e.key)
      ) {
        e.preventDefault()
        applySelectedPolygonOverlap("subtract")
        return
      }

      if (
        !e.ctrlKey &&
        !e.metaKey &&
        !e.altKey &&
        [".", "。"].includes(e.key)
      ) {
        e.preventDefault()
        const selectedContext = resolveSelectedObjectContext()
        if (!selectedContext) {
          notifications.show({
            color: "yellow",
            message: "请先选中单个对象",
          })
          return
        }
        if (selectedContext.objectType === "polygon") {
          applySelectedPolygonOverlap("unite")
          return
        }
        if (selectedContext.objectType === "brush") {
          applySelectedBrushIntersectUnite()
          return
        }
        notifications.show({
          color: "yellow",
          message: "融合快捷键仅支持 polygon/brush 类型对象",
        })
        return
      }

      if (e.altKey && e.key.toLowerCase() === "x") {
        e.preventDefault()
        if (!isView) {
          if (topRef.current?.requestSubmitTaskConfirm) {
            topRef.current.requestSubmitTaskConfirm()
          } else {
            topRef.current?.commitTaskByActionKey?.()
          }
        }
        return
      }

      if (commandKey && lowerKey === "f") {
        e.preventDefault()
        handleMergeSelectedObjects()
        return
      }

      if (e.key === "Alt" && mode !== "support") {
        e.preventDefault()
        // console.log(e.key);
        setEditMode(!editMode)
        if (!editMode) setPressA(false)
      }

      if (e.key === "Shift") {
        e.preventDefault()
        setShift(true)
        useKeyboardStore.getState().setShift(true)
      }

      if (mode !== "support" && /^[0-9]$/.test(e.key)) {
        if (commandKey) e.preventDefault()
        if (e.key === "0") {
          handleShortcut("10", commandKey)
        } else {
          handleShortcut(e.key, commandKey)
        }
      }

      if (lowerKey === "s" && commandKey) {
        e.preventDefault()
        setIsConfirmSave(true)
      }

      if (lowerKey === "g" && commandKey) {
        e.preventDefault()
        paperContainerRef.current.handleCreatePathGroup()
      }

      if (e.key === "Control" || e.key === "Meta") {
        e.preventDefault()
        useKeyboardStore.getState().setCtrl(true)
      }
      // 辅助标注 分割
      if (e.key === "F1") {
        // 判断类型
        e.preventDefault()
        const schema = getOperationSchema(
          useTopToolsStore.getState().activeOperation
        )
        console.log(schema)
        if (schema?.label_type !== 102) {
          showNotification({
            title: "错误",
            message: "当前类型不支持辅助标注！",
            color: "red",
          })
        } else {
          if (mode === "support") {
            // 辅助标注生成对应标注结果
            paperContainerRef.current.saveSupportAnnotationData()
          } else {
            usePaperStore.getState().setPaperMode("support")
          }
        }
      }

      // 追踪
      if (e.key === "i") {
        e.preventDefault()
        paperContainerRef.current.renderTrackingAnnotation()
      }

      // 复制
      if (lowerKey === "c" && commandKey) {
        e.preventDefault()
        useKeyboardStore
          .getState()
          .setCopyDataIds(
            useObjectStore.getState().selectedPath[
              useBottomToolsStore.getState().activeImage
            ]
          )
      }

      // 粘贴
      if (lowerKey === "v" && commandKey) {
        e.preventDefault()
        paperContainerRef.current.copyAnnotationDataToMultiFrame()
      }

      // 撤回
      if (lowerKey === "z" && commandKey) {
        e.preventDefault()
        // 清除当前页数据
        const clear = () => {
          usePaperStore
            .getState()
            .group!.getItems({})
            .filter((item) => item.data.id)
            .forEach((item) => {
              item.remove()
            })
        }
        const data = useLabelStore.getState().popStateStack()
        console.log("弹出栈的数据", data)
        if (data) {
          clear()
          const imageId = useBottomToolsStore.getState().activeImage
          const pathGroupMap = new Map()
          for (const [imageKey, categories] of data) {
            const groupMap = new Map()
            for (const [key, value] of categories) {
              value.forEach((item) => {
                const vId = item[2].parentGroupId
                if (vId)
                  if (groupMap.has(vId)) groupMap.get(vId).push(item[0])
                  else groupMap.set(vId, [item[0]])
              })
              if (imageId === imageKey)
                paperContainerRef.current.renderPolygons(key, categories)
            }
            pathGroupMap.set(imageKey, groupMap)
          }
          useRightToolsStore.getState().setPathGroupMap(pathGroupMap)

          console.log(pathGroupMap)
          // if (data.size) {
          //   if (data.has(imageId)) {
          //     const map = data.get(imageId);
          //     if (map) {
          //       for (const [key, value] of map) {
          //         console.log(key, value);
          //         paperContainerRef.current.renderPolygons(key, map);
          //       }
          //     }
          //   }
          // }
          // 更新当前数据
          useLabelStore.getState().setLabel(safeClone(data))
        }
      }

      if (e.key === "p") {
        e.preventDefault()
        if (projectDetail?.label_type === 6 && qaToolContainerRef.current)
          qaToolContainerRef.current.checkWrongWords()
      }

      // if (e.key === "=" && (e.metaKey || e.ctrlKey)) {
      //   e.preventDefault();
      //   setScale((num) => {
      //     let newScale = num + 0.1;
      //     return newScale;
      //   });
      // } else if (e.key === "-" && (e.metaKey || e.ctrlKey)) {
      //   e.preventDefault();
      //   setScale((num) => {
      //     if (num <= 1) {
      //       let newScale = num * 0.9;
      //       return newScale;
      //     } else {
      //       let newScale = num - 0.1;
      //       return newScale;
      //     }
      //   });
      // } else if (e.key === "3") {
      //   e.preventDefault();
      // } else if (e.key === "4") {
      //   e.preventDefault();
      // } else if (e.key === "5") {
      //   e.preventDefault();
      // } else if (e.key === "a" && (e.metaKey || e.ctrlKey)) {
      //   e.preventDefault();
      // }
    }
    const up = (e: KeyboardEvent) => {
      // console.log("键盘抬起", e);
      if (e.key === "Shift") {
        setShift(false)
        useKeyboardStore.getState().setShift(false)
      }
      if (e.key === "Control" || e.key === "Meta") {
        useKeyboardStore.getState().setCtrl(false)
      }
    }
    const wheel = (e: WheelEvent) => {
      const target = e.target as HTMLElement | null
      if (target?.closest('[data-label-context-menu="true"]')) return
      e.preventDefault()
      const currentScale = useTopToolsStore.getState().scale
      let newScale = e.deltaY < 0 ? currentScale * 1.1 : currentScale * 0.9
      if (newScale >= 100) return
      setScale(newScale)
      setGroupScale(newScale)
    }

    const mouseclick = (_e: MouseEvent) => {
      useTopToolsStore.getState().setSubAttrPresetShow(false)
    }

    const mousemove = (event: { clientX: number; clientY: number }) => {
      paperContainerRef.current.handleCrosshairMove(event)
    }

    let element = document.getElementById("resize-container")
    document.addEventListener("keydown", down)
    document.addEventListener("keyup", up)
    element?.addEventListener("wheel", wheel)
    element?.addEventListener("mousedown", mouseclick)
    element?.addEventListener("mousemove", mousemove)

    return () => {
      document.removeEventListener("keydown", down)
      document.removeEventListener("keyup", up)
      element?.removeEventListener("wheel", wheel)
      element?.removeEventListener("mousedown", mouseclick)
      element?.removeEventListener("mousemove", mousemove)
    }
  }, [
    applySelectedBrushIntersectUnite,
    applySelectedPolygonOverlap,
    editMode,
    enterSelectedObjectDrawMode,
    focusInput,
    getOperationSchema,
    handleToggleHideSelectedObject,
    handleMergeSelectedObjects,
    handleContinueSelectedPolygon,
    handleShortcut,
    isView,
    renderMode,
    projectDetail?.label_type,
    setEditMode,
    setGroupScale,
    setDrawOption,
    setPressA,
    setScale,
    setShift,
    resolveSelectedObjectContext,
  ])

  useEffect(() => {
    if (drawAction === "none" || pressA) return
    restoreDrawShortcutSnapshot()
  }, [drawAction, pressA, restoreDrawShortcutSnapshot])

  const setGlobalLoading = useCallback((flag: boolean) => {
    setLoading(flag)
  }, [])

  // 切换图片时清空复制保存的数组
  useEffect(() => {
    useKeyboardStore.getState().setCopyDataIds([])
    useKeyboardStore.getState().setDrawAction("none")
    usePaperStore.getState().setContinuePolygonTarget(null)
    drawShortcutSnapshotRef.current = null
  }, [activeImage])

  const handleBeforeUnload = (e: any) => {
    e.preventDefault()
    // 备份
    topRef.current.handleBackup("auto_backup")
    // 清空操作栏状态
    useObjectStore.getState().resetPathAndOperationStatus()
  }

  // 更新Splitter状态
  const updateSplitterSizes = useCallback(() => {
    console.log(
      "document.documentElement.clientWidth",
      document.documentElement.clientWidth
    )

    const len = document.documentElement.clientWidth - 4
    let arr = [len, 0, 0, 0, 0, 0]
    const flags = [
      showTaskList,
      showGroupList,
      showDescList && projectDetail && projectDetail.label_type === 5,
      showDescList && projectDetail && projectDetail.label_type === 6,
      showObjectList,
    ]
    let count = 0
    flags.forEach((flag) => {
      if (flag) count++
    })
    if (count === 1 || count === 2) {
      arr[0] = count === 1 ? len * 0.8 : len * 0.6
      flags.forEach((flag, index) => {
        if (flag) {
          arr[index + 1] = len * 0.2
        }
      })
    } else if (count === 3) {
      arr[0] = len * 0.55
      flags.forEach((flag, index) => {
        if (flag) {
          arr[index + 1] = len * 0.15
        }
      })
    } else if (count === 4) {
      arr[0] = len * 0.6
      flags.forEach((flag, index) => {
        if (flag) {
          arr[index + 1] = len * 0.1
        }
      })
    }
    setSizes(arr)
  }, [projectDetail, showDescList, showGroupList, showObjectList, showTaskList])

  useEffect(() => {
    updateSplitterSizes()
  }, [updateSplitterSizes])

  useEffect(() => {
    if (!isView) {
      const delay = 10 * 60000
      // 编辑模式下，修改labelData时启动或更新定时器
      useTimerStore.getState().startTimer(() => {
        useTopToolsStore.getState().setIsView(true)
      }, delay)
    } else {
      useTimerStore.getState().clearTimer()
    }

    // 组件卸载时清理定时器
    return () => {
      useTimerStore.getState().clearTimer()
    }
  }, [isView, labelData])

  // auto save
  useEffect(() => {
    let cancelled = false
    if (autoSaveTimerRef.current !== null) {
      clearTimeout(autoSaveTimerRef.current)
      autoSaveTimerRef.current = null
    }
    if (!isAutoSave || isView) return

    const minute = 60 * 1000
    const delay = autoSaveGap * minute
    const scheduleNext = () => {
      autoSaveTimerRef.current = setTimeout(async () => {
        if (cancelled) return
        if (autoSaveInProgressRef.current) {
          scheduleNext()
          return
        }
        autoSaveInProgressRef.current = true
        try {
          await topRef.current?.handleSave?.()
        } catch (error) {
          console.error("[LabelNossr] auto save failed", error)
        } finally {
          autoSaveInProgressRef.current = false
          if (!cancelled) scheduleNext()
        }
      }, delay)
    }
    scheduleNext()
    return () => {
      cancelled = true
      if (autoSaveTimerRef.current !== null) {
        clearTimeout(autoSaveTimerRef.current)
        autoSaveTimerRef.current = null
      }
    }
  }, [autoSaveGap, isAutoSave, isView])

  useEffect(() => {
    window.addEventListener("beforeunload", handleBeforeUnload)
    return () => {
      window.removeEventListener("beforeunload", handleBeforeUnload)
    }
  }, [])

  const bottomToolsHeight = useMemo(() => {
    if (projectDetail?.label_type === 5 && metaOperation) {
      return 128
    }
    return 80
  }, [metaOperation, projectDetail?.label_type])

  const mainBoxHeight = useMemo(() => {
    return `calc(100% - ${headerHeight}px - ${70 + bottomToolsHeight}px)`
  }, [bottomToolsHeight, headerHeight])

  const selectedTagPreviewList = useMemo(() => {
    if (!showTags) return []
    const selectedIds = selectedPathMap[activeImage] || []
    if (!selectedIds.length) return []
    const imageLabel = labelData.get(activeImage)
    if (!imageLabel) return []

    const selectedIdSet = new Set(selectedIds)
    const rawItems: Array<{ id: number; content: string }> = []

    imageLabel.forEach((labelList, categoryId) => {
      const category = objectOperations.find(
        (item) => item.category_id.toString() === categoryId
      )
      const labelClass = category?.label_class || ""
      labelList.forEach(([id, _points, detail]) => {
        if (!selectedIdSet.has(id)) return
        const parentGroupId =
          usePaperStore.getState().getItemById(id)?.data?.parentGroupId ??
          detail?.parentGroupId
        let content = ""
        if (showTagsConfigs.includes("ID")) {
          content += `${id}${parentGroupId ? `@${parentGroupId}` : ""}`
        }
        if (showTagsConfigs.includes("类别")) {
          content += `${content ? " " : ""}${labelClass}`
        }
        if (
          detail?.sub_attributes &&
          Object.keys(detail.sub_attributes).length
        ) {
          Object.keys(detail.sub_attributes).forEach((key) => {
            if (showTagsConfigs.includes("子属性")) content += ` ${key}:`
            if (showTagsConfigs.includes("属性值"))
              content += `${detail.sub_attributes[key]};`
          })
        }

        const normalized = content.trim()
        if (!normalized) return
        rawItems.push({ id, content: normalized })
      })
    })

    return selectedIds
      .map((id) => rawItems.find((item) => item.id === id))
      .filter((item): item is { id: number; content: string } => !!item)
  }, [
    activeImage,
    labelData,
    objectOperations,
    selectedPathMap,
    showTags,
    showTagsConfigs,
  ])

  const fullscreenRef = useRef<HTMLDivElement>(null)

  return (
    <Stack w="100%" h="100vh" pos="relative" ref={fullscreenRef} gap={0}>
      <LoadingOverlay
        // 视频场景仅阻塞到首帧可见；后续抽帧在后台继续，页面保持可操作。
        visible={loading || (videoFetchPendingCount > 0 && !hasReadyVideoFrame)}
        zIndex={GLOBAL_LOADING_Z_INDEX}
        overlayProps={{ radius: "sm", blur: 2 }}
      />
      <Box w="100%" h={80} pos="relative">
        <TopTools
          ref={topRef}
          taskDetail={taskDetail}
          projectDetail={projectDetail}
          oldWorkLoad={oldWorkLoad}
          updateOldWorkLoad={updateOldWorkLoad}
          isLabelTask={isLabelTask}
          renderPolygons={
            paperContainerRef.current
              ? paperContainerRef.current.renderPolygons
              : null
          }
        />
        {sourceVideoNames.length > 0 && (
          <Flex
            pos="absolute"
            top={8}
            right={12}
            style={{ zIndex: 2101 }}
            gap="xs">
            <Button
              size="xs"
              variant="light"
              // 当前仅下载第一个视频，便于快速核对源视频与抽帧质量。
              loading={downloadingVideoName === sourceVideoNames[0]}
              onClick={handleDownloadSourceVideo}>
              {sourceVideoNames.length > 1
                ? `下载源视频(1/${sourceVideoNames.length})`
                : "下载源视频"}
            </Button>
          </Flex>
        )}
      </Box>
      <Flex w="calc(100% - 4px)" h={mainBoxHeight}>
        {/* <Flex h="100%" w={"100%"}> */}
        <Box w={sizes[0] - leftWidth} style={{ flexShrink: 0 }}>
          <Box h="100%" id="resize-container">
            <Flex h="100%" w="100%">
              <Flex
                direction="column"
                h="100%"
                w={`calc(100% - 64px)`}
                pos="relative"
                style={{ overflow: "hidden" }}>
                <ScaleComponent
                  options={{
                    offsetX: 0,
                    offsetY: 0,
                    scale: scale,
                  }}
                  mode="horizontal"
                />
                <Flex h="calc(100% - 32px)">
                  <ScaleComponent
                    options={{
                      offsetX: 0,
                      offsetY: 0,
                      scale: scale,
                    }}
                    mode="vertical"
                  />
                  <Box pos="relative" style={{ flex: 1, minWidth: 0 }}>
                    <PaperContainer
                      imgSrc={"/test.jpeg"}
                      ref={paperContainerRef}
                      forceUpdate={forceUpdate}
                      labelState={labelState}
                      projectDetail={projectDetail}
                      updateLoadingFlag={setGlobalLoading}
                    />
                    {selectedTagPreviewList.length > 0 && (
                      <Box
                        pos="absolute"
                        top={8}
                        right={8}
                        p="xs"
                        maw={360}
                        miw={200}
                        style={{
                          zIndex: 100,
                          pointerEvents: "none",
                          borderRadius: 8,
                          background: "rgba(255, 255, 255, 0.9)",
                          border:
                            "1px solid light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-4))",
                        }}>
                        <Box fz={12} fw={600} mb={4}>
                          选中对象标签
                        </Box>
                        <Stack gap={4}>
                          {selectedTagPreviewList.map((item) => (
                            <Box
                              key={item.id}
                              fz={12}
                              style={{
                                lineHeight: 1.4,
                                wordBreak: "break-word",
                              }}>
                              {item.content}
                            </Box>
                          ))}
                        </Stack>
                      </Box>
                    )}
                  </Box>
                </Flex>
              </Flex>
              <ScaleToolContainer />
            </Flex>
          </Box>
        </Box>
        <Box
          w={sizes[1]}
          miw={showTaskList ? 300 : 0}
          style={{
            position: "relative",
            flexShrink: 0,
            overflow: "hidden",
            opacity: showTaskList ? 1 : 0,
            visibility: showTaskList ? "visible" : "hidden",
            pointerEvents: showTaskList ? "auto" : "none",
            transition: RIGHT_PANEL_TRANSITION,
            willChange: RIGHT_PANEL_WILL_CHANGE,
          }}>
          <Box style={getRightPanelViewportStyle(Math.max(sizes[1], 300))}>
            <RightTaskTools
              projectDetail={projectDetail}
              project_id={projectId}
              task_id={taskId}
            />
          </Box>
        </Box>
        <Box
          w={sizes[2]}
          miw={showGroupList ? 200 : 0}
          style={{
            position: "relative",
            flexShrink: 0,
            overflow: "hidden",
            opacity: showGroupList ? 1 : 0,
            visibility: showGroupList ? "visible" : "hidden",
            pointerEvents: showGroupList ? "auto" : "none",
            transition: RIGHT_PANEL_TRANSITION,
            willChange: RIGHT_PANEL_WILL_CHANGE,
          }}>
          <Box style={getRightPanelViewportStyle(Math.max(sizes[2], 200))}>
            <RightGroupTools
              labelState={labelState}
              projectDetail={projectDetail}
            />
          </Box>
        </Box>
        <Box
          w={sizes[3]}
          miw={
            showDescList && projectDetail && projectDetail.label_type === 5
              ? 350
              : 0
          }
          style={{
            position: "relative",
            flexShrink: 0,
            overflow: "hidden",
            opacity:
              showDescList && projectDetail && projectDetail.label_type === 5
                ? 1
                : 0,
            visibility:
              showDescList && projectDetail && projectDetail.label_type === 5
                ? "visible"
                : "hidden",
            pointerEvents:
              showDescList && projectDetail && projectDetail.label_type === 5
                ? "auto"
                : "none",
            transition: RIGHT_PANEL_TRANSITION,
            willChange: RIGHT_PANEL_WILL_CHANGE,
          }}>
          <Box style={getRightPanelViewportStyle(Math.max(sizes[3], 350))}>
            <RightDescTools taskDetail={taskDetail} />
          </Box>
        </Box>
        <Box
          w={sizes[4]}
          miw={
            showDescList && projectDetail && projectDetail.label_type === 6
              ? 350
              : 0
          }
          style={{
            position: "relative",
            flexShrink: 0,
            overflow: "hidden",
            opacity:
              showDescList && projectDetail && projectDetail.label_type === 6
                ? 1
                : 0,
            visibility:
              showDescList && projectDetail && projectDetail.label_type === 6
                ? "visible"
                : "hidden",
            pointerEvents:
              showDescList && projectDetail && projectDetail.label_type === 6
                ? "auto"
                : "none",
            transition: RIGHT_PANEL_TRANSITION,
            willChange: RIGHT_PANEL_WILL_CHANGE,
          }}>
          <Box style={getRightPanelViewportStyle(Math.max(sizes[4], 350))}>
            <RightQATools ref={qaToolContainerRef} taskDetail={taskDetail} />
          </Box>
        </Box>
        <Box
          w={sizes[5]}
          miw={showObjectList ? 350 : 0}
          style={{
            position: "relative",
            flexShrink: 0,
            overflow: "hidden",
            opacity: showObjectList ? 1 : 0,
            visibility: showObjectList ? "visible" : "hidden",
            pointerEvents: showObjectList ? "auto" : "none",
            transition: RIGHT_PANEL_TRANSITION,
            willChange: RIGHT_PANEL_WILL_CHANGE,
          }}>
          <Box style={getRightPanelViewportStyle(Math.max(sizes[5], 350))}>
            <RightObjectTools
              taskDetail={taskDetail}
              labelState={labelState}
              projectDetail={projectDetail}
            />
          </Box>
        </Box>
        {/* </Flex> */}
      </Flex>
      <Box
        w="100%"
        h={bottomToolsHeight}
        style={{
          overflow: "hidden",
          opacity: showMultiFrame ? 1 : 0,
          pointerEvents: showMultiFrame ? "auto" : "none",
          transition: "opacity 160ms ease",
        }}>
        <BottomTools
          labelState={labelState}
          projectDetail={projectDetail}
          taskDetail={taskDetail}
          renderPolygons={
            paperContainerRef.current
              ? paperContainerRef.current.renderPolygons
              : null
          }
        />
      </Box>
      {isConfirmSave && (
        <ConfirmModal
          open={isConfirmSave}
          title="保存标注结果"
          content="是否确定保存当前标注结果？"
          onOk={async () => {
            try {
              await topRef.current?.handleSave()
              setIsConfirmSave(false)
            } catch {}
          }}
          onCancel={() => {
            setIsConfirmSave(false)
          }}
        />
      )}
    </Stack>
  )
}

export default LabelPage
