import {
    CSSProperties,
    ReactElement,
    useCallback,
    useEffect,
    useRef,
    useState,
} from "react"
import { angle, middlePos, Pos, relativeTo } from "./Pos"

import "../../style/bendable_arrows.css"
import Draggable from "react-draggable"

export interface BendableArrowProps {
    startPos: Pos
    segments: Segment[]
    onSegmentsChanges: (edges: Segment[]) => void

    startRadius?: number
    endRadius?: number

    style?: ArrowStyle
}

export interface ArrowStyle {
    width?: number
    head?: () => ReactElement
    tail?: () => ReactElement
}

const ArrowStyleDefaults = {
    width: 4,
}

export interface Segment {
    next: Pos
    controlPoint?: Pos
}

function constraintInCircle(pos: Pos, from: Pos, radius: number): Pos {
    const theta = angle(pos, from)

    return {
        x: pos.x - Math.sin(theta) * radius,
        y: pos.y - Math.cos(theta) * radius,
    }
}

function Triangle({ fill }: { fill: string }) {
    return (
        <svg viewBox={"0 0 50 50"} width={20} height={20}>
            <polygon points={"50 0, 0 0, 25 40"} fill={fill} />
        </svg>
    )
}

export default function BendableArrow({
    startPos,

    segments,
    onSegmentsChanges,

    style,
    startRadius = 0,
    endRadius = 0,
}: BendableArrowProps) {
    const containerRef = useRef<HTMLDivElement>(null)
    const svgRef = useRef<SVGSVGElement>(null)

    const pathRef = useRef<SVGPathElement>(null)

    const styleWidth = style?.width ?? ArrowStyleDefaults.width

    const [controlPointsDots, setControlPointsDots] = useState<ReactElement[]>(
        [],
    )

    useEffect(() => {
        setInternalSegments(segments)
    }, [segments])

    const [internalSegments, setInternalSegments] = useState(segments)

    const [isSelected, setIsSelected] = useState(false)

    const headRef = useRef<HTMLDivElement>(null)
    const tailRef = useRef<HTMLDivElement>(null)

    useEffect(() => {
        const basePos =
            containerRef.current!.parentElement!.getBoundingClientRect()

        setControlPointsDots(computeControlPoints(basePos))

        const selectionHandler = (e: MouseEvent) => {
            if (!(e.target instanceof Node)) return

            const isSelected = containerRef.current!.contains(e.target)
            setIsSelected(isSelected)
        }

        document.addEventListener("mousedown", selectionHandler)
        return () => document.removeEventListener("mousedown", selectionHandler)
    }, [])

    function computeControlPoints(basePos: Pos) {
        return internalSegments.map(({ next, controlPoint }, i) => {
            const prev = i == 0 ? startPos : internalSegments[i - 1].next

            const prevRelative = relativeTo(prev, basePos)
            const nextRelative = relativeTo(next, basePos)

            const cpPos = controlPoint
                ? relativeTo(controlPoint, basePos)
                : {
                      x: prevRelative.x / 2 + nextRelative.x / 2,
                      y: prevRelative.y / 2 + nextRelative.y / 2,
                  }

            return (
                <ControlPoint
                    key={i}
                    pos={cpPos}
                    basePos={basePos}
                    onPosValidated={(controlPoint) => {
                        const segment = internalSegments[i]
                        const segments = internalSegments.toSpliced(i, 1, {
                            ...segment,
                            controlPoint,
                        })
                        onSegmentsChanges(segments)
                    }}
                    onMoves={(controlPoint) => {
                        setInternalSegments((is) => {
                            return is.toSpliced(i, 1, {
                                ...is[i],
                                controlPoint,
                            })
                        })
                    }}
                />
            )
        })
    }

    const update = useCallback(() => {
        // only one segment is supported for now, which is the first.
        // any other segments will be ignored
        const segment = internalSegments[0] ?? null

        if (segment == null) throw new Error("segments might not be empty.")

        const endPos = segment.next

        const basePos =
            containerRef.current!.parentElement!.getBoundingClientRect()

        const startRelative = relativeTo(startPos, basePos)
        const endRelative = relativeTo(endPos!, basePos)

        const controlPoint = segment.controlPoint
            ? relativeTo(segment.controlPoint, basePos)
            : {
                  x: startRelative.x / 2 + endRelative.x / 2,
                  y: startRelative.y / 2 + endRelative.y / 2,
              }

        const tailPos = constraintInCircle(
            startRelative,
            controlPoint,
            startRadius!,
        )
        const headPos = constraintInCircle(
            endRelative,
            controlPoint,
            endRadius!,
        )

        const left = Math.min(tailPos.x, headPos.x)
        const top = Math.min(tailPos.y, headPos.y)

        Object.assign(tailRef.current!.style, {
            left: tailPos.x + "px",
            top: tailPos.y + "px",
            transformOrigin: "top center",
            transform: `translateX(-50%) rotate(${
                -angle(tailPos, controlPoint) * (180 / Math.PI)
            }deg)`,
        } as CSSProperties)

        Object.assign(headRef.current!.style, {
            left: headPos.x + "px",
            top: headPos.y + "px",
            transformOrigin: "top center",
            transform: `translateX(-50%) rotate(${
                -angle(headPos, controlPoint) * (180 / Math.PI)
            }deg)`,
        } as CSSProperties)

        const svgStyle: CSSProperties = {
            left: left + "px",
            top: top + "px",
        }

        const segmentsRelatives = internalSegments.map(
            ({ next, controlPoint }) => {
                return {
                    next: relativeTo(next, basePos),
                    cp: controlPoint
                        ? relativeTo(controlPoint, basePos)
                        : undefined,
                }
            },
        )

        const computedSegments = segmentsRelatives
            .map(({ next: n, cp }, idx) => {
                let next = n

                if (idx == internalSegments.length - 1) {
                    //if it is the last element
                    next = constraintInCircle(next, controlPoint, endRadius!)
                }

                if (cp == undefined) {
                    return `L${next.x - left} ${next.y - top}`
                }

                return `C${cp.x - left} ${cp.y - top}, ${cp.x - left} ${
                    cp.y - top
                }, ${next.x - left} ${next.y - top}`
            })
            .join(" ")

        const d = `M${tailPos.x - left} ${tailPos.y - top} ` + computedSegments
        pathRef.current!.setAttribute("d", d)
        Object.assign(svgRef.current!.style, svgStyle)

        setControlPointsDots(computeControlPoints(basePos))
    }, [startPos, internalSegments])

    useEffect(update, [update])

    return (
        <div
            ref={containerRef}
            style={{ position: "absolute", top: 0, left: 0 }}>
            <svg
                ref={svgRef}
                style={{
                    overflow: "visible",
                    position: "absolute",
                    pointerEvents: "none",
                }}>
                <path
                    className="arrow-path"
                    ref={pathRef}
                    stroke={"#000"}
                    strokeWidth={styleWidth}
                    fill="none"
                />
            </svg>

            <div
                className={"arrow-head"}
                style={{ position: "absolute", transformOrigin: "center" }}
                ref={headRef}>
                {style?.head?.call(style) ?? <Triangle fill={"red"} />}
            </div>

            <div
                className={"arrow-tail"}
                style={{ position: "absolute", transformOrigin: "center" }}
                ref={tailRef}>
                {style?.tail?.call(style) ?? <Triangle fill={"blue"} />}
            </div>

            {isSelected && controlPointsDots}
        </div>
    )
}

interface ControlPointProps {
    pos: Pos
    basePos: Pos
    onMoves: (currentPos: Pos) => void
    onPosValidated: (newPos: Pos | undefined) => void
    radius?: number
}

function ControlPoint({
    pos,
    onMoves,
    onPosValidated,
    radius = 7,
}: ControlPointProps) {
    const ref = useRef<HTMLDivElement>(null)

    return (
        <Draggable
            nodeRef={ref}
            onStop={() => {
                const pointPos = middlePos(ref.current!.getBoundingClientRect())
                onPosValidated(pointPos)
            }}
            onDrag={() => {
                const pointPos = middlePos(ref.current!.getBoundingClientRect())
                onMoves(pointPos)
            }}
            position={{ x: pos.x - radius, y: pos.y - radius }}>
            <div
                ref={ref}
                className={"arrow-edge-control-point"}
                style={{
                    position: "absolute",
                    width: radius * 2,
                    height: radius * 2,
                }}
                onKeyDown={(e) => {
                    if (e.key == "Delete") {
                        onPosValidated(undefined)
                    }
                }}
                tabIndex={0}
            />
        </Draggable>
    )
}
