// @author Ann Yanich
import '@fontsource/roboto';
import cytoscape from 'cytoscape';
import fcose from 'cytoscape-fcose';
import cola from 'cytoscape-cola';
import spread from 'cytoscape-spread';
import popper from 'cytoscape-popper';
import panzoom from 'cytoscape-panzoom';
import dagre from 'cytoscape-dagre';
import 'cytoscape-panzoom/cytoscape.js-panzoom.css';
import 'cytoscape-panzoom/font-awesome-4.0.3/css/font-awesome.css';
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
import axios from 'B/axios';
import { GraphEdge, GraphNode, pktTypToString } from './types';
import { cytoscapeStyle } from './style';
import {
  fcoseLayout,
  colaLayoutInteractive,
  colaLayoutInitial,
} from './layouts';
cytoscape.use(dagre);
cytoscape.use(fcose);
cytoscape.use(spread);
cytoscape.use(popper);
panzoom(cytoscape);
declare global {
  interface Window {
    agbForwardingGraph: GraphEdge[];
    agbNames: GraphNode[];
    agbId: number;
  }
}

if (process.env.NODE_ENV === 'development' && module.hot) {
  module.hot.accept();
}

// Respond to events from another page that may embed this one in a popup window
// or iframe.
window.addEventListener('message', (event) => {
  if (event?.data?.type === 'navigateToAgb') {
    navigateToAgb(event.data.agbId);
  }
});

let cy: cytoscape.Core;
cy = initCytoscape();
cy.nodes().on('click', onClickNode);
cy.nodes().on('cxttap', onRightClickNode);
cy.edges().on('click', function (e) {
  onClickEdge(e.target);
});
window.addEventListener('resize', (event) => {
  cy.resize();
  cy.fit(cy.elements(), 24);
});

function initCytoscape(): cytoscape.Core {
  const layoutOptions = {
    name: 'dagre',
    rankDir: 'LR',
    rankSep: 400,
  };

  const cy = cytoscape({
    container: document.getElementById('agbForwardingGraphViewer'),
    elements: importGraph(window.agbNames, window.agbForwardingGraph),
    layout: layoutOptions,
    maxZoom: 2,
    minZoom: 0.3,
    style: cytoscapeStyle,
  });
  cy.panzoom({ minZoom: 0.3, maxZoom: 2 });
  return cy;
}

function importGraph(
  nodes: GraphNode[],
  edges: GraphEdge[]
): cytoscape.ElementDefinition[] {
  const cytoscapeNodes: cytoscape.ElementDefinition[] = nodes.map((n) => ({
    group: 'nodes',
    data: {
      id: n.agb_id.toString(),
      name: n.agb_name,
      agb_typ: n.agb_typ,
      width: 10 + n.agb_name.length * 10,
      height: 50,
    },
  }));
  const knownNodeIds: Set<string> = new Set(
    cytoscapeNodes.map((n) => n.data.id)
  );

  const cytoscapeEdges: cytoscape.ElementDefinition[] = edges.map((edge) => ({
    group: 'edges',
    data: {
      name: edge.edge_name,
      source: edge.source.toString(),
      target: edge.target.toString(),
      type: edge.edge_type,
      pktType: pktTypToString(edge.pkt_typ),
    },
  }));

  // Some edges may refer to non-existent AGBs.
  // Generate placeholder nodes for them.
  cytoscapeEdges.forEach((edge) => {
    function addUnknownNode(id: string) {
      const name = `#${id}`;
      cytoscapeNodes.push({
        group: 'nodes',
        data: {
          id,
          name,
          agb_typ: '',
          width: 10 + name.length * 10,
          height: 50,
        },
      });
      knownNodeIds.add(id);
    }
    if (!knownNodeIds.has(edge.data.source)) {
      addUnknownNode(edge.data.source);
    }
    if (!knownNodeIds.has(edge.data.target)) {
      addUnknownNode(edge.data.target);
    }
  });
  return cytoscapeNodes.concat(cytoscapeEdges);
}

function getGraphForNode(id: string): Promise<cytoscape.ElementDefinition[]> {
  return axios.get(`/config/agbForwardingGraphJson/${id}`).then((success) => {
    return importGraph(success.data.names, success.data.graph);
  });
}

function onClickNode(e: cytoscape.EventObject) {
  const id = e.target.id();
  navigateToAgb(id, e);
}
function onRightClickNode(event: cytoscape.EventObject) {
  openAgbInConfigPage(event.target.id());
}

function navigateToAgb(agbId: string, cytoscapeEvent?: cytoscape.EventObject) {
  getGraphForNode(agbId).then((newElDefinitions) => {
    const nonDuplicateElements = newElDefinitions.filter((def) => {
      return !cy
        .elements()
        .toArray()
        .some((e) => {
          return (
            (e.group() === 'nodes' && e.id() === def.data.id) ||
            (e.group() === 'edges' &&
              e.data('source') === def.data.source &&
              e.data('target') === def.data.target &&
              e.data('type') === def.data.type)
          );
        });
    });
    nonDuplicateElements.forEach((ele) => {
      if (ele.group === 'nodes' && cytoscapeEvent) {
        ele.position = { ...cytoscapeEvent.target.position() };
      }
    });
    const newElements = cy.add(nonDuplicateElements);
    newElements.nodes().on('click', onClickNode);
    newElements.nodes().on('cxttap', onRightClickNode);
    newElements.edges().on('click', function (e) {
      onClickEdge(e.target);
    });
    const rootNeighborhood = cy.$('#' + agbId).closedNeighborhood();
    const layoutOptions = {
      name: 'dagre',
      rankDir: 'LR',
      rankSep: 400,
    };
    cy.remove(cy.elements().not(rootNeighborhood));
    const layout = cy.layout(layoutOptions).run();
  });
}

function onClickEdge(edge: cytoscape.EdgeSingular) {
  const ref = edge.popperRef();
  const dummyDomEle = document.createElement('div');
  const tip = tippy(dummyDomEle, {
    getReferenceClientRect: ref.getBoundingClientRect,
    trigger: 'manual',
    content: () => {
      const content = document.createElement('div');
      content.innerHTML = edge.data('name');
      return content;
    },
  });
  tip.show();
}

function openAgbInConfigPage(agbId: string) {
  if (!window.opener) {
    window.alert('window.opener not found');
  } else {
    window.opener.postMessage({ type: 'navigateToAgb', agbId }, '*');
  }
}

function getPath(startNode: string, endNode: string): string[] {
  const visited = new Set<string>();
  const queue = [[startNode, []]];
  while (queue.length > 0) {
    const [node, path] = queue.shift()!;
    if (node === endNode) {
      // Return the path if we have reached the end node
      return [...path, endNode];
    }
    if (visited.has(node)) {
      continue;
    }
    visited.add(node);
    const neighbors = cy.getElementById(node).neighborhood().nodes();
    for (const neighbor of neighbors) {
      queue.push([neighbor.id(), [...path, node]]);
    }
  }
  // If we reach here, there is no path between the start and end node
  return [];
}
