import { SmartBezierEdge, SmartStepEdge, SmartStraightEdge } from '@tisoap/react-flow-smart-edge';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useDispatch } from 'react-redux';
import ReactFlow, { Edge, MiniMap, Node, ReactFlowInstance, ReactFlowProvider, useEdgesState, useNodesState } from 'reactflow';
import 'reactflow/dist/style.css';
import { Icon, Panel } from '../../components';
import { Button } from '../../components/buttons';
import { Popover, PopoverContent, PopoverTrigger } from '../../components/layout/Popover';
import { Tooltip, TooltipContent, TooltipTrigger } from '../../components/tooltip/Tooltip';
import { useSchema, useSchemaGraph, useSchemaSettings, useSearchResultSchema } from '../../data-access';
import { resultSetFocus } from '../../data-access/store/interactionSlice';
import { toSchemaGraphology } from '../../data-access/store/schemaSlice';
import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '../../graph-layout';
import { ConnectionDragLine, ConnectionLine } from '../../querybuilder';
import { NodeEdge } from '../pills/edges/node-edge';
import { SelfEdge } from '../pills/edges/self-edge';
import { SchemaEntityPill } from '../pills/nodes/entity/SchemaEntityPill';
import { SchemaListEntityPill } from '../pills/nodes/entity/SchemaListEntityPill';
import { SchemaListRelationPill } from '../pills/nodes/relation/SchemaListRelationPill';
import { SchemaRelationPill } from '../pills/nodes/relation/SchemaRelationPill';
import { schemaExpandRelation, schemaGraphology2Reactflow } from '../schema-utils';
import { SchemaSettings } from './SchemaSettings';

interface Props {
  content?: string;
  auth?: boolean;
  onRemove?: () => void;
}

const graphEntityPillNodeTypes = {
  entity: SchemaEntityPill,
  relation: SchemaRelationPill,
};
const listEntityPillNodeTypes = {
  entity: SchemaListEntityPill,
  relation: SchemaListRelationPill,
};

const edgeTypes = {
  nodeEdge: NodeEdge,
  selfEdge: SelfEdge,
  bezier: SmartBezierEdge,
  connection: ConnectionLine,
  straight: SmartStraightEdge,
  step: SmartStepEdge,
};

export enum SchemaViewState {
  SchemaGraphS = 'Schema Graph Small',
  SchemaGraphM = 'Schema Graph Medium',
  SchemaGraphL = 'Schema Graph Large',
  SchemaListS = 'Schema List Small',
  SchemaListM = 'Schema List Medium',
  SchemaListL = 'Schema List Large',
}

export const Schema = (props: Props) => {
  const settings = useSchemaSettings();
  const searchResults = useSearchResultSchema();
  const dispatch = useDispatch();
  const [nodes, setNodes, onNodesChange] = useNodesState([] as Node[]);
  const [edges, setEdges, onEdgesChange] = useEdgesState([] as Edge[]);

  const [nodeTypes, setNodeTypes] = useState<{
    entity: React.FC<any>;
    relation: React.FC<any>;
  }>(graphEntityPillNodeTypes);

  // viewport
  const initialViewportRef = useRef<{ x: number; y: number; zoom: number } | null>(null);
  const [hasLayoutBeenRun, setHasLayoutBeenRun] = useState(false);

  // Time threshold for distinguishing between a click and a drag
  const isPillClicked = useRef<boolean>(false);

  const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null);
  const reactFlowRef = useRef<HTMLDivElement>(null);

  // In case the schema is updated
  const schema = useSchema();
  const schemaGraphology = useMemo(() => toSchemaGraphology(schema.graph), [schema.graph]);
  const layout = useRef<AlgorithmToLayoutProvider<AllLayoutAlgorithms>>();

  const [viewSelected, setViewState] = useState<SchemaViewState>(SchemaViewState.SchemaListS);

  function updateLayout() {
    const layoutFactory = new LayoutFactory();
    layout.current = layoutFactory.createLayout(settings.layoutName);
  }

  const maxZoom = 1.2;
  const fitView = () => {
    if (reactFlowInstanceRef.current) {
      reactFlowInstanceRef.current.fitView({maxZoom});
    }
  };

  useEffect(() => {
    updateLayout();
    if (sessionStorage.getItem('firstUserConnection') === 'true') {
      sessionStorage.setItem('firstUserConnection', 'false');
    } else {
      sessionStorage.setItem('firstUserConnection', 'true');
    }
  }, []);

  async function layoutGraph() {
    setNodeTypes(graphEntityPillNodeTypes);
    updateLayout();
    const expandedSchema = schemaExpandRelation(schemaGraphology);
    const bounds = reactFlowRef.current?.getBoundingClientRect();
    const xy = bounds ? { x1: 50, x2: bounds.width - 50, y1: 50, y2: bounds.height - 200 } : { x1: 0, x2: 500, y1: 0, y2: 1000 };
    // layout.current?.setVerbose(true);
    await layout.current?.layout(expandedSchema, xy);
    const schemaFlow = schemaGraphology2Reactflow(expandedSchema, settings.connectionType, settings.animatedEdges);

    let nodesWithRef, edgesWithRef;
    if (!hasLayoutBeenRun) {
      nodesWithRef = schemaFlow.nodes.map((node) => {
        return {
          ...node,
          data: { ...node.data, reactFlowRef, tooltipClose: false },
        };
      });

      edgesWithRef = schemaFlow.edges.map((edge) => {
        return {
          ...edge,
          data: { ...edge.data, reactFlowRef, tooltipClose: false },
        };
      });

      setHasLayoutBeenRun(true);
    } else {
      nodesWithRef = schemaFlow.nodes.map((node) => {
        return {
          ...node,
          data: { ...node.data },
        };
      });

      edgesWithRef = schemaFlow.edges.map((edge) => {
        return {
          ...edge,
          data: { ...edge.data },
        };
      });
    }

    setNodes(nodesWithRef);
    setEdges(edgesWithRef);
    setTimeout(() => fitView(), 100);
  }

  async function layoutList() {
    setNodeTypes(listEntityPillNodeTypes);
    updateLayout();
    const expandedSchema = schemaExpandRelation(schemaGraphology);
    const bounds = reactFlowRef.current?.getBoundingClientRect();
    const xy = bounds ? { x1: 50, x2: bounds.width - 50, y1: 50, y2: bounds.height - 200 } : { x1: 0, x2: 500, y1: 0, y2: 1000 };
    await layout.current?.layout(expandedSchema, xy);
    const schemaFlow = schemaGraphology2Reactflow(expandedSchema, settings.connectionType, settings.animatedEdges);

    schemaFlow.nodes = schemaFlow.nodes.filter((node) => !node.id.toLowerCase().includes('bloom'));
    schemaFlow.edges = schemaFlow.edges.filter((edge) => !edge.id.toLowerCase().includes('bloom'));

    schemaFlow.nodes = schemaFlow.nodes.filter((node) => !node.id.toLowerCase().includes('bloom'));
    schemaFlow.edges = schemaFlow.edges.filter((edge) => !edge.id.toLowerCase().includes('bloom'));

    let nodesWithRef, edgesWithRef;
    if (!hasLayoutBeenRun) {
      nodesWithRef = schemaFlow.nodes.map((node) => {
        return {
          ...node,
          data: { ...node.data, reactFlowRef, tooltipClose: false },
        };
      });

      edgesWithRef = schemaFlow.edges.map((edge) => {
        return {
          ...edge,
          data: { ...edge.data, reactFlowRef, tooltipClose: false },
        };
      });

      setHasLayoutBeenRun(true);
    } else {
      nodesWithRef = schemaFlow.nodes.map((node) => {
        return {
          ...node,
          data: { ...node.data },
        };
      });

      edgesWithRef = schemaFlow.edges.map((edge) => {
        return {
          ...edge,
          data: { ...edge.data },
        };
      });
    }

    setNodes(nodesWithRef);
    setEdges(edgesWithRef);
    setTimeout(() => fitView(), 100);
  }

  useEffect(() => {
    setViewState(settings.schemaViewState);

    if (schemaGraphology === undefined || schemaGraphology.order == 0) {
      setNodes([]);
      setEdges([]);
      return;
    }

    if (
      settings.schemaViewState === SchemaViewState.SchemaGraphS ||
      settings.schemaViewState === SchemaViewState.SchemaGraphM ||
      settings.schemaViewState === SchemaViewState.SchemaGraphL
    ) {
      layoutGraph();
    } else {
      layoutList();
    }
  }, [schema.graph, settings]);

  useEffect(() => {
    setNodes((nds) =>
      nds.map((node) => ({
        ...node,
        selected: searchResults.includes(node.id) || searchResults.includes(node.data.label),
      })),
    );
  }, [searchResults]);

  const nodeColor = (node: any) => {
    switch (node.type) {
      case 'entity':
        return 'hsl(var(--clr-node))';
      case 'relation':
        return 'hsl(var(--clr-relation))';
      default:
        return '#ff0072';
    }
  };

  const handleOnClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
    const target = event.target as HTMLElement;
    const clickedOutsideNode = target.classList.contains('react-flow__pane');

    setNodes((nds) =>
      nds.map((node) => ({
        ...node,
        data: {
          ...node.data,
          tooltipClose: clickedOutsideNode,
        },
      })),
    );

    setEdges((edg) =>
      edg.map((edge) => ({
        ...edge,
        data: {
          ...edge.data,
          tooltipClose: clickedOutsideNode,
        },
      })),
    );
  };

  return (
    <Panel
      title="Schema"
      className="schema-panel"
      tooltips={
        <>
          <Tooltip>
            <TooltipTrigger>
              <Button
                variantType="secondary"
                variant="ghost"
                size="xs"
                iconComponent="icon-[ic--baseline-remove]"
                onClick={() => {
                  if (props.onRemove) props.onRemove();
                }}
              />
            </TooltipTrigger>
            <TooltipContent>
              <p>Hide</p>
            </TooltipContent>
          </Tooltip>
          <Tooltip>
            <TooltipTrigger>
              <Button
                variantType="secondary"
                variant="ghost"
                size="xs"
                iconComponent="icon-[ic--baseline-content-copy]"
                onClick={() => {
                  // Copy the schema to the clipboard
                  navigator.clipboard.writeText(JSON.stringify(schema.graph, null, 2));
                }}
              />
            </TooltipTrigger>
            <TooltipContent>
              <p>Copy Schema to Clipboard</p>
            </TooltipContent>
          </Tooltip>
          <Tooltip>
            <TooltipTrigger>
              <Button
                variantType="secondary"
                variant="ghost"
                size="xs"
                iconComponent="icon-[ic--baseline-fullscreen]"
                onClick={() => {
                  fitView();
                }}
              />
            </TooltipTrigger>
            <TooltipContent>
              <p>Fit to screen</p>
            </TooltipContent>
          </Tooltip>
          <Popover>
            <PopoverTrigger>
              <Tooltip>
                <TooltipTrigger>
                  <Button
                    variantType="secondary"
                    variant="ghost"
                    size="xs"
                    iconComponent="icon-[ic--baseline-settings]"
                    className="schema-settings"
                  />
                </TooltipTrigger>
                <TooltipContent>
                  <p>Schema Settings</p>
                </TooltipContent>
              </Tooltip>
            </PopoverTrigger>
            <PopoverContent>
              <SchemaSettings />
            </PopoverContent>
          </Popover>
        </>
      }
    >
      <div className="w-full h-full flex flex-col justify-between" ref={reactFlowRef}>
        {schema.loading ? (
          <div className="h-full flex flex-col items-center justify-center">
            <Icon component="icon-[mingcute--loading-line]" size={56} className="w-15 h-15 animate-spin " />
          </div>
        ) : nodes.length === 0 ? (
          <p className="m-3 text-xl font-bold">No Elements</p>
        ) : (
          <ReactFlowProvider>
            <ReactFlow
              snapGrid={[10, 10]}
              snapToGrid
              onlyRenderVisibleElements={false}
              nodesDraggable={true}
              nodeTypes={nodeTypes}
              edgeTypes={edgeTypes}
              connectionLineComponent={ConnectionDragLine}
              onNodesChange={onNodesChange}
              onEdgesChange={onEdgesChange}
              onMouseDownCapture={() => dispatch(resultSetFocus({ focusType: 'schema' }))}
              nodes={nodes}
              edges={edges}
              onInit={(reactFlowInstance) => {
                reactFlowInstanceRef.current = reactFlowInstance;
                setTimeout(() => fitView(), 100);
              }}
              onClick={handleOnClick}
              proOptions={{ hideAttribution: true }}
            >
              {settings.showMinimap && <MiniMap nodeColor={nodeColor} />}
            </ReactFlow>
          </ReactFlowProvider>
        )}
      </div>
    </Panel>
  );
};