(function() {
  "use strict";
  function buildLayerAdjacency(boxes) {
    const refToLayer = {};
    const adjacency = {};
    if (!Array.isArray(boxes)) return { refToLayer, adjacency };
    const typedBoxes = boxes;
    typedBoxes.forEach(function(box, li) {
      const nodes = box && box.nodes ? box.nodes : [];
      nodes.forEach(function(n) {
        if (n && n.ref) refToLayer[n.ref] = li;
      });
    });
    typedBoxes.forEach(function(box) {
      const nodes = box && box.nodes ? box.nodes : [];
      nodes.forEach(function(n) {
        if (!n || !n.ref) return;
        const fromRef = n.ref;
        const rels = n.relationships && n.relationships.to || [];
        rels.forEach(function(rel) {
          const toRef = rel && rel.ref;
          if (!toRef || toRef === fromRef) return;
          if (refToLayer[toRef] == null) return;
          (adjacency[fromRef] = adjacency[fromRef] || []).push(toRef);
          (adjacency[toRef] = adjacency[toRef] || []).push(fromRef);
        });
      });
    });
    return { refToLayer, adjacency };
  }
  function reorderLayersForCrossingReduction(boxes) {
    if (!Array.isArray(boxes) || boxes.length < 2) return boxes;
    const typedBoxes = boxes;
    const workingLayers = typedBoxes.map(function(box) {
      return (box.nodes || []).slice();
    });
    const adj = buildLayerAdjacency(typedBoxes);
    const refToLayer = adj.refToLayer;
    const adjacency = adj.adjacency;
    let posIndex = {};
    function rebuildPosIndex() {
      posIndex = {};
      workingLayers.forEach(function(layer) {
        layer.forEach(function(n, p) {
          if (n && n.ref) posIndex[n.ref] = p;
        });
      });
    }
    rebuildPosIndex();
    function barycentreFor(node, considerLayerIdx) {
      if (!node.ref) return null;
      const neighbours = adjacency[node.ref] || [];
      let sum = 0;
      let count = 0;
      for (let k = 0; k < neighbours.length; k++) {
        const nbr = neighbours[k];
        if (refToLayer[nbr] === considerLayerIdx && posIndex[nbr] != null) {
          sum += posIndex[nbr];
          count++;
        }
      }
      return count === 0 ? null : sum / count;
    }
    function sortLayerByNeighbour(layerIdx, neighbourLayerIdx) {
      if (neighbourLayerIdx < 0 || neighbourLayerIdx >= workingLayers.length) return;
      const layer = workingLayers[layerIdx];
      const decorated = layer.map(function(n, i) {
        const b = n && n.ref ? barycentreFor(n, neighbourLayerIdx) : null;
        return { node: n, bary: b != null ? b : i, originalIndex: i };
      });
      decorated.sort(function(a, b) {
        if (a.bary !== b.bary) return a.bary - b.bary;
        return a.originalIndex - b.originalIndex;
      });
      workingLayers[layerIdx] = decorated.map(function(d) {
        return d.node;
      });
    }
    function crossingsBetween(upperIdx) {
      if (upperIdx >= workingLayers.length - 1) return 0;
      const upper = workingLayers[upperIdx];
      const pairs = [];
      for (let i = 0; i < upper.length; i++) {
        const n = upper[i];
        if (!n || !n.ref) continue;
        const nbrs = adjacency[n.ref] || [];
        for (let k = 0; k < nbrs.length; k++) {
          if (refToLayer[nbrs[k]] === upperIdx + 1) {
            const vPos = posIndex[nbrs[k]];
            if (vPos != null) pairs.push({ u: i, v: vPos });
          }
        }
      }
      let c = 0;
      for (let a = 0; a < pairs.length; a++) {
        for (let b = a + 1; b < pairs.length; b++) {
          if ((pairs[a].u - pairs[b].u) * (pairs[a].v - pairs[b].v) < 0) c++;
        }
      }
      return c;
    }
    function totalCrossings() {
      let t = 0;
      for (let i = 0; i < workingLayers.length - 1; i++) t += crossingsBetween(i);
      return t;
    }
    const MAX_ITER = 12;
    let prevCrossings = totalCrossings();
    for (let iter = 0; iter < MAX_ITER; iter++) {
      for (let i = 1; i < workingLayers.length; i++) {
        sortLayerByNeighbour(i, i - 1);
        rebuildPosIndex();
      }
      for (let j = workingLayers.length - 2; j >= 0; j--) {
        sortLayerByNeighbour(j, j + 1);
        rebuildPosIndex();
      }
      const newCrossings = totalCrossings();
      if (newCrossings >= prevCrossings) break;
      prevCrossings = newCrossings;
    }
    return typedBoxes.map(function(box, i) {
      return Object.assign({}, box, { nodes: workingLayers[i] });
    });
  }
  function assignParallelSplay(edges) {
    const groups = {};
    edges.forEach(function(e) {
      const a = e.fromRef;
      const b = e.toRef;
      const key = a < b ? a + "\0" + b : b + "\0" + a;
      (groups[key] = groups[key] || []).push(e);
    });
    Object.keys(groups).forEach(function(k) {
      const group = groups[k];
      if (group.length < 2) return;
      group.sort(function(x, y) {
        if (x.fromRef !== y.fromRef) return x.fromRef < y.fromRef ? -1 : 1;
        if (x.toRef !== y.toRef) return x.toRef < y.toRef ? -1 : 1;
        return 0;
      });
      const n = group.length;
      group.forEach(function(e, i) {
        e.parallelOffset = i - (n - 1) / 2;
        e.parallelCount = n;
      });
    });
  }
  const api = {
    buildLayerAdjacency,
    reorderLayersForCrossingReduction,
    assignParallelSplay
  };
  if (typeof module !== "undefined" && module && module.exports) {
    module.exports = api;
  } else {
    const g = globalThis;
    if (g.window) {
      g.window.C2SGraphLayout = api;
    }
  }
})();
