// The Lucia project.
// Copyright 2023-2026 DSP, inc.  All rights reserved.
// Authors:
//   chiayin.kuo@dsp.im (chiayin), 2023/1/31
//   imacat.yang@dsp.im (imacat), 2023/9/23
//   cindy.chang@dsp.im (Cindy Chang), 2024/5/30
/**
 * @module cytoscapeMap Cytoscape.js process map rendering with
 *   interactive node/edge highlighting, tooltips, and position persistence.
 */

import cytoscape from "cytoscape";
import dagre from "cytoscape-dagre";
import fcose from "cytoscape-fcose";
import cola from "cytoscape-cola";
import tippy from "tippy.js";
import "tippy.js/dist/tippy.css";
import { useMapPathStore } from "@/stores/mapPathStore";
import { getTimeLabel } from "@/module/timeLabel.js";
import { createTooltipContent } from "@/module/tooltipContent.js";
import { useCytoscapeStore } from "@/stores/cytoscapeStore";
import { SAVE_KEY_NAME } from "@/constants/constants.js";

/**
 * Composes display text for frequency-type data layer values.
 *
 * @param {string} baseText - The base label text prefix.
 * @param {string} dataLayerOption - The data layer option key
 *   (e.g. "rel_freq" for relative frequency).
 * @param {number} optionValue - The numeric value to format.
 * @returns {string} The formatted text with the value appended.
 */
const composeFreqTypeText = (baseText, dataLayerOption, optionValue) => {
  let text = baseText;
  const textInt =
    dataLayerOption === "rel_freq"
      ? baseText + optionValue * 100 + "%"
      : baseText + optionValue;
  const textFloat =
    dataLayerOption === "rel_freq"
      ? baseText + (optionValue * 100).toFixed(2) + "%"
      : baseText + optionValue.toFixed(2);
  // Check if the value is an integer; if not, round to 2 decimal places.
  text = Number.isInteger(optionValue) ? textInt : textFloat;
  return text;
};

// Register layout algorithms
cytoscape.use(dagre);
cytoscape.use(fcose);
cytoscape.use(cola);

/**
 * Creates and configures a Cytoscape.js process map instance with
 * interactive features including node/edge highlighting, tooltips,
 * and position persistence via localStorage.
 *
 * @param {Object} mapData - The map data containing nodes and edges.
 * @param {number} mapData.startId - The start node ID.
 * @param {number} mapData.endId - The end node ID.
 * @param {Array} mapData.nodes - Array of node data objects.
 * @param {Array} mapData.edges - Array of edge data objects.
 * @param {string} dataLayerType - The data layer type ("freq" or "duration").
 * @param {string} dataLayerOption - The data layer option key
 *   (e.g. "abs_freq", "rel_freq", "rel_duration").
 * @param {string} curveStyle - The edge curve style
 *   ("unbundled-bezier" or "taxi").
 * @param {string} rank - The layout direction ("TB" or "LR").
 * @param {HTMLElement} graphId - The DOM container element for Cytoscape.
 * @returns {cytoscape.Core} The configured Cytoscape instance.
 */
export default function cytoscapeMap(
  mapData,
  dataLayerType,
  dataLayerOption,
  curveStyle,
  rank,
  graphId,
) {
  // Set the color and style for each node and edge
  let nodes = mapData.nodes;
  let edges = mapData.edges;

  // create Cytoscape
  let cy = cytoscape({
    container: graphId,
    elements: {
      nodes: nodes, // Node data
      edges: edges, // Edge data
    },
    layout: {
      name: "dagre",
      rankDir: rank, // Vertical TB | Horizontal LR, variable from 'cytoscape-dagre' plugin
    },
    style: [
      // Style changes when a node is selected
      {
        selector: "node:selected",
        style: {
          "border-color": "red",
          "border-width": "3",
        },
      },
      // Node styling
      {
        selector: "node",
        style: {
          label: function (node) {
            // Text to display on the node
            // node.data(this.dataLayerType+"."+this.dataLayerOption) accesses the original array value at node.data.key.value
            let optionValue = node.data(`${dataLayerType}.${dataLayerOption}`);
            let text = "";
            const STRING_LIMIT = 8;
            if (node.data("label").length > STRING_LIMIT) {
              // If text exceeds STRING_LIMIT, append "..." and add line breaks (\n)
              // Using data() because Cytoscape converts array data to function calls
              text = `${node.data("label").substr(0, STRING_LIMIT)}...\n\n`;
            } else {
              // Pad with spaces to match the label width for consistent sizing
              text = `${node.data("label").padEnd(STRING_LIMIT, "　")}\n\n`;
            }

            // In elements, activity is categorized as default, so check if the node is an activity before adding text.
            // Use parseInt (integer) or parseFloat (float) to convert strings to numbers.
            // Relative values need to be converted to percentages (%)
            if (node.data("type") === "activity") {
              let textDurRel;
              let timeLabelInt;
              let timeLabelFloat;
              let textTimeLabel;

              switch (dataLayerType) {
                case "freq": // Frequency
                  text = composeFreqTypeText(
                    text,
                    dataLayerOption,
                    optionValue,
                  );
                  break;
                case "duration": // Duration: Relative is percentage %, others need time unit conversion.
                  // Relative %
                  textDurRel = text + (optionValue * 100).toFixed(2) + "%";
                  // Timelabel
                  timeLabelInt = text + getTimeLabel(optionValue);
                  timeLabelFloat = text + getTimeLabel(optionValue.toFixed(2));

                  // Check if the value is an integer; if not, round to 2 decimal places.
                  textTimeLabel =
                    Number.isInteger(optionValue)
                      ? timeLabelInt
                      : timeLabelFloat;

                  text =
                    dataLayerOption === "rel_duration"
                      ? textDurRel
                      : textTimeLabel;
                  break;
              }
            }

            return text;
          },
          "text-opacity": 0.7,
          "background-color": "data(backgroundColor)",
          "border-color": "data(bordercolor)",
          "border-width": function (node) {
            return node.data("type") === "activity" ? "1" : "2";
          },
          "background-image": "data(nodeImageUrl)",
          "background-opacity": "data(backgroundOpacity)", // Transparent background
          "border-opacity": "data(borderOpacity)", // Transparent border
          shape: "data(shape)",
          "text-wrap": "wrap",
          "text-max-width": "data(width)", // Wrap text within the node
          "text-overflow-wrap": "anywhere", // Allow wrapping at any position
          "text-margin-x": function (node) {
            return node.data("type") === "activity" ? -5 : 0;
          },
          "text-margin-y": function (node) {
            return node.data("type") === "activity" ? 2 : 0;
          },
          padding: 0,
          "text-justification": "left",
          "text-halign": "center",
          "text-valign": "center",
          height: "data(height)",
          width: "data(width)",
          color: "data(textColor)",
          "line-height": "0.7rem",
          "font-size": 14,
        },
      },
      // Edge styling
      {
        selector: "edge",
        style: {
          content: function (edge) {
            // Text displayed on the edge
            let optionValue = edge.data(`${dataLayerType}.${dataLayerOption}`);
            let result = "";
            let edgeInt;
            let edgeFloat;
            let edgeDurRel;
            let timeLabelInt;
            let timeLabelFloat;
            let edgeTimeLabel;
            if (optionValue === "") return optionValue;

            switch (dataLayerType) {
              case "freq":
                edgeInt =
                  dataLayerOption === "rel_freq"
                    ? optionValue * 100 + "%"
                    : optionValue;
                edgeFloat =
                  dataLayerOption === "rel_freq"
                    ? (optionValue * 100).toFixed(2) + "%"
                    : optionValue.toFixed(2);

                // Check if the value is an integer; if not, round to 2 decimal places.
                result =
                  Number.isInteger(optionValue) ? edgeInt : edgeFloat;
                break;

              case "duration": // Duration: Relative is percentage %, others need time unit conversion.
                // Relative %
                edgeDurRel = (optionValue * 100).toFixed(2) + "%";
                // Timelabel
                timeLabelInt = getTimeLabel(optionValue);
                timeLabelFloat = getTimeLabel(optionValue.toFixed(2));
                edgeTimeLabel =
                  Number.isInteger(optionValue)
                    ? timeLabelInt
                    : timeLabelFloat;

                result =
                  dataLayerOption === "rel_duration"
                    ? edgeDurRel
                    : edgeTimeLabel;
                break;
            }
            return result;
          },
          "curve-style": curveStyle, // unbundled-bezier | taxi
          "overlay-opacity": 0, // Set overlay-opacity to 0 to remove the gray shadow
          "target-arrow-shape": "triangle", // Arrow shape pointing to target: triangle
          color: "gray", //#0066cc
          //'control-point-step-size':100, // Distance between Bezier curve control points
          width: "data(lineWidth)",
          "line-style": "data(edgeStyle)",
          "text-margin-y": "0.7rem",
          //"text-rotation": "autorotate",
        },
      },
      {
        selector: ".highlight-edge",
        style: {
          color: "#0099FF",
          "line-color": "#0099FF",
          "overlay-color": "#0099FF",
          "overlay-opacity": 0.2,
          "overlay-padding": "5px",
        },
      },
      {
        selector: ".highlight-node",
        style: {
          "overlay-color": "#0099FF",
          "overlay-opacity": 0.01,
          "overlay-padding": "5px",
        },
      },
      {
        selector: "edge[source = target]", // Select self-loop edges
        style: {
          "loop-direction": "0deg", // Control the loop direction
          "loop-sweep": "-60deg", // Control the loop arc; adjust to change size
          "control-point-step-size": 50, // Control the loop radius; increase to enlarge the loop
        },
      },
    ],
  });

  // When an edge is clicked, apply glow effect to the edge and its label
  cy.on("tap", "edge", function (event) {
    cy.edges().removeClass("highlight-edge");
    event.target.addClass("highlight-edge");
  });

  // When a node is clicked, apply glow effect to the node and adjacent edges
  cy.on("tap, mousedown", "node", function (event) {
    useMapPathStore().onNodeClickHighlightEdges(event.target);
  });

  // When an edge is clicked, apply glow effect to the edge and both endpoint nodes
  cy.on("tap, mousedown", "edge", function (event) {
    useMapPathStore().onEdgeClickHighlightNodes(event.target);
  });

  // creat tippy.js
  let tip;
  cy.on("mouseover", "node", function (event) {
    tip?.destroy();
    const node = event.target;
    let ref = node.popperRef();
    let dummyDomEle = document.createElement("div");
    let content = createTooltipContent(node.data("label"));
    tip = new tippy(dummyDomEle, {
      // tippy props:
      getReferenceClientRect: ref.getBoundingClientRect,
      trigger: "manual",
      content: content,
    });
    if (node.data("label").length > 10) tip.show();
  });
  cy.on("mouseout", "node", function (event) {
    tip?.destroy();
    tip = null;
  });

  // here we remember and recall positions
  const cytoscapeStore = useCytoscapeStore();
  cy.ready(() => {
    cytoscapeStore.loadPositionsFromStorage(rank);
    // Check if localStorage has previously saved visit data.
    // If saved node positions exist, restore them for rendering.
    try {
      const raw = localStorage.getItem(SAVE_KEY_NAME);
      if (raw) {
        const allGraphsRemembered = JSON.parse(raw);
        const currentGraphNodesRemembered = allGraphsRemembered[
          cytoscapeStore.currentGraphId
        ]
          ? allGraphsRemembered[cytoscapeStore.currentGraphId][rank]
          : null; // May be undefined
        if (currentGraphNodesRemembered) {
          currentGraphNodesRemembered.forEach((nodeRemembered) => {
            const nodeToDecide = cy.getElementById(nodeRemembered.id);
            if (nodeToDecide) {
              nodeToDecide.position(nodeRemembered.position);
            }
          });
        }
      }
    } catch {
      // Corrupted localStorage data; ignore and use defaults
    }
    // Save the current positions of all nodes when the view is first entered
    const allNodes = cy.nodes();
    allNodes.forEach((nodeFirstlySave) => {
      cytoscapeStore.saveNodePosition(
        nodeFirstlySave.id(),
        nodeFirstlySave.position(),
        rank,
      );
    });

    // After node positions change, save the updated positions.
    // rank represents whether the user is in horizontal or vertical layout mode.
    cy.on("dragfree", "node", (event) => {
      const nodeToSave = event.target;
      cytoscapeStore.saveNodePosition(
        nodeToSave.id(),
        nodeToSave.position(),
        rank,
      );
    });
  });

  return cy;
}
