"use client"

import { memo, useCallback, useEffect, useRef, useState } from "react"
import { NodeViewWrapper, type NodeViewProps } from "@tiptap/react"
import {
  computeGraphicLayoutStyle,
  resizeWithHandle,
  type ResizeHandle,
  RESIZE_HANDLES,
} from "@/lib/drive/docs-graphic-layout"
import {
  computeCropImageStyle,
  parseGraphicAttrs,
  type DocsGraphicAttrs,
  type DocsShapeType,
} from "@/lib/drive/docs-graphic-types"
import { DocsGraphicCropOverlay } from "@/components/drive/richtext/docs-graphic-crop-overlay"
import { DocsGraphicContextMenu } from "@/components/drive/richtext/docs-graphic-context-menu"
import { cn } from "@/lib/utils"

function ShapePreview({
  shapeType,
  fill,
  stroke,
  strokeWidth,
}: {
  shapeType: DocsShapeType
  fill: string
  stroke: string
  strokeWidth: number
}) {
  if (shapeType === "ellipse") {
    return (
      <svg width="100%" height="100%" viewBox="0 0 100 100" aria-hidden>
        <ellipse cx="50" cy="50" rx="46" ry="40" fill={fill} stroke={stroke} strokeWidth={strokeWidth} />
      </svg>
    )
  }
  if (shapeType === "line") {
    return (
      <svg width="100%" height="100%" viewBox="0 0 100 100" aria-hidden>
        <line x1="8" y1="50" x2="92" y2="50" stroke={stroke} strokeWidth={strokeWidth + 1} />
      </svg>
    )
  }
  if (shapeType === "arrow") {
    return (
      <svg width="100%" height="100%" viewBox="0 0 100 100" aria-hidden>
        <line x1="10" y1="50" x2="78" y2="50" stroke={stroke} strokeWidth={strokeWidth + 1} />
        <polygon points="78,38 92,50 78,62" fill={stroke} />
      </svg>
    )
  }
  return (
    <svg width="100%" height="100%" viewBox="0 0 100 100" aria-hidden>
      <rect x="6" y="10" width="88" height="80" rx="4" fill={fill} stroke={stroke} strokeWidth={strokeWidth} />
    </svg>
  )
}

function GraphicContent({ attrs }: { attrs: DocsGraphicAttrs }) {
  if (attrs.graphicType === "image") {
    if (!attrs.src) {
      return (
        <div className="flex h-full w-full items-center justify-center bg-[#f1f3f4] text-xs text-[#5f6368]">
          Image
        </div>
      )
    }
    const cropStyle = computeCropImageStyle(attrs)
    return (
      <img
        src={attrs.src}
        alt={attrs.alt || ""}
        draggable={false}
        className="block h-full w-full"
        style={{
          objectFit: Object.keys(cropStyle.img).length ? undefined : "contain",
          ...cropStyle.img,
          clipPath: cropStyle.clipPath,
        }}
      />
    )
  }

  if (attrs.graphicType === "gradient") {
    return (
      <div
        className="h-full w-full"
        style={{ background: attrs.gradientCss }}
        aria-hidden
      />
    )
  }

  return (
    <ShapePreview
      shapeType={attrs.shapeType}
      fill={attrs.fill}
      stroke={attrs.stroke}
      strokeWidth={attrs.strokeWidth}
    />
  )
}

function ResizeHandleBtn({
  handle,
  onPointerDown,
}: {
  handle: ResizeHandle
  onPointerDown: (handle: ResizeHandle, event: React.PointerEvent) => void
}) {
  const posClass: Record<ResizeHandle, string> = {
    nw: "left-0 top-0 cursor-nwse-resize",
    n: "left-1/2 top-0 -translate-x-1/2 cursor-ns-resize",
    ne: "right-0 top-0 cursor-nesw-resize",
    e: "right-0 top-1/2 -translate-y-1/2 cursor-ew-resize",
    se: "right-0 bottom-0 cursor-nwse-resize",
    s: "left-1/2 bottom-0 -translate-x-1/2 cursor-ns-resize",
    sw: "left-0 bottom-0 cursor-nesw-resize",
    w: "left-0 top-1/2 -translate-y-1/2 cursor-ew-resize",
  }

  return (
    <span
      role="presentation"
      className={cn(
        "docs-graphic-handle absolute z-20 size-2.5 rounded-full border border-white bg-[#1a73e8] shadow",
        posClass[handle]
      )}
      onPointerDown={(event) => {
        event.preventDefault()
        event.stopPropagation()
        onPointerDown(handle, event)
      }}
    />
  )
}

function RotationHandle({
  onPointerDown,
}: {
  onPointerDown: (event: React.PointerEvent) => void
}) {
  return (
    <span
      role="presentation"
      className="docs-graphic-rotate-handle absolute left-1/2 top-0 z-20 flex -translate-x-1/2 -translate-y-[calc(100%+8px)] cursor-grab items-center justify-center"
      onPointerDown={(event) => {
        event.preventDefault()
        event.stopPropagation()
        onPointerDown(event)
      }}
    >
      <span className="size-2.5 rounded-full border border-white bg-[#1a73e8] shadow" />
      <span className="absolute top-full h-2 w-px bg-[#1a73e8]" aria-hidden />
    </span>
  )
}

function DocsGraphicNodeViewInner({
  node,
  updateAttributes,
  selected,
  editor,
  getPos,
  extension,
}: NodeViewProps) {
  const attrs = parseGraphicAttrs(node.attrs as Record<string, unknown>)
  const layout = computeGraphicLayoutStyle(attrs)
  const editable = editor.isEditable
  const inline = extension.name === "docsInlineGraphic"
  const replaceInputRef = useRef<HTMLInputElement>(null)
  const dragRef = useRef<{ startX: number; startY: number; originX: number; originY: number } | null>(
    null
  )
  const pendingDragRef = useRef<{
    pointerId: number
    startX: number
    startY: number
    originX: number
    originY: number
    host: HTMLElement
  } | null>(null)
  const resizeRef = useRef<{
    handle: ResizeHandle
    startX: number
    startY: number
    width: number
    height: number
    originX: number
    originY: number
    lockAspect: boolean
  } | null>(null)
  const rotateRef = useRef<{
    centerX: number
    centerY: number
    startAngle: number
    originRotation: number
  } | null>(null)
  const [interacting, setInteracting] = useState(false)
  const [trackingPointer, setTrackingPointer] = useState(false)
  const [cropMode, setCropMode] = useState(false)

  useEffect(() => {
    if (!selected) setCropMode(false)
  }, [selected])

  const onDragPointerDown = useCallback(
    (event: React.PointerEvent) => {
      if (!editable || !selected || cropMode) return
      if ((event.target as HTMLElement).closest(".docs-graphic-handle, .docs-graphic-rotate-handle, .docs-graphic-crop")) {
        return
      }

      event.stopPropagation()

      const host = event.currentTarget as HTMLElement
      pendingDragRef.current = {
        pointerId: event.pointerId,
        startX: event.clientX,
        startY: event.clientY,
        originX: attrs.x,
        originY: attrs.y,
        host,
      }
      setTrackingPointer(true)
    },
    [attrs.x, attrs.y, cropMode, editable, selected]
  )

  const onResizeStart = useCallback(
    (handle: ResizeHandle, event: React.PointerEvent) => {
      if (!editable || cropMode) return
      event.preventDefault()
      ;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
      resizeRef.current = {
        handle,
        startX: event.clientX,
        startY: event.clientY,
        width: attrs.width,
        height: attrs.height,
        originX: attrs.x,
        originY: attrs.y,
        lockAspect: event.shiftKey,
      }
      setInteracting(true)
    },
    [attrs.height, attrs.width, attrs.x, attrs.y, cropMode, editable]
  )

  const onRotateStart = useCallback(
    (event: React.PointerEvent) => {
      if (!editable || cropMode) return
      event.preventDefault()
      ;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
      const host = (event.currentTarget as HTMLElement).closest(".docs-graphic") as HTMLElement
      const rect = host.getBoundingClientRect()
      const centerX = rect.left + rect.width / 2
      const centerY = rect.top + rect.height / 2
      const startAngle = Math.atan2(event.clientY - centerY, event.clientX - centerX)
      rotateRef.current = {
        centerX,
        centerY,
        startAngle,
        originRotation: attrs.rotationDeg,
      }
      setInteracting(true)
    },
    [attrs.rotationDeg, cropMode, editable]
  )

  useEffect(() => {
    if (!interacting && !trackingPointer) return

    const onMove = (event: PointerEvent) => {
      if (pendingDragRef.current && !dragRef.current) {
        const pending = pendingDragRef.current
        if (pending.pointerId !== event.pointerId) return
        const dx = event.clientX - pending.startX
        const dy = event.clientY - pending.startY
        if (Math.hypot(dx, dy) < 4) return

        const prose = pending.host.closest(".ProseMirror") as HTMLElement | null
        let originX = pending.originX
        let originY = pending.originY

        const needsAbsolute =
          attrs.placement !== "absolute" &&
          attrs.wrap !== "behind" &&
          attrs.wrap !== "in-front"

        if (needsAbsolute && prose) {
          const proseRect = prose.getBoundingClientRect()
          const rect = pending.host.getBoundingClientRect()
          originX = Math.round(rect.left - proseRect.left)
          originY = Math.round(rect.top - proseRect.top)
          updateAttributes({
            placement: "absolute",
            x: originX,
            y: originY,
          })
        }

        pending.host.setPointerCapture(event.pointerId)
        dragRef.current = {
          startX: pending.startX,
          startY: pending.startY,
          originX,
          originY,
        }
        pendingDragRef.current = null
        setInteracting(true)
      }
      if (dragRef.current) {
        const { startX, startY, originX, originY } = dragRef.current
        updateAttributes({
          x: Math.round(originX + (event.clientX - startX)),
          y: Math.round(originY + (event.clientY - startY)),
        })
      }
      if (resizeRef.current) {
        const { handle, startX, startY, width, height, originX, originY, lockAspect } =
          resizeRef.current
        const next = resizeWithHandle(
          handle,
          width,
          height,
          event.clientX - startX,
          event.clientY - startY,
          24,
          lockAspect || event.shiftKey
        )
        const patch: Partial<DocsGraphicAttrs> = {
          width: next.width,
          height: next.height,
        }
        if (
          attrs.placement === "absolute" ||
          attrs.wrap === "behind" ||
          attrs.wrap === "in-front"
        ) {
          patch.x = Math.round(originX + next.xOffset)
          patch.y = Math.round(originY + next.yOffset)
        }
        updateAttributes(patch)
      }
      if (rotateRef.current) {
        const { centerX, centerY, startAngle, originRotation } = rotateRef.current
        const angle = Math.atan2(event.clientY - centerY, event.clientX - centerX)
        const delta = ((angle - startAngle) * 180) / Math.PI
        updateAttributes({ rotationDeg: Math.round(originRotation + delta) })
      }
    }

    const onUp = () => {
      dragRef.current = null
      pendingDragRef.current = null
      resizeRef.current = null
      rotateRef.current = null
      setInteracting(false)
      setTrackingPointer(false)
    }

    window.addEventListener("pointermove", onMove)
    window.addEventListener("pointerup", onUp)
    return () => {
      window.removeEventListener("pointermove", onMove)
      window.removeEventListener("pointerup", onUp)
    }
  }, [attrs.placement, attrs.wrap, interacting, trackingPointer, updateAttributes])

  const selectNode = useCallback(() => {
    const pos = getPos()
    if (typeof pos !== "number") return
    editor.chain().focus().setNodeSelection(pos).run()
  }, [editor, getPos])

  const replaceImage = useCallback(() => {
    replaceInputRef.current?.click()
  }, [])

  const graphicBody = (
    <div
      className={cn(
        "docs-graphic",
        selected && "docs-graphic--selected",
        interacting && "docs-graphic--interacting",
        cropMode && "docs-graphic--cropping",
        layout.behindText && "docs-graphic--behind",
        layout.inFrontText && "docs-graphic--front"
      )}
      style={layout.inner}
      contentEditable={false}
      onPointerDown={(event) => {
        selectNode()
        onDragPointerDown(event)
      }}
      onDoubleClick={(event) => {
        if (!editable || attrs.graphicType !== "image") return
        event.preventDefault()
        event.stopPropagation()
        setCropMode(true)
      }}
    >
      <div className="docs-graphic__content" style={layout.content}>
        <GraphicContent attrs={attrs} />
      </div>
      {selected && editable && cropMode && attrs.graphicType === "image" ? (
        <DocsGraphicCropOverlay
          attrs={attrs}
          frameWidth={attrs.width}
          frameHeight={attrs.height}
          onChange={updateAttributes}
          onDone={() => setCropMode(false)}
        />
      ) : null}
      {selected && editable && !cropMode ? (
        <>
          <span className="docs-graphic-outline" aria-hidden />
          <RotationHandle onPointerDown={onRotateStart} />
          {RESIZE_HANDLES.map((handle) => (
            <ResizeHandleBtn key={handle} handle={handle} onPointerDown={onResizeStart} />
          ))}
        </>
      ) : null}
      <input
        ref={replaceInputRef}
        type="file"
        accept="image/*"
        className="hidden"
        onChange={(event) => {
          const file = event.target.files?.[0]
          if (!file) return
          const reader = new FileReader()
          reader.onload = () => {
            updateAttributes({ src: reader.result as string })
          }
          reader.readAsDataURL(file)
          event.target.value = ""
        }}
      />
    </div>
  )

  return (
    <NodeViewWrapper
      as={inline ? "span" : "div"}
      className={cn("docs-graphic-host", inline && "docs-graphic-host--inline")}
      style={layout.wrapper}
      data-graphic-type={attrs.graphicType}
      data-wrap={attrs.wrap}
      data-placement={attrs.placement}
    >
      {selected && editable ? (
        <DocsGraphicContextMenu
          editor={editor}
          onCrop={() => setCropMode(true)}
          onReplaceImage={replaceImage}
        >
          {graphicBody}
        </DocsGraphicContextMenu>
      ) : (
        graphicBody
      )}
    </NodeViewWrapper>
  )
}

export const DocsGraphicNodeView = memo(DocsGraphicNodeViewInner)
