"use client";

import "@xyflow/react/dist/style.css";

import {
  Background,
  BackgroundVariant,
  Controls,
  MiniMap,
  MarkerType,
  Panel,
  ReactFlow,
  ReactFlowProvider,
  useEdgesState,
  useNodesState,
  useReactFlow,
  type Node,
} from "@xyflow/react";
import {
  Box,
  Chip,
  IconButton,
  Paper,
  Popover,
  Stack,
  Tooltip,
  Typography,
} from "@mui/material";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import MapOutlinedIcon from "@mui/icons-material/MapOutlined";
import RestartAltIcon from "@mui/icons-material/RestartAlt";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { ItemLotTraceResponse } from "@/app/api/itemTracing";
import ItemTracingNodeDetailPanel from "./ItemTracingNodeDetailPanel";
import ItemTracingFlowGraphSearch from "./ItemTracingFlowGraphSearch";
import {
  buildReactFlowGraph,
  reactFlowGraphExtent,
  type TraceFlowNodeData,
} from "./buildReactFlowGraph";
import { traceFlowNodeTypes } from "./TraceFlowNodes";
import { traceFlowEdgeTypes } from "./TraceFlowEdge";
import {
  VIEWPORT_HEIGHT,
  FIT_VIEW_MIN_ZOOM,
  FIT_VIEW_MAX_ZOOM,
  DO_GROUP_HEADER,
} from "./traceFlowConstants";
import {
  minimapNodeColor,
  phaseChipColor,
  phaseLabelKey,
  kindLabelKey,
} from "./traceFlowNodeUtils";
import { searchTraceGraphNodes } from "./traceGraphSearch";
import type { CompiledTraceGraph } from "./compileTraceGraph";
import { TraceGraphNode } from "./buildTraceGraphNodes";
import type { WarehouseFocusRequest } from "./ItemTracing";
import type { TraceGraphPhase } from "./traceGraphLayout";

type TraceParams = {
  stockInLineId?: number;
  lotNo?: string;
  itemCode?: string;
};

type Props = {
  data: ItemLotTraceResponse;
  compiledGraph: CompiledTraceGraph;
  onTrace?: (params: TraceParams) => void;
  focusWarehouse?: WarehouseFocusRequest | null;
};

const DETAIL_PANEL_WIDTH = 280;

const LEGEND_ROWS: Array<{ key: string; tipKey: string }> = [
  { key: "flowLegendTime", tipKey: "flowLegendTimeTip" },
  { key: "flowLegendPhase", tipKey: "flowLegendPhaseTip" },
  { key: "flowLegendBranch", tipKey: "flowLegendBranchTip" },
  { key: "flowLegendArrow", tipKey: "flowLegendArrowTip" },
  { key: "flowLegendPrelude", tipKey: "flowLegendPreludeTip" },
];

const ItemTracingFlowGraphInner: React.FC<Props> = ({
  data,
  compiledGraph,
  onTrace,
  focusWarehouse,
}) => {
  const { t } = useTranslation("itemTracing");
  const { fitView } = useReactFlow();
  const [selectedNode, setSelectedNode] = useState<TraceGraphNode | null>(null);
  const [showMinimap, setShowMinimap] = useState(true);
  const [searchQuery, setSearchQuery] = useState("");
  const [activeMatchIndex, setActiveMatchIndex] = useState(0);
  const [legendAnchor, setLegendAnchor] = useState<HTMLElement | null>(null);
  const [hiddenPhases, setHiddenPhases] = useState<Set<TraceGraphPhase>>(
    () => new Set(),
  );
  const [collapsedGroupIds, setCollapsedGroupIds] = useState<Set<string>>(
    () => new Set(),
  );

  const layout = compiledGraph.layout;
  const hasJoPrelude = Boolean(data.joPrelude);

  const phaseLabels = useMemo(
    () =>
      Object.fromEntries(
        layout.phaseOrder.map((phase) => [phase, t(phaseLabelKey(phase))]),
      ),
    [layout.phaseOrder, t],
  );

  const graphElements = useMemo(
    () => buildReactFlowGraph(layout, phaseLabels, compiledGraph.edgePairs),
    [layout, phaseLabels, compiledGraph.edgePairs],
  );

  const nodesWithCallbacks = useMemo(
    () =>
      graphElements.nodes.map((node) =>
        node.type === "traceEvent"
          ? {
              ...node,
              data: { ...node.data, onTrace },
            }
          : node,
      ),
    [graphElements.nodes, onTrace],
  );

  const [nodes, setNodes, onNodesChange] = useNodesState(nodesWithCallbacks);
  const [edges, setEdges, onEdgesChange] = useEdgesState(graphElements.edges);

  const kindLabel = useCallback(
    (kind: TraceGraphNode["kind"]) => t(kindLabelKey(kind)),
    [t],
  );

  const phaseVisible = useCallback(
    (phase: TraceGraphPhase | undefined) =>
      phase == null || hiddenPhases.size === 0 || !hiddenPhases.has(phase),
    [hiddenPhases],
  );

  const visibleLayoutNodes = useMemo(
    () => layout.nodes.filter((n) => phaseVisible(n.phase)),
    [layout.nodes, phaseVisible],
  );

  const matchIds = useMemo(
    () => searchTraceGraphNodes(visibleLayoutNodes, searchQuery, kindLabel),
    [visibleLayoutNodes, searchQuery, kindLabel],
  );

  const activeNodeId =
    matchIds.length > 0 ? matchIds[activeMatchIndex % matchIds.length] : null;

  const focusFitNodes = useMemo(() => {
    if (!activeNodeId) return [];
    const ids = new Set<string>([activeNodeId]);
    const activeRf = graphElements.nodes.find((n) => n.id === activeNodeId);
    if (activeRf?.parentId) ids.add(activeRf.parentId);
    graphElements.edges.forEach((edge) => {
      if (edge.source === activeNodeId) ids.add(edge.target);
      if (edge.target === activeNodeId) ids.add(edge.source);
    });
    return Array.from(ids, (id) => ({ id }));
  }, [activeNodeId, graphElements.edges, graphElements.nodes]);

  useEffect(() => {
    setSearchQuery("");
    setActiveMatchIndex(0);
    setSelectedNode(null);
    setHiddenPhases(new Set());
    setCollapsedGroupIds(new Set());
  }, [data]);

  useEffect(() => {
    setActiveMatchIndex(0);
  }, [searchQuery]);

  const togglePhase = useCallback(
    (phase: TraceGraphPhase) => {
      setHiddenPhases((prev) => {
        const next = new Set(prev);
        if (next.has(phase)) next.delete(phase);
        else next.add(phase);
        // If every phase would be hidden, treat as "show all"
        if (next.size >= layout.phaseOrder.length) return new Set();
        return next;
      });
    },
    [layout.phaseOrder.length],
  );

  const toggleGroupCollapse = useCallback((groupId: string) => {
    setCollapsedGroupIds((prev) => {
      const next = new Set(prev);
      if (next.has(groupId)) next.delete(groupId);
      else next.add(groupId);
      return next;
    });
  }, []);

  const resetPhaseFilter = useCallback(() => setHiddenPhases(new Set()), []);

  const displayedNodes = useMemo(() => {
    const hasSearch = searchQuery.trim().length > 0;
    const matchSet = new Set(matchIds);
    const groupIdsWithChildMatch = new Set<string>();
    visibleLayoutNodes.forEach((n) => {
      if (n.doGroupId && matchSet.has(n.id))
        groupIdsWithChildMatch.add(n.doGroupId);
    });
    const visibleIds = new Set(visibleLayoutNodes.map((n) => n.id));
    layout.nodes.forEach((n) => {
      if (n.doGroupId && visibleIds.has(n.id)) visibleIds.add(n.doGroupId);
    });

    return nodes
      .filter((node) => {
        if (node.type === "phaseLabel" || node.type === "dateHeader")
          return true;
        if (node.type === "doGroup" || node.type === "pickGroup") {
          return layout.nodes.some(
            (n) => n.doGroupId === node.id && phaseVisible(n.phase),
          );
        }
        if (node.type === "traceEvent") {
          const layoutNode = (node.data as TraceFlowNodeData | undefined)
            ?.layoutNode;
          if (
            layoutNode?.doGroupId &&
            collapsedGroupIds.has(layoutNode.doGroupId)
          ) {
            return false;
          }
          return phaseVisible(layoutNode?.phase);
        }
        return true;
      })
      .map((node) => {
        if (
          node.type !== "traceEvent" &&
          node.type !== "doGroup" &&
          node.type !== "pickGroup"
        ) {
          return node;
        }
        const isMatch =
          matchSet.has(node.id) ||
          ((node.type === "doGroup" || node.type === "pickGroup") &&
            groupIdsWithChildMatch.has(node.id));
        const isFocused = node.id === activeNodeId;
        const isSelected = selectedNode?.id === node.id;
        const isGroup = node.type === "doGroup" || node.type === "pickGroup";
        const groupCollapsed = isGroup && collapsedGroupIds.has(node.id);
        const collapsedH = DO_GROUP_HEADER;
        return {
          ...node,
          ...(groupCollapsed
            ? {
                style: { ...node.style, height: collapsedH },
                height: collapsedH,
                measured: {
                  width: node.measured?.width ?? node.width ?? 0,
                  height: collapsedH,
                },
              }
            : {}),
          data: {
            ...node.data,
            searchActive: hasSearch,
            searchMatch: isMatch,
            searchFocused: isFocused,
            nodeSelected: isSelected,
            ...(isGroup
              ? {
                  groupCollapsed,
                  onToggleGroupCollapse: () => toggleGroupCollapse(node.id),
                }
              : {}),
          },
        };
      });
  }, [
    nodes,
    searchQuery,
    matchIds,
    activeNodeId,
    selectedNode?.id,
    layout.nodes,
    visibleLayoutNodes,
    phaseVisible,
    collapsedGroupIds,
    toggleGroupCollapse,
  ]);

  const displayedEdges = useMemo(() => {
    const visibleEventIds = new Set(
      displayedNodes
        .filter(
          (n) =>
            n.type === "traceEvent" ||
            n.type === "doGroup" ||
            n.type === "pickGroup",
        )
        .map((n) => n.id),
    );
    const phaseFiltered = edges.filter(
      (edge) =>
        visibleEventIds.has(edge.source) && visibleEventIds.has(edge.target),
    );

    const hasSearch = searchQuery.trim().length > 0 && matchIds.length > 0;
    const selectedId = selectedNode?.id ?? null;
    const selectedGroupId = selectedNode?.doGroupId?.trim() || null;
    const hasSelection = Boolean(selectedId);
    if (!hasSearch && !hasSelection) return phaseFiltered;

    const matchSet = hasSearch ? new Set(matchIds) : null;
    const selectionIds = new Set<string>();
    if (selectedId) {
      selectionIds.add(selectedId);
      if (selectedGroupId) selectionIds.add(selectedGroupId);
    }

    const focusStroke = "#1565c0";
    const matchStroke = "#ef6c00";
    const selectStroke = "#2e7d32";

    return phaseFiltered.map((edge) => {
      const sourceMatch = matchSet?.has(edge.source) ?? false;
      const targetMatch = matchSet?.has(edge.target) ?? false;
      const connectsHighlighted =
        Boolean(matchSet) && sourceMatch && targetMatch;
      const touchesFocus = Boolean(
        activeNodeId &&
          (edge.source === activeNodeId || edge.target === activeNodeId),
      );
      const touchesSelection =
        hasSelection &&
        (selectionIds.has(edge.source) || selectionIds.has(edge.target));
      const shouldHighlight =
        connectsHighlighted || touchesFocus || touchesSelection;

      if (!shouldHighlight) {
        return {
          ...edge,
          zIndex: 0,
          style: {
            ...edge.style,
            stroke: "#bdbdbd",
            strokeWidth: 1,
            opacity: 0.18,
          },
          labelStyle: { ...edge.labelStyle, opacity: 0.2 },
          markerEnd: {
            type: MarkerType.ArrowClosed,
            color: "#bdbdbd",
            width: 14,
            height: 14,
          },
        };
      }

      const stroke = touchesSelection
        ? selectStroke
        : touchesFocus
          ? focusStroke
          : matchStroke;
      const strokeWidth = touchesSelection || touchesFocus ? 3 : 2.5;
      const zIndex = touchesSelection ? 4 : touchesFocus ? 3 : 2;

      return {
        ...edge,
        zIndex,
        style: {
          ...edge.style,
          stroke,
          strokeWidth,
          opacity: 1,
        },
        labelStyle: {
          ...edge.labelStyle,
          fill: touchesSelection
            ? selectStroke
            : touchesFocus
              ? focusStroke
              : "#e65100",
          fontWeight: 700,
          opacity: 1,
        },
        labelBgStyle: {
          ...edge.labelBgStyle,
          fill: touchesSelection
            ? "#e8f5e9"
            : touchesFocus
              ? "#e3f2fd"
              : "#fff8e1",
          fillOpacity: 1,
        },
        markerEnd: {
          type: MarkerType.ArrowClosed,
          color: stroke,
          width: touchesSelection || touchesFocus ? 18 : 16,
          height: touchesSelection || touchesFocus ? 18 : 16,
        },
      };
    });
  }, [
    edges,
    searchQuery,
    matchIds,
    activeNodeId,
    selectedNode,
    displayedNodes,
  ]);

  useEffect(() => {
    setNodes(nodesWithCallbacks);
    setEdges(graphElements.edges);
    const timer = window.setTimeout(() => {
      fitView({
        padding: 0.04,
        minZoom: FIT_VIEW_MIN_ZOOM,
        maxZoom: FIT_VIEW_MAX_ZOOM,
        duration: 200,
      });
    }, 50);
    return () => window.clearTimeout(timer);
  }, [nodesWithCallbacks, graphElements.edges, setNodes, setEdges, fitView]);

  useEffect(() => {
    if (!activeNodeId || !searchQuery.trim()) return;
    const timer = window.setTimeout(() => {
      fitView({
        nodes: focusFitNodes,
        padding: 0.55,
        duration: 280,
        maxZoom: 1.25,
      });
    }, 60);
    return () => window.clearTimeout(timer);
  }, [activeNodeId, searchQuery, focusFitNodes, fitView]);

  useEffect(() => {
    if (!focusWarehouse) return;
    const { inventoryLotId, warehouseCode } = focusWarehouse;
    const locPrefix = `loc-${inventoryLotId}-`;
    const wh = warehouseCode.trim().toUpperCase();
    const focusIds = layout.nodes
      .filter((n) => {
        if (n.id.startsWith(locPrefix)) return true;
        if (inventoryLotId !== data.lot.inventoryLotId) return false;
        if (n.id.startsWith("loc-")) return false;
        if (!wh) return true;
        return (n.warehouseCode ?? "").trim().toUpperCase() === wh;
      })
      .map((n) => n.id);
    if (focusIds.length === 0) return;
    setSearchQuery(warehouseCode.trim() || String(inventoryLotId));
    setActiveMatchIndex(0);
    const timer = window.setTimeout(() => {
      fitView({
        nodes: focusIds.map((id) => ({ id })),
        padding: 0.2,
        duration: 320,
        maxZoom: 1.1,
      });
    }, 80);
    return () => window.clearTimeout(timer);
  }, [focusWarehouse, layout.nodes, data.lot.inventoryLotId, fitView]);

  const handleSearchPrev = useCallback(() => {
    if (matchIds.length === 0) return;
    setActiveMatchIndex(
      (prev) => (prev - 1 + matchIds.length) % matchIds.length,
    );
  }, [matchIds.length]);

  const handleSearchNext = useCallback(() => {
    if (matchIds.length === 0) return;
    setActiveMatchIndex((prev) => (prev + 1) % matchIds.length);
  }, [matchIds.length]);

  const handleSearchClear = useCallback(() => {
    setSearchQuery("");
    setActiveMatchIndex(0);
  }, []);

  const onNodeClick = useCallback(
    (_: React.MouseEvent, node: Node<TraceFlowNodeData>) => {
      if (node.type === "traceEvent" && node.data?.layoutNode?.id) {
        setSelectedNode(node.data.layoutNode);
      }
    },
    [],
  );

  const translateExtent = useMemo(
    () => reactFlowGraphExtent(layout, graphElements.graphHeight),
    [layout, graphElements.graphHeight],
  );

  if (!layout.nodes.length) {
    return (
      <Paper variant="outlined" sx={{ p: 2 }}>
        <Typography color="text.secondary">{t("noRecords")}</Typography>
      </Paper>
    );
  }

  return (
    <Paper variant="outlined" sx={{ p: 2 }}>
      <Stack
        direction={{ xs: "column", sm: "row" }}
        alignItems={{ sm: "center" }}
        spacing={1}
        sx={{ mb: 1 }}
      >
        <Typography variant="subtitle1" fontWeight={600} sx={{ flexGrow: 1 }}>
          {t("flowGraph")}
        </Typography>
        <Tooltip title={t("flowLegendOpen")} arrow>
          <IconButton
            size="small"
            aria-label={t("flowLegendOpen")}
            onClick={(e) => setLegendAnchor(e.currentTarget)}
          >
            <InfoOutlinedIcon fontSize="small" />
          </IconButton>
        </Tooltip>
      </Stack>

      <Typography
        variant="caption"
        color="text.secondary"
        display="block"
        sx={{ mb: 1 }}
      >
        {hasJoPrelude ? t("flowGraphHintJo") : t("flowGraphHint")}
        {" · "}
        {t("flowZoomPanHint")}
      </Typography>

      <Stack spacing={0.75} alignItems="center" sx={{ mb: 1.5 }}>
        <Typography variant="caption" color="text.secondary">
          {t("flowPhaseFilter")}
        </Typography>
        <Stack
          direction="row"
          flexWrap="wrap"
          gap={0.75}
          alignItems="center"
          justifyContent="center"
        >
          {layout.phaseOrder.map((phase) => {
            const active = phaseVisible(phase);
            const chipColor = phaseChipColor(phase);
            return (
              <Chip
                key={phase}
                size="small"
                label={t(phaseLabelKey(phase))}
                color={chipColor}
                variant={active ? "filled" : "outlined"}
                onClick={() => togglePhase(phase)}
                sx={{
                  borderRadius: 1.5,
                  height: 28,
                  fontWeight: 600,
                  cursor: "pointer",
                  letterSpacing: 0.1,
                  opacity: active ? 1 : 0.8,
                  transition: "box-shadow 0.15s ease, opacity 0.15s ease, filter 0.15s ease",
                  "& .MuiChip-label": { px: 1.25 },
                  "&:hover": {
                    opacity: 1,
                    boxShadow: 1,
                    filter: active ? "brightness(1.06)" : undefined,
                  },
                }}
              />
            );
          })}
          <Chip
            size="small"
            icon={<RestartAltIcon />}
            label={t("flowPhaseFilterReset")}
            color="error"
            variant="filled"
            disabled={hiddenPhases.size === 0}
            onClick={hiddenPhases.size > 0 ? resetPhaseFilter : undefined}
            sx={{
              borderRadius: 1.5,
              height: 28,
              fontWeight: 700,
              cursor: hiddenPhases.size === 0 ? "default" : "pointer",
              letterSpacing: 0.2,
              "& .MuiChip-icon": { ml: 0.5, fontSize: 16 },
              "& .MuiChip-label": { px: 1.25 },
              "&:hover":
                hiddenPhases.size === 0
                  ? undefined
                  : {
                      boxShadow: 1,
                      filter: "brightness(1.08)",
                    },
            }}
          />
        </Stack>
      </Stack>

      <Popover
        open={Boolean(legendAnchor)}
        anchorEl={legendAnchor}
        onClose={() => setLegendAnchor(null)}
        anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
        transformOrigin={{ vertical: "top", horizontal: "right" }}
      >
        <Box sx={{ p: 2, maxWidth: 360 }}>
          <Typography variant="subtitle2" fontWeight={600} sx={{ mb: 1 }}>
            {t("flowLegendTitle")}
          </Typography>
          <Stack spacing={1}>
            {LEGEND_ROWS.map(({ key, tipKey }) => (
              <Box key={key}>
                <Typography variant="body2" fontWeight={600}>
                  {t(key)}
                </Typography>
                <Typography variant="caption" color="text.secondary">
                  {t(tipKey)}
                </Typography>
              </Box>
            ))}
            {hasJoPrelude && (
              <Typography variant="caption" color="text.secondary">
                {t("flowGraphPathJo")}
              </Typography>
            )}
            <Typography variant="caption" color="text.secondary">
              {t("flowNodeDetailHint")}
            </Typography>
          </Stack>
        </Box>
      </Popover>

      <Box
        sx={{
          height: VIEWPORT_HEIGHT,
          borderRadius: 1,
          border: 1,
          borderColor: "divider",
          overflow: "hidden",
          bgcolor: "action.hover",
          display: "flex",
          "& .react-flow__attribution": { display: "none" },
        }}
      >
        {selectedNode && (
          <Box
            sx={{ width: DETAIL_PANEL_WIDTH, flexShrink: 0, height: "100%" }}
          >
            <ItemTracingNodeDetailPanel
              node={selectedNode}
              onClose={() => setSelectedNode(null)}
            />
          </Box>
        )}
        <Box
          sx={{ flex: 1, minWidth: 0, height: "100%", position: "relative" }}
        >
          <ReactFlow
            nodes={displayedNodes}
            edges={displayedEdges}
            onNodesChange={onNodesChange}
            onEdgesChange={onEdgesChange}
            elevateEdgesOnSelect={false}
            defaultEdgeOptions={{ zIndex: 0 }}
            nodeTypes={traceFlowNodeTypes}
            edgeTypes={traceFlowEdgeTypes}
            onNodeClick={onNodeClick}
            onPaneClick={() => setSelectedNode(null)}
            nodesDraggable={false}
            nodesConnectable={false}
            elementsSelectable
            panOnScroll
            zoomOnScroll
            minZoom={0.15}
            maxZoom={2.5}
            translateExtent={translateExtent}
            proOptions={{ hideAttribution: true }}
            style={{ width: "100%", height: "100%" }}
          >
            <Panel position="top-right">
              <ItemTracingFlowGraphSearch
                query={searchQuery}
                matchCount={matchIds.length}
                activeIndex={activeMatchIndex}
                onQueryChange={setSearchQuery}
                onPrev={handleSearchPrev}
                onNext={handleSearchNext}
                onClear={handleSearchClear}
              />
            </Panel>
            <Background
              variant={BackgroundVariant.Dots}
              gap={16}
              size={1}
              color="#bdbdbd"
            />
            <Panel position="bottom-left" style={{ left: 12, bottom: 12 }}>
              <Stack spacing={0.5} alignItems="center">
                <Tooltip
                  title={
                    showMinimap ? t("flowMinimapHide") : t("flowMinimapShow")
                  }
                  arrow
                  placement="right"
                >
                  <IconButton
                    size="small"
                    onClick={() => setShowMinimap((v) => !v)}
                    aria-label={
                      showMinimap ? t("flowMinimapHide") : t("flowMinimapShow")
                    }
                    sx={{
                      bgcolor: "background.paper",
                      border: 1,
                      borderColor: "divider",
                      boxShadow: 1,
                      borderRadius: 1,
                      width: 28,
                      height: 28,
                      "&:hover": { bgcolor: "background.paper" },
                    }}
                  >
                    <MapOutlinedIcon
                      fontSize="small"
                      color={showMinimap ? "primary" : "action"}
                    />
                  </IconButton>
                </Tooltip>
                <Box
                  sx={{
                    "& .react-flow__controls": {
                      position: "static",
                      margin: 0,
                      boxShadow: 1,
                    },
                  }}
                >
                  <Controls showInteractive={false} />
                </Box>
              </Stack>
            </Panel>
            {showMinimap && (
              <MiniMap
                nodeColor={(node) => {
                  if (
                    node.type !== "traceEvent" &&
                    node.type !== "doGroup" &&
                    node.type !== "pickGroup"
                  ) {
                    return "#e0e0e0";
                  }
                  const layoutNode = (
                    node.data as TraceFlowNodeData | undefined
                  )?.layoutNode;
                  return layoutNode?.kind
                    ? minimapNodeColor(layoutNode.kind)
                    : "#e0e0e0";
                }}
                pannable
                zoomable
                style={{ border: "1px solid #e0e0e0", borderRadius: 4 }}
              />
            )}
          </ReactFlow>
        </Box>
      </Box>
    </Paper>
  );
};

/** FP-MTMS Version Checklist | Functions Ref. No. 14 | v1.0.0 | 2026-07-17 */
const ItemTracingFlowGraph: React.FC<Props> = (props) => (
  <ReactFlowProvider>
    <ItemTracingFlowGraphInner {...props} />
  </ReactFlowProvider>
);

export default ItemTracingFlowGraph;
