import { Dispatch, memo, ReactElement, SetStateAction, useCallback, useEffect, useRef, useState } from "react";

import { Mode } from "@/statecharts/runtime_types";
import { arraysEqual, objectsEqual, setsEqual } from "@/util/util";
import { ArrowPart, ConcreteSyntax, Diamond, RectSide, Rountangle, Text } from "../../statecharts/concrete_syntax";
import { Connections } from "../../statecharts/detect_connections";
import { TraceableError } from "../../statecharts/parser";
import { ArcDirection, arcDirection } from "../../util/geometry";
import { InsertMode } from "../TopPanel/InsertModes";
import { ArrowSVG } from "./ArrowSVG";
import { DiamondSVG } from "./DiamondSVG";
import { HistorySVG } from "./HistorySVG";
import { RountangleSVG } from "./RountangleSVG";
import { TextSVG } from "./TextSVG";
import "./VisualEditor.css";
import { useCopyPaste } from "./hooks/useCopyPaste";
import { useMouse } from "./hooks/useMouse";

export type VisualEditorState = ConcreteSyntax & {
  nextID: number;
  selection: Selection;
};

export type RountangleSelectable = {
  part: RectSide;
  uid: string;
}
type ArrowSelectable = {
  part: ArrowPart;
  uid: string;
}
type TextSelectable = {
  part: "text";
  uid: string;
}
type HistorySelectable = {
  part: "history";
  uid: string;
}
type Selectable = RountangleSelectable | ArrowSelectable | TextSelectable | HistorySelectable;

export type Selection = Selectable[];

type VisualEditorProps = {
  state: VisualEditorState,
  commitState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
  replaceState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
  conns: Connections,
  syntaxErrors: TraceableError[],
  insertMode: InsertMode,
  highlightActive: Set<string>,
  highlightTransitions: string[],
  setModal: Dispatch<SetStateAction<ReactElement|null>>,
  zoom: number;
};

export const VisualEditor = memo(function VisualEditor({state, commitState, replaceState, conns, syntaxErrors: errors, insertMode, highlightActive, highlightTransitions, setModal, zoom}: VisualEditorProps) {

  // While dragging, the editor is in a temporary state (a state that is not committed to the edit history). If the temporary state is not null, then this state will be what you see.
  // const [temporaryState, setTemporaryState] = useState<VisualEditorState | null>(null);

  // const state = temporaryState || committedState;

  // uid's of selected rountangles
  const selection = state.selection;

  const refSVG = useRef<SVGSVGElement>(null);

  useEffect(() => {
    // bit of a hacky way to force the animation on fired transitions to replay, if the new 'rt' contains the same fired transitions as the previous one
    requestAnimationFrame(() => {
      document.querySelectorAll(".arrow.fired").forEach(el => {
        // @ts-ignore
        el.style.animation = 'none';
        requestAnimationFrame(() => {
          // @ts-ignore
          el.style.animation = ''; 
        })
      });
    })
  }, [highlightTransitions]);


  const {onCopy, onPaste, onCut} = useCopyPaste(state, commitState, selection);

  const {onMouseDown, selectionRect} = useMouse(insertMode, zoom, refSVG,
    state,
    commitState,
    replaceState);


  // for visual feedback, when selecting/moving one thing, we also highlight (in green) all the things that belong to the thing we selected.
  const sidesToHighlight: {[key: string]: RectSide[]} = {};
  const arrowsToHighlight: {[key: string]: boolean} = {};
  const textsToHighlight: {[key: string]: boolean} = {};
  const rountanglesToHighlight: {[key: string]: boolean} = {};
  const historyToHighlight: {[key: string]: boolean} = {};
  for (const selected of selection) {
    const sides = conns.arrow2SideMap.get(selected.uid);
    if (sides) {
      const [startSide, endSide] = sides;
      if (startSide) sidesToHighlight[startSide.uid] = [...sidesToHighlight[startSide.uid]||[], startSide.part];
      if (endSide) sidesToHighlight[endSide.uid] = [...sidesToHighlight[endSide.uid]||[], endSide.part];
    }
    const texts = [
      ...(conns.arrow2TextMap.get(selected.uid) || []),
      ...(conns.rountangle2TextMap.get(selected.uid) || []),
    ];
    for (const textUid of texts) {
      textsToHighlight[textUid] = true;
    }
    const arrows = conns.side2ArrowMap.get(selected.uid + '/' + selected.part) || [];
    if (arrows) {
      for (const [arrowPart, arrowUid] of arrows) {
        arrowsToHighlight[arrowUid] = true;
      }
    }
    const arrow2 = conns.text2ArrowMap.get(selected.uid);
    if (arrow2) {
      arrowsToHighlight[arrow2] = true;
    }
    const rountangleUid = conns.text2RountangleMap.get(selected.uid)
    if (rountangleUid) {
      rountanglesToHighlight[rountangleUid] = true;
    }
    const history = conns.arrow2HistoryMap.get(selected.uid);
    if (history) {
      historyToHighlight[history] = true;
    }
    const arrow3 = conns.history2ArrowMap.get(selected.uid) || [];
    for (const arrow of arrow3) {
      arrowsToHighlight[arrow] = true;
    }
  }

  const onEditText = useCallback((text: Text, newText: string) => {
    if (newText === "") {
      // delete text node
      commitState(state => ({
        ...state,
        texts: state.texts.filter(t => t.uid !== text.uid),
      }));
    }
    else {
      commitState(state => ({
        ...state,
        texts: state.texts.map(t => {
          if (t.uid === text.uid) {
            return {
              ...text,
              text: newText,
            }
          }
          else {
            return t;
          }
        }),
      }));
    }
  }, [commitState]);

  const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);

  const size = 4000*zoom;

  return <svg width={size} height={size}
      className={"svgCanvas"+(highlightActive.has("root")?" active":"")}
      onMouseDown={onMouseDown}
      onContextMenu={e => e.preventDefault()}
      ref={refSVG}

      viewBox={`0 0 4000 4000`}

      onCopy={onCopy}
      onPaste={onPaste}
      onCut={onCut}
    >
      <defs>
        <marker
          id="initialMarker"
          viewBox="0 0 9 9"
          refX="4.5"
          refY="4.5"
          markerWidth="9"
          markerHeight="9"
          markerUnits="userSpaceOnUse">
          <circle cx={4.5} cy={4.5} r={4.5}/>
        </marker>
        <marker
          id="arrowEnd"
          viewBox="0 0 10 10"
          refX="5"
          refY="5"
          markerWidth="12"
          markerHeight="12"
          orient="auto-start-reverse"
          markerUnits="userSpaceOnUse">
          <path d="M 0 0 L 10 5 L 0 10 z"/>
        </marker>
      </defs>

    {(rootErrors.length>0) && <text className="error" x={5} y={20}>{rootErrors.join(' ')}</text>}

    <Rountangles rountangles={state.rountangles} {...{selection, sidesToHighlight, rountanglesToHighlight, errors, highlightActive}}/>
    <Diamonds diamonds={state.diamonds} {...{selection, sidesToHighlight, rountanglesToHighlight, errors}}/>

    {state.history.map(history => <>
      <HistorySVG
        key={history.uid}
        selected={Boolean(selection.find(h => h.uid === history.uid))}
        highlight={Boolean(historyToHighlight[history.uid])}
        {...history}
        />
    </>)}

    {state.arrows.map(arrow => {
      const sides = conns.arrow2SideMap.get(arrow.uid);
      let arc = "no" as ArcDirection;
      if (sides && sides[0]?.uid === sides[1]?.uid && sides[0]!.uid !== undefined) {
        arc = arcDirection(sides[0]!.part, sides[1]!.part);
      }
      const initialMarker = sides && sides[0] === undefined && sides[1] !== undefined;
      return <ArrowSVG
        key={arrow.uid}
        arrow={arrow}
        selected={selection.filter(a => a.uid === arrow.uid).map(({part})=> part as ArrowPart)}
        error={errors
          .filter(({shapeUid}) => shapeUid === arrow.uid)
          .map(({message}) => message).join(', ')}
        highlight={arrowsToHighlight.hasOwnProperty(arrow.uid)}
        fired={highlightTransitions.includes(arrow.uid)}
        arc={arc}
        initialMarker={Boolean(initialMarker)}
        />;
      }
    )}

    <Texts texts={state.texts} {...{selection, textsToHighlight, errors, onEditText, setModal}}/>

    {selectionRect}
  </svg>;
});

const Rountangles = memo(function Rountangles({rountangles, selection, sidesToHighlight, rountanglesToHighlight, errors, highlightActive}: {rountangles: Rountangle[], selection: Selection, sidesToHighlight: {[key: string]: RectSide[]}, rountanglesToHighlight: {[key: string]: boolean}, errors: TraceableError[], highlightActive: Mode}) {
  return <>{rountangles.map(rountangle => {
    return <RountangleSVG
      key={rountangle.uid}
      rountangle={rountangle}
      selected={selection.filter(r => r.uid === rountangle.uid).map(({part}) => part as RectSide)}
      highlight={[...(sidesToHighlight[rountangle.uid] || []), ...(rountanglesToHighlight[rountangle.uid]?["left","right","top","bottom"]:[]) as RectSide[]]}
      error={errors
        .filter(({shapeUid}) => shapeUid === rountangle.uid)
        .map(({message}) => message).join(', ')}
      active={highlightActive.has(rountangle.uid)}
    />})}</>;
}, (p, n) => {
  return arraysEqual(p.rountangles, n.rountangles)
    && arraysEqual(p.selection, n.selection)
    && objectsEqual(p.sidesToHighlight, n.sidesToHighlight)
    && objectsEqual(p.rountanglesToHighlight, n.rountanglesToHighlight)
    && arraysEqual(p.errors, n.errors)
    && setsEqual(p.highlightActive, n.highlightActive);
});

const Diamonds = memo(function Diamonds({diamonds, selection, sidesToHighlight, rountanglesToHighlight, errors}: {diamonds: Diamond[], selection: Selection, sidesToHighlight: {[key: string]: RectSide[]}, rountanglesToHighlight: {[key: string]: boolean}, errors: TraceableError[]}) {
  return <>{diamonds.map(diamond => <>
    <DiamondSVG
      key={diamond.uid}
      diamond={diamond}
      selected={selection.filter(r => r.uid === diamond.uid).map(({part})=>part as RectSide)}
      highlight={[...(sidesToHighlight[diamond.uid] || []), ...(rountanglesToHighlight[diamond.uid]?["left","right","top","bottom"]:[]) as RectSide[]]}
      error={errors
        .filter(({shapeUid}) => shapeUid === diamond.uid)
        .map(({message}) => message).join(', ')}
      active={false}/>
  </>)}</>;
}, (p, n) => {
  return arraysEqual(p.diamonds, n.diamonds)
    && arraysEqual(p.selection, n.selection)
    && objectsEqual(p.sidesToHighlight, n.sidesToHighlight)
    && objectsEqual(p.rountanglesToHighlight, n.rountanglesToHighlight)
    && arraysEqual(p.errors, n.errors);
});

const Texts = memo(function Texts({texts, selection, textsToHighlight, errors, onEditText, setModal}: {texts: Text[], selection: Selection, textsToHighlight: {[key: string]: boolean}, errors: TraceableError[], onEditText: (text: Text, newText: string) => void, setModal: Dispatch<SetStateAction<ReactElement|null>>}) {
  return <>{texts.map(txt => {
    return <TextSVG
      key={txt.uid}
      error={errors.find(({shapeUid}) => txt.uid === shapeUid)}
      text={txt}
      selected={Boolean(selection.filter(s => s.uid === txt.uid).length)}
      highlight={textsToHighlight.hasOwnProperty(txt.uid)}
      onEdit={onEditText}
      setModal={setModal}
    />
  })}</>;
}, (p, n) => {
  return arraysEqual(p.texts, n.texts)
    && arraysEqual(p.selection, n.selection)
    && objectsEqual(p.textsToHighlight, n.textsToHighlight)
    && arraysEqual(p.errors, n.errors)
    && p.onEditText === n.onEditText
    && p.setModal === n.setModal;
});

