import { createContext, forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react";
import Viz from 'viz.js';
import { Module, render } from 'viz.js/full.render.js';
import * as d3 from 'd3';
import { graphToDot } from "../Graphviz";
import type { BreadcrumbItemType } from "antd/es/breadcrumb/Breadcrumb";
import NodeContextMenu from "./NodeContextMenu";
import NodeRenameModal from "./NodeRenameModal";
import { useGraphsStore } from "../stores/GraphsStore";
import { useKeysdownStore, useSelectedNodesStore } from "../stores/ArrayStore";
import { useLoadStore } from "../stores/LoadStore";


export class GraphModel {
  nodes: NodeModel[] = [];
  edges: EdgeModel[] = [];
}

export class EdgeModel {
  from: string;
  to: string;
  id: string;

  constructor(from: string, to: string, id: string) {
    this.from = from;
    this.to = to;
    this.id = id;
  }
}

export class NodeModel {
  public id: string;
  public label?: string;

  constructor(id: string, name: string | undefined = undefined) {
    this.id = id;
    this.label = name;
  }
}

export interface GraphLevel extends BreadcrumbItemType {

}

export interface NodeContext {
  nodeId: string;
  nodeName?: string;
  coords: { x: number, y: number };
  isEmptyArea?: boolean;
}

export interface GraphContext {
  graphId: string;
  selectGraphId: React.Dispatch<React.SetStateAction<string>>;
  graph: GraphModel;
  setGraph: React.Dispatch<React.SetStateAction<GraphModel>>;
}

export interface OpenNodeContext {
  opened: boolean;
  nodeContext: NodeContext | undefined;
}

const viz = new Viz({ Module, render });
export const graphContext = createContext<GraphContext | null>(null);

export interface GraphHandle {
  navigateTo: (nodeId: string, path: Array<{ id: string; name: string | undefined }>) => void;
}

const Graph = forwardRef<GraphHandle, { setGraphPath: React.Dispatch<React.SetStateAction<BreadcrumbItemType[]>>, onNavigate?: (graphId: string) => void }>(function Graph({ setGraphPath, onNavigate }, ref) {
  const containerRef = useRef(null);
  const [graph, setGraph] = useState(defaultGraph());
  const [contextMenuOpened, openContextMenu] = useState(false);
  const [renameModalOpened, openRenameModal] = useState(false);
  const [graphId, selectGraphId] = useState('main');
  const graphIdRef = useRef(graphId);
  const [graphsPath, setGraphsPath] = useState([createPathSegment('main', 'Main')])
  const [nodeContext, openNodeContext] = useState<null | NodeContext>(null);  
  graphIdRef.current = graphId;

  const graphContextValue = {
    graphId: graphId,
    selectGraphId: selectGraphId,
    graph: graph,
    setGraph: setGraph
  };
  const isKeyPressed = useKeysdownStore(store => store.has);
  const selectNode = useSelectedNodesStore(store => store.add);
  const deselectNode = useSelectedNodesStore(store => store.remove);
  const isNodeSelected = useSelectedNodesStore(store => store.has);
  const anyNodeSelected = useSelectedNodesStore(store => store.hasAny);
  const deselectAllNodes = useSelectedNodesStore(store => store.clear);
  const loadCount = useLoadStore(state => state.loadCount);

  useEffect(() => {
    setGraphPath(graphsPath);
  }, [graphsPath])

  useEffect(() => {
    renderGraph();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [graph]);

  // Persist graph edits to the store so they survive breadcrumb navigation.
  // We intentionally read graphId via ref (not as a dep) so this effect only
  // fires on graph content changes, never on navigation (graphId changes).
  useEffect(() => {
    const state = useGraphsStore.getState() as { graphsById: Map<string, GraphModel> };
    state.graphsById.set(graphIdRef.current, graph);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [graph]);

  useEffect(() => {
    if (nodeContext) {
      openContextMenu(true);
    }
  }, [nodeContext]);

  useEffect(() => {
    const container = containerRef.current as unknown as HTMLElement;
    if (!container) return;
    const handler = (event: MouseEvent) => {
      if ((event.target as Element).closest('svg')) return;
      event.preventDefault();
      openNodeContext({
        nodeId: '',
        coords: { x: event.clientX, y: event.clientY },
        isEmptyArea: true,
      });
    };
    container.addEventListener('contextmenu', handler);
    return () => container.removeEventListener('contextmenu', handler);
  }, []);

  useEffect(() => {
    if (loadCount === 0) return;
    const state = useGraphsStore.getState() as { graphsById: Map<string, GraphModel> };
    const mainGraph = state.graphsById.get('main');
    if (mainGraph) setGraph(mainGraph);
    selectGraphId('main');
    setGraphsPath([createPathSegment('main', 'Main')]);
    openNodeContext(null);
    openContextMenu(false);
    onNavigate?.('main');
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [loadCount]);

  useEffect(() => {
    if (nodeContext?.nodeId === graphId) {
      graphsPath.push(createPathSegment(nodeContext.nodeId, nodeContext.nodeName));
      setGraphsPath([...graphsPath])
    }
    const state = useGraphsStore.getState() as { graphsById: Map<string, GraphModel> };
    const graphsById = state.graphsById;
    const graph = graphsById.get(graphId);
    if (graph) {
      setGraph(graph);
    }
    onNavigate?.(graphId);
  }, [graphId]);

  async function renderGraph() {
    const dot = graphToDot(graph);

    try {
      const svgElement = await viz.renderSVGElement(dot, { engine: 'dot' });
      const container = containerRef.current;
      if (!container) return;
      (container as HTMLElement).innerHTML = '';
      (container as HTMLElement).appendChild(svgElement);
      attachInteractions(svgElement);
    } catch (e) {
      console.error('Viz render error', e);
    }
  }

  function attachInteractions(svgElement: SVGSVGElement) {
    const svg = d3.select(svgElement);
    svg.selectAll('g.edge')
      .style('cursor', 'not-allowed')
      .on('click', function () {
        const d3Node = d3.select(this);
        const id = d3Node.attr('id');
        setGraph(prev => ({ ...prev, edges: [...prev.edges.filter(e => e.id !== id)] }));
      })
    svg.selectAll('g.node')
      .style('cursor', 'pointer')
      .on('click', function () {
        const d3Node = d3.select(this);
        const d3Rect = d3Node.select('g.node polygon');
        const id = d3Node.attr('id');
        if (isKeyPressed('Control')) {
          if (isNodeSelected(id)) {
            deselectNode(id);
            d3Rect
              .attr("stroke", "#000000");
          } else {
            selectNode(id);
            d3Rect
              .attr("stroke", "#1677ff")
          }
        } else {
          if (anyNodeSelected()) {
            linkSelectedNodesAsParents(id);
            deselectAllNodes();
            d3.selectAll('g.node polygon')
              .attr("stroke", "#000000");
          } else {
            createChildNode(id);
          }
        }
      })
      .on('contextmenu', function (event) {
        const id = d3.select(this).attr('id');
        const node = graph.nodes.find(n => n.id === id);

        if (!node) {
          return;
        }

        event.preventDefault();
        event.stopPropagation();
        openNodeContext({
          nodeId: id,
          nodeName: node.label,
          coords: { x: event.clientX, y: event.clientY },
        })
      });

    svg.on('contextmenu', function (event) {
      const target = event.target as Element;
      if (target.closest('g.node') || target.closest('g.edge')) {
        return;
      }
      event.preventDefault();
      openNodeContext({
        nodeId: '',
        coords: { x: event.clientX, y: event.clientY },
        isEmptyArea: true,
      });
    });
  }

  function linkSelectedNodesAsParents(childNodeId: string) {
    const selectedNodesIds = useSelectedNodesStore.getState().items;
    setGraph(prev => ({ ...prev, edges: [...prev.edges, ...selectedNodesIds.map(parentId => ({ from: parentId, to: childNodeId, id: crypto.randomUUID() }))] }))
  }

  function createChildNode(parentId: string) {
    const id = crypto.randomUUID();
    setGraph(prev => ({ ...prev, nodes: [...prev.nodes, { id, label: getRandomWords(1)[0] }], edges: [...prev.edges, { from: parentId, to: id, id: crypto.randomUUID() }] }));
  }

  function getRandomWords(count: number) {
    return randomWordList(count);
  }

  function createPathSegment(
    pathSegmentId: string,
    selectedNodeName: string | undefined)
    : BreadcrumbItemType {
    return {
      title: selectedNodeName,
      key: pathSegmentId,
      onClick: () => {
        const index = graphsPath.findIndex(p => p.key === pathSegmentId);
        setGraphsPath(prev => {
          prev.splice(index + 1);

          return [...prev];
        });
        selectGraphId(pathSegmentId);
      }
    } as BreadcrumbItemType;
  }

  useImperativeHandle(ref, () => ({
    navigateTo(nodeId, path) {
      const newPath = path.map(p => createPathSegment(p.id, p.name));
      openNodeContext(null);
      setGraphsPath(newPath);
      selectGraphId(nodeId);
    }
  }));

  return (
    <div className="flex-1 p-4">
      <div ref={containerRef} className="w-full h-full bg-white rounded shadow" style={{ minHeight: '600px', overflow: 'auto' }}>

      </div>
      <graphContext.Provider value={graphContextValue}>
        <NodeContextMenu
          nodeContext={nodeContext!}
          contextMenuOpened={contextMenuOpened}
          openContextMenu={openContextMenu}
          openRenameModal={openRenameModal}>
        </NodeContextMenu>
        <NodeRenameModal
          nodeContext={nodeContext!}
          renameModalOpened={renameModalOpened}
          openRenameModal={openRenameModal}>
        </NodeRenameModal>
      </graphContext.Provider>
    </div>
  )
});

export default Graph;

export function randomWordList(count: number) {
  const wordList = ['Apple', 'Sun', 'Flame', 'Earth', 'Forest', 'Dream', 'Sky', 'Shadow', 'Flower', 'Ocean', 'River', 'Path', 'Sand', 'Night', 'Star', 'Rain', 'Light', 'Tree', 'Wave', 'Storm', 'Stone', 'Snow', 'Cloud', 'Heart', 'Mountain', 'Leaf', 'Bird', 'Wind', 'Fire', 'Wolf'];
  return wordList.sort(() => Math.random() - 0.5).slice(0, count);
}

export function defaultGraph(): GraphModel {
  const start = crypto.randomUUID();
  const end = crypto.randomUUID();

  return {
    nodes: [
      { id: start, label: 'Start' },
      { id: end, label: 'End' }
    ],
    edges: [
      { from: start, to: end, id: crypto.randomUUID() },
    ]
  };
}
