import { css } from '@emotion/css';
import { useEffect, useMemo, useRef, useState } from 'react';

import { type GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { type DirectionDimensionConfig, DirectionDimensionMode, ConnectionDirection } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
import { type Scene } from 'app/features/canvas/runtime/scene';

import { type ConnectionCoordinates } from '../../panelcfg.gen';
import { type ConnectionState } from '../../types';
import {
  calculateAngle,
  calculateCoordinates,
  calculateDistance,
  calculateMidpoint,
  getConnectionStyles,
  getParentBoundingClientRect,
} from '../../utils';

import { CONNECTION_VERTEX_ADD_ID, CONNECTION_VERTEX_ID } from './Connections';

type Props = {
  setSVGRef: (anchorElement: SVGSVGElement) => void;
  setLineRef: (anchorElement: SVGLineElement) => void;
  setSVGVertexRef: (anchorElement: SVGSVGElement) => void;
  setVertexPathRef: (anchorElement: SVGPathElement) => void;
  setVertexRef: (anchorElement: SVGCircleElement) => void;
  scene: Scene;
};

let idCounter = 0;
const htmlElementTypes = ['input', 'textarea'];

export const ConnectionSVG = ({
  setSVGRef,
  setLineRef,
  setSVGVertexRef,
  setVertexPathRef,
  setVertexRef,
  scene,
}: Props) => {
  const styles = useStyles2(getStyles);

  const headId = Date.now() + '_' + idCounter++;
  const CONNECTION_LINE_ID = useMemo(() => `connectionLineId-${headId}`, [headId]);
  const EDITOR_HEAD_ID = useMemo(() => `editorHead-${headId}`, [headId]);
  const defaultArrowColor = config.theme2.colors.text.primary;
  const defaultArrowSize = 2;
  const defaultArrowDirection: DirectionDimensionConfig = {
    mode: DirectionDimensionMode.Fixed,
    fixed: ConnectionDirection.Forward,
  };
  const maximumVertices = 10;

  const [selectedConnection, setSelectedConnection] = useState<ConnectionState | undefined>(undefined);

  // Need to use ref to ensure state is not stale in event handler
  const selectedConnectionRef = useRef(selectedConnection);
  useEffect(() => {
    selectedConnectionRef.current = selectedConnection;
  });

  useEffect(() => {
    if (scene.panel.context.instanceState?.selectedConnection) {
      setSelectedConnection(scene.panel.context.instanceState?.selectedConnection);
    }
  }, [scene.panel.context.instanceState?.selectedConnection]);

  const onKeyUp = (e: KeyboardEvent) => {
    const target = e.target;

    if (!(target instanceof HTMLElement)) {
      return;
    }

    if (htmlElementTypes.indexOf(target.nodeName.toLowerCase()) > -1) {
      return;
    }

    // Backspace (8) or delete (46)
    if (e.keyCode === 8 || e.keyCode === 46) {
      if (selectedConnectionRef.current && selectedConnectionRef.current.source) {
        selectedConnectionRef.current.source.options.connections =
          selectedConnectionRef.current.source.options.connections?.filter(
            (connection) => connection !== selectedConnectionRef.current?.info
          );
        selectedConnectionRef.current.source.onChange(selectedConnectionRef.current.source.options);

        setSelectedConnection(undefined);
        scene.connections.select(undefined);
        scene.connections.updateState();
        scene.save();
      }
    } else {
      // Prevent removing event listener if key is not delete
      return;
    }

    document.removeEventListener('keyup', onKeyUp);
    scene.selecto!.rootContainer!.removeEventListener('click', clearSelectedConnection);
  };

  const clearSelectedConnection = (event: MouseEvent) => {
    const eventTarget = event.target;

    const shouldResetSelectedConnection = !(
      eventTarget instanceof SVGLineElement && eventTarget.id === CONNECTION_LINE_ID
    );

    if (shouldResetSelectedConnection) {
      setSelectedConnection(undefined);
      scene.connections.select(undefined);
    }
  };

  const selectConnection = (connection: ConnectionState) => {
    if (scene.isEditingEnabled) {
      setSelectedConnection(connection);
      scene.connections.select(connection);

      document.addEventListener('keyup', onKeyUp);
      scene.selecto!.rootContainer!.addEventListener('click', clearSelectedConnection);
    }
  };

  // Figure out target and then target's relative coordinates drawing (if no target do parent)
  const renderConnections = () => {
    return (
      scene.connections.state
        // Render selected connection last, ensuring it is above other connections
        .sort((_a, b) => (selectedConnection === b && scene.panel.context.instanceState.selectedConnection ? -1 : 0))
        .map((v, idx) => {
          const { source, target, info, vertices, index } = v;
          const sourceRect = source.div?.getBoundingClientRect();
          const parent = source.div?.parentElement;
          const transformScale = scene.scale;
          const parentRect = getParentBoundingClientRect(scene);

          if (!sourceRect || !parent || !parentRect) {
            return;
          }

          const { x1, y1, x2, y2 } = calculateCoordinates(sourceRect, parentRect, info, target, transformScale);

          let { xStart, yStart, xEnd, yEnd } = { xStart: x1, yStart: y1, xEnd: x2, yEnd: y2 };
          if (v.sourceOriginal && v.targetOriginal) {
            xStart = v.sourceOriginal.x;
            yStart = v.sourceOriginal.y;
            xEnd = v.targetOriginal.x;
            yEnd = v.targetOriginal.y;
          } else if (source.options.connections) {
            // If original source or target coordinates are not set for the current connection, set them
            if (
              !source.options.connections[index].sourceOriginal ||
              !source.options.connections[index].targetOriginal
            ) {
              source.options.connections[index].sourceOriginal = { x: x1, y: y1 };
              source.options.connections[index].targetOriginal = { x: x2, y: y2 };
            }
          }

          const midpoint = calculateMidpoint(x1, y1, x2, y2);
          const xDist = xEnd - xStart;
          const yDist = yEnd - yStart;

          const { strokeColor, strokeWidth, strokeRadius, arrowDirection, lineStyle, shouldAnimate } =
            getConnectionStyles(
              info,
              scene,
              defaultArrowSize,
              defaultArrowDirection.fixed ?? ConnectionDirection.Forward
            );

          const isSelected = selectedConnection === v && scene.panel.context.instanceState.selectedConnection;

          const connectionCursorStyle = scene.isEditingEnabled ? 'grab' : '';
          const selectedStyles = { stroke: '#44aaff', strokeOpacity: 0.6, strokeWidth: strokeWidth + 5 };

          const CONNECTION_HEAD_ID_START = `connectionHeadStart-${headId + Math.random()}`;
          const CONNECTION_HEAD_ID_END = `connectionHeadEnd-${headId + Math.random()}`;

          const radius = strokeRadius;
          // Create vertex path and populate array of add vertex controls
          const addVertices: ConnectionCoordinates[] = [];
          let pathString = `M${x1} ${y1} `;
          if (vertices?.length) {
            vertices.map((vertex, index) => {
              const x = vertex.x;
              const y = vertex.y;

              // Convert vertex relative coordinates to scene coordinates
              const X = x * xDist + xStart;
              const Y = y * yDist + yStart;

              // Initialize coordinates for first arc control point
              let xa = X;
              let ya = Y;

              // Initialize coordinates for second arc control point
              let xb = X;
              let yb = Y;

              // Initialize half arc distance and segment angles
              let lHalfArc = 0;
              let angle1 = 0;
              let angle2 = 0;

              // Only calculate arcs if there is a radius
              if (radius) {
                if (index < vertices.length - 1) {
                  const Xn = vertices[index + 1].x * xDist + xStart;
                  const Yn = vertices[index + 1].y * yDist + yStart;
                  if (index === 0) {
                    // First vertex
                    angle1 = calculateAngle(x1, y1, X, Y);
                    angle2 = calculateAngle(X, Y, Xn, Yn);
                  } else {
                    // All vertices
                    const previousVertex = vertices[index - 1];
                    const Xp = previousVertex.x * xDist + xStart;
                    const Yp = previousVertex.y * yDist + yStart;
                    angle1 = calculateAngle(Xp, Yp, X, Y);
                    angle2 = calculateAngle(X, Y, Xn, Yn);
                  }
                } else {
                  // Last vertex
                  if (index > 0) {
                    // Not also the first vertex
                    const previousVertex = vertices[index - 1];
                    const Xp = previousVertex.x * xDist + xStart;
                    const Yp = previousVertex.y * yDist + yStart;
                    angle1 = calculateAngle(Xp, Yp, X, Y);
                  } else {
                    angle1 = calculateAngle(x1, y1, X, Y);
                  }
                  angle2 = calculateAngle(X, Y, x2, y2);
                }

                // Calculate angle between two segments where arc will be placed
                const theta = angle2 - angle1; //radians
                // Attempt to determine if arc is counter clockwise (ccw)
                const ccw = theta < 0;
                // Half arc is used for arc control points
                lHalfArc = radius * Math.tan(theta / 2);
                if (ccw) {
                  lHalfArc *= -1;
                }
              }

              if (index === 0) {
                // For first vertex
                addVertices.push(
                  calculateMidpoint((x1 - xStart) / (xEnd - xStart), (y1 - yStart) / (yEnd - yStart), x, y)
                );

                // Only calculate arcs if there is a radius
                if (radius) {
                  // Length of segment
                  const lSegment = calculateDistance(X, Y, x1, y1);
                  if (Math.abs(lHalfArc) > 0.5 * Math.abs(lSegment)) {
                    // Limit curve control points to mid segment
                    lHalfArc = 0.5 * lSegment;
                  }
                  // Default next point to last point
                  let Xn = x2;
                  let Yn = y2;
                  if (index < vertices.length - 1) {
                    // Not also the last point
                    const nextVertex = vertices[index + 1];
                    Xn = nextVertex.x * xDist + xStart;
                    Yn = nextVertex.y * yDist + yStart;
                  }

                  // Length of next segment
                  const lSegmentNext = calculateDistance(X, Y, Xn, Yn);
                  if (Math.abs(lHalfArc) > 0.5 * Math.abs(lSegmentNext)) {
                    // Limit curve control points to mid segment
                    lHalfArc = 0.5 * lSegmentNext;
                  }
                  // Calculate arc control points
                  const lDelta = lSegment - lHalfArc;
                  xa = Math.round(lDelta * Math.cos(angle1) + x1);
                  ya = Math.round(lDelta * Math.sin(angle1) + y1);
                  xb = Math.round(lHalfArc * Math.cos(angle2) + X);
                  yb = Math.round(lHalfArc * Math.sin(angle2) + Y);

                  // Check if arc control points are inside of segment, otherwise swap sign
                  if ((xa > X && xa > x1) || (xa < X && xa < x1)) {
                    xa = (lDelta + 2 * lHalfArc) * Math.cos(angle1) + x1;
                    ya = (lDelta + 2 * lHalfArc) * Math.sin(angle1) + y1;
                    xb = -lHalfArc * Math.cos(angle2) + X;
                    yb = -lHalfArc * Math.sin(angle2) + Y;
                  }
                }
              } else {
                // For all other vertices
                const previousVertex = vertices[index - 1];
                addVertices.push(calculateMidpoint(previousVertex.x, previousVertex.y, x, y));

                // Only calculate arcs if there is a radius
                if (radius) {
                  // Convert previous vertex relative coorindates to scene coordinates
                  const Xp = previousVertex.x * xDist + xStart;
                  const Yp = previousVertex.y * yDist + yStart;

                  // Length of segment
                  const lSegment = calculateDistance(X, Y, Xp, Yp);
                  if (Math.abs(lHalfArc) > 0.5 * Math.abs(lSegment)) {
                    // Limit curve control points to mid segment
                    lHalfArc = 0.5 * lSegment;
                  }
                  // Default next point to last point
                  let Xn = x2;
                  let Yn = y2;
                  if (index < vertices.length - 1) {
                    // Not also the last point
                    const nextVertex = vertices[index + 1];
                    Xn = nextVertex.x * xDist + xStart;
                    Yn = nextVertex.y * yDist + yStart;
                  }

                  // Length of next segment
                  const lSegmentNext = calculateDistance(X, Y, Xn, Yn);
                  if (Math.abs(lHalfArc) > 0.5 * Math.abs(lSegmentNext)) {
                    // Limit curve control points to mid segment
                    lHalfArc = 0.5 * lSegmentNext;
                  }

                  // Calculate arc control points
                  const lDelta = lSegment - lHalfArc;
                  xa = Math.round(lDelta * Math.cos(angle1) + Xp);
                  ya = Math.round(lDelta * Math.sin(angle1) + Yp);
                  xb = Math.round(lHalfArc * Math.cos(angle2) + X);
                  yb = Math.round(lHalfArc * Math.sin(angle2) + Y);

                  // Check if arc control points are inside of segment, otherwise swap sign
                  if ((xa > X && xa > Xp) || (xa < X && xa < Xp)) {
                    xa = (lDelta + 2 * lHalfArc) * Math.cos(angle1) + Xp;
                    ya = (lDelta + 2 * lHalfArc) * Math.sin(angle1) + Yp;
                    xb = -lHalfArc * Math.cos(angle2) + X;
                    yb = -lHalfArc * Math.sin(angle2) + Y;
                  }
                }
              }
              if (index === vertices.length - 1) {
                // For last vertex only
                addVertices.push(
                  calculateMidpoint((x2 - xStart) / (xEnd - xStart), (y2 - yStart) / (yEnd - yStart), x, y)
                );
              }
              // Add segment to path
              pathString += `L${xa} ${ya} `;

              if (lHalfArc !== 0) {
                // Add arc if applicable
                pathString += `Q ${X} ${Y} ${xb} ${yb} `;
              }
            });
            // Add last segment
            pathString += `L${x2} ${y2}`;
          }

          const markerStart =
            arrowDirection === ConnectionDirection.Reverse || arrowDirection === ConnectionDirection.Both
              ? `url(#${CONNECTION_HEAD_ID_START})`
              : undefined;

          const markerEnd =
            arrowDirection === ConnectionDirection.Forward || arrowDirection === ConnectionDirection.Both
              ? `url(#${CONNECTION_HEAD_ID_END})`
              : undefined;

          const getAnimationDirection = () => {
            let values = '100;0';

            if (arrowDirection === ConnectionDirection.Reverse) {
              values = '0;100';
            }

            return values;
          };

          return (
            <svg className={styles.connection} key={idx}>
              <g onClick={() => selectConnection(v)}>
                <defs>
                  <marker
                    id={CONNECTION_HEAD_ID_START}
                    markerWidth="10"
                    markerHeight="7"
                    refX="0"
                    refY="3.5"
                    orient="auto"
                    stroke={strokeColor}
                  >
                    <polygon points="10 0, 0 3.5, 10 7" fill={strokeColor} />
                  </marker>
                  <marker
                    id={CONNECTION_HEAD_ID_END}
                    markerWidth="10"
                    markerHeight="7"
                    refX="10"
                    refY="3.5"
                    orient="auto"
                    stroke={strokeColor}
                  >
                    <polygon points="0 0, 10 3.5, 0 7" fill={strokeColor} />
                  </marker>
                </defs>
                {vertices?.length ? (
                  <g>
                    <path
                      id={`${CONNECTION_LINE_ID}_transparent`}
                      d={pathString}
                      cursor={connectionCursorStyle}
                      pointerEvents="auto"
                      stroke="transparent"
                      strokeWidth={15}
                      fill={'none'}
                      style={isSelected ? selectedStyles : {}}
                    />
                    <path
                      d={pathString}
                      stroke={strokeColor}
                      strokeWidth={strokeWidth}
                      strokeDasharray={lineStyle}
                      strokeDashoffset={1}
                      fill={'none'}
                      markerEnd={markerEnd}
                      markerStart={markerStart}
                    >
                      {shouldAnimate && (
                        <animate
                          attributeName="stroke-dashoffset"
                          values={getAnimationDirection()}
                          dur="5s"
                          calcMode="linear"
                          repeatCount="indefinite"
                          fill={'freeze'}
                        />
                      )}
                    </path>
                    {isSelected && (
                      <g>
                        {vertices.map((value, index) => {
                          return (
                            <circle
                              id={CONNECTION_VERTEX_ID}
                              data-index={index}
                              key={`${CONNECTION_VERTEX_ID}${index}_${idx}`}
                              cx={value.x * xDist + xStart}
                              cy={value.y * yDist + yStart}
                              r={5}
                              stroke={strokeColor}
                              className={styles.vertex}
                              cursor={'crosshair'}
                              pointerEvents="auto"
                            />
                          );
                        })}
                        {vertices.length < maximumVertices &&
                          addVertices.map((value, index) => {
                            return (
                              <circle
                                id={CONNECTION_VERTEX_ADD_ID}
                                data-index={index}
                                key={`${CONNECTION_VERTEX_ADD_ID}${index}_${idx}`}
                                cx={value.x * xDist + xStart}
                                cy={value.y * yDist + yStart}
                                r={4}
                                stroke={strokeColor}
                                className={styles.addVertex}
                                cursor={'crosshair'}
                                pointerEvents="auto"
                              />
                            );
                          })}
                      </g>
                    )}
                  </g>
                ) : (
                  <g>
                    <line
                      id={`${CONNECTION_LINE_ID}_transparent`}
                      cursor={connectionCursorStyle}
                      pointerEvents="auto"
                      stroke="transparent"
                      strokeWidth={15}
                      style={isSelected ? selectedStyles : {}}
                      x1={x1}
                      y1={y1}
                      x2={x2}
                      y2={y2}
                    />
                    <line
                      id={CONNECTION_LINE_ID}
                      stroke={strokeColor}
                      pointerEvents="auto"
                      strokeWidth={strokeWidth}
                      markerEnd={markerEnd}
                      markerStart={markerStart}
                      strokeDasharray={lineStyle}
                      strokeDashoffset={1}
                      x1={x1}
                      y1={y1}
                      x2={x2}
                      y2={y2}
                      cursor={connectionCursorStyle}
                    >
                      {shouldAnimate && (
                        <animate
                          attributeName="stroke-dashoffset"
                          values={getAnimationDirection()}
                          dur="5s"
                          calcMode="linear"
                          repeatCount="indefinite"
                          fill={'freeze'}
                        />
                      )}
                    </line>
                    {isSelected && (
                      <circle
                        id={CONNECTION_VERTEX_ADD_ID}
                        data-index={0}
                        cx={midpoint.x}
                        cy={midpoint.y}
                        r={4}
                        stroke={strokeColor}
                        className={styles.addVertex}
                        cursor={'crosshair'}
                        pointerEvents="auto"
                      />
                    )}
                  </g>
                )}
              </g>
            </svg>
          );
        })
    );
  };

  return (
    <>
      <svg ref={setSVGRef} className={styles.editorSVG}>
        <defs>
          <marker
            id={EDITOR_HEAD_ID}
            markerWidth="10"
            markerHeight="7"
            refX="10"
            refY="3.5"
            orient="auto"
            stroke={defaultArrowColor}
          >
            <polygon points="0 0, 10 3.5, 0 7" fill={defaultArrowColor} />
          </marker>
        </defs>
        <line ref={setLineRef} stroke={defaultArrowColor} strokeWidth={2} markerEnd={`url(#${EDITOR_HEAD_ID})`} />
      </svg>
      <svg ref={setSVGVertexRef} className={styles.editorSVG}>
        <path
          ref={setVertexPathRef}
          stroke={defaultArrowColor}
          strokeWidth={2}
          strokeDasharray={'5, 5'}
          fill={'none'}
        />
        <circle ref={setVertexRef} stroke={defaultArrowColor} r={4} className={styles.vertex} />
      </svg>
      {renderConnections()}
    </>
  );
};

const getStyles = (theme: GrafanaTheme2) => ({
  editorSVG: css({
    position: 'absolute',
    pointerEvents: 'none',
    width: '100%',
    height: '100%',
    zIndex: 1000,
    display: 'none',
  }),
  connection: css({
    position: 'absolute',
    width: '100%',
    height: '100%',
    zIndex: 1000,
    pointerEvents: 'none',
  }),
  vertex: css({
    fill: '#44aaff',
    strokeWidth: 2,
  }),
  addVertex: css({
    fill: '#44aaff',
    opacity: 0.5,
    strokeWidth: 1,
  }),
});
