/*
 * This module provides coordinate assignment based on Brandes and Köpf, "Fast
 * and Simple Horizontal Coordinate Assignment."
 */
import type { ID } from '../../../types';
import { EdgeData, NodeData } from '../../../types';
import { DagreGraph, GraphNode } from '../graph';
import type { DagreAlign } from '../types';
import { buildLayerMatrix, minBy } from '../util';

/*
 * Marks all edges in the graph with a type-1 conflict with the "type1Conflict"
 * property. A type-1 conflict is one where a non-inner segment crosses an
 * inner segment. An inner segment is an edge with both incident nodes marked
 * with the "dummy" property.
 *
 * This algorithm scans layer by layer, starting with the second, for type-1
 * conflicts between the current layer and the previous layer. For each layer
 * it scans the nodes from left to right until it reaches one that is incident
 * on an inner segment. It then scans predecessors to determine if they have
 * edges that cross that inner segment. At the end a final scan is done for all
 * nodes on the current rank to see if they cross the last visited inner
 * segment.
 *
 * This algorithm (safely) assumes that a dummy node will only be incident on a
 * single node in the layers being scanned.
 */

type Conflicts = Record<ID, Record<ID, boolean>>;

export const findType1Conflicts = (g: DagreGraph, layering?: ID[][]) => {
  const conflicts = {};

  const visitLayer = (prevLayer: ID[], layer: ID[]) => {
    // last visited node in the previous layer that is incident on an inner
    // segment.
    let k0 = 0;
    // Tracks the last node in this layer scanned for crossings with a type-1
    // segment.
    let scanPos = 0;
    const prevLayerLength = prevLayer.length;
    const lastNode = layer?.[layer?.length - 1];

    layer?.forEach((v: ID, i: number) => {
      const w = findOtherInnerSegmentNode(g, v);
      const k1 = w ? g.getNode(w.id)!.data.order! : prevLayerLength;

      if (w || v === lastNode) {
        layer.slice(scanPos, i + 1)?.forEach((scanNode) => {
          g.getPredecessors(scanNode)?.forEach((u) => {
            const uLabel = g.getNode(u.id)!;
            const uPos = uLabel.data.order!;
            if (
              (uPos < k0 || k1 < uPos) &&
              !(uLabel.data.dummy && g.getNode(scanNode)?.data.dummy)
            ) {
              addConflict(conflicts, u.id, scanNode);
            }
          });
        });
        scanPos = i + 1;
        k0 = k1;
      }
    });

    return layer;
  };

  if (layering?.length) {
    layering.reduce(visitLayer);
  }
  return conflicts;
};

export const findType2Conflicts = (g: DagreGraph, layering?: ID[][]) => {
  const conflicts = {};

  function scan(
    south: ID[],
    southPos: number,
    southEnd: number,
    prevNorthBorder: number,
    nextNorthBorder: number,
  ) {
    let v: ID;
    for (let i = southPos; i < southEnd; i++) {
      v = south[i];
      if (g.getNode(v)?.data.dummy) {
        g.getPredecessors(v)?.forEach((u) => {
          const uNode = g.getNode(u.id)!;
          if (
            uNode.data.dummy &&
            (uNode.data.order! < prevNorthBorder ||
              uNode.data.order! > nextNorthBorder)
          ) {
            addConflict(conflicts, u.id, v);
          }
        });
      }
    }
  }

  function getScannedKey(params: Parameters<typeof scan>) {
    // south数组可能很大，不适合做key
    return JSON.stringify(params.slice(1));
  }

  function scanIfNeeded(
    params: Parameters<typeof scan>,
    scanCache: Map<ID, boolean>,
  ) {
    const cacheKey = getScannedKey(params);

    if (scanCache.get(cacheKey)) return;

    scan(...params);
    scanCache.set(cacheKey, true);
  }

  const visitLayer = (north: ID[], south: ID[]) => {
    let prevNorthPos = -1;
    let nextNorthPos: number;
    let southPos = 0;

    const scanned = new Map<ID, boolean>();

    south?.forEach((v: ID, southLookahead: number) => {
      if (g.getNode(v)?.data.dummy === 'border') {
        const predecessors = g.getPredecessors(v) || [];
        if (predecessors.length) {
          nextNorthPos = g.getNode(predecessors[0].id)!.data.order!;
          scanIfNeeded(
            [south, southPos, southLookahead, prevNorthPos, nextNorthPos],
            scanned,
          );
          southPos = southLookahead;
          prevNorthPos = nextNorthPos;
        }
      }
      scanIfNeeded(
        [south, southPos, south.length, nextNorthPos, north.length],
        scanned,
      );
    });

    return south;
  };

  if (layering?.length) {
    layering.reduce(visitLayer);
  }
  return conflicts;
};

export const findOtherInnerSegmentNode = (g: DagreGraph, v: ID) => {
  if (g.getNode(v)?.data.dummy) {
    return g.getPredecessors(v)?.find((u) => g.getNode(u.id).data.dummy);
  }
};

export const addConflict = (conflicts: Conflicts, v: ID, w: ID) => {
  let vv = v;
  let ww = w;
  if (vv > ww) {
    const tmp = vv;
    vv = ww;
    ww = tmp;
  }

  let conflictsV = conflicts[vv];
  if (!conflictsV) {
    conflicts[vv] = conflictsV = {};
  }
  conflictsV[ww] = true;
};

export const hasConflict = (conflicts: Conflicts, v: ID, w: ID) => {
  let vv = v;
  let ww = w;
  if (vv > ww) {
    const tmp = v;
    vv = ww;
    ww = tmp;
  }
  return !!conflicts[vv];
};

/*
 * Try to align nodes into vertical "blocks" where possible. This algorithm
 * attempts to align a node with one of its median neighbors. If the edge
 * connecting a neighbor is a type-1 conflict then we ignore that possibility.
 * If a previous node has already formed a block with a node after the node
 * we're trying to form a block with, we also ignore that possibility - our
 * blocks would be split in that scenario.
 */
export const verticalAlignment = (
  g: DagreGraph,
  layering: ID[][],
  conflicts: Conflicts,
  neighborFn: (v: ID) => NodeData[],
) => {
  const root: Record<ID, ID> = {};
  const align: Record<ID, ID> = {};
  const pos: Record<ID, number> = {};

  // We cache the position here based on the layering because the graph and
  // layering may be out of sync. The layering matrix is manipulated to
  // generate different extreme alignments.
  layering?.forEach((layer) => {
    layer?.forEach((v, order: number) => {
      root[v] = v;
      align[v] = v;
      pos[v] = order;
    });
  });

  layering?.forEach((layer) => {
    let prevIdx = -1;
    layer?.forEach((v) => {
      let ws = neighborFn(v).map((n) => n.id);

      if (ws.length) {
        ws = ws.sort((a: ID, b: ID) => pos[a] - pos[b]);
        const mp = (ws.length - 1) / 2;
        for (let i = Math.floor(mp), il = Math.ceil(mp); i <= il; ++i) {
          const w = ws[i];
          if (
            align[v] === v &&
            prevIdx < pos[w] &&
            !hasConflict(conflicts, v, w)
          ) {
            align[w] = v;
            align[v] = root[v] = root[w];
            prevIdx = pos[w];
          }
        }
      }
    });
  });

  return { root, align };
};

export const horizontalCompaction = (
  g: DagreGraph,
  layering: ID[][],
  root: Record<ID, ID>,
  align: Record<ID, ID>,
  nodesep: number,
  edgesep: number,
  reverseSep?: boolean,
) => {
  // This portion of the algorithm differs from BK due to a number of problems.
  // Instead of their algorithm we construct a new block graph and do two
  // sweeps. The first sweep places blocks with the smallest possible
  // coordinates. The second sweep removes unused space by moving blocks to the
  // greatest coordinates without violating separation.
  const xs: Record<ID, number> = {};
  const blockG = buildBlockGraph(
    g,
    layering,
    root,
    nodesep,
    edgesep,
    reverseSep,
  );
  const borderType = reverseSep ? 'borderLeft' : 'borderRight';

  const iterate = (
    setXsFunc: (param: ID) => void,
    nextNodesFunc: (param: ID) => GraphNode<NodeData>[],
  ) => {
    let stack = blockG.getAllNodes();
    let elem = stack.pop();
    const visited: Record<ID, boolean> = {};
    while (elem) {
      if (visited[elem.id]) {
        setXsFunc(elem.id);
      } else {
        visited[elem.id] = true;
        stack.push(elem);
        stack = stack.concat(nextNodesFunc(elem.id));
      }

      elem = stack.pop();
    }
  };

  // First pass, assign smallest coordinates
  const pass1 = (elem: ID) => {
    xs[elem] = (blockG.getRelatedEdges(elem, 'in') || []).reduce(
      (acc: number, e) => {
        return Math.max(acc, (xs[e.source] || 0) + e.data.weight!);
      },
      0,
    );
  };

  // Second pass, assign greatest coordinates
  const pass2 = (elem: ID) => {
    const min = (blockG.getRelatedEdges(elem, 'out') || []).reduce(
      (acc: number, e) => {
        return Math.min(acc, (xs[e.target] || 0) - e.data.weight!);
      },
      Number.POSITIVE_INFINITY,
    );

    const node = g.getNode(elem)!;
    if (
      min !== Number.POSITIVE_INFINITY &&
      node.data.borderType !== borderType
    ) {
      xs[elem] = Math.max(xs[elem], min);
    }
  };

  iterate(pass1, blockG.getPredecessors.bind(blockG));
  iterate(pass2, blockG.getSuccessors.bind(blockG));

  // Assign x coordinates to all nodes
  Object.values(align)?.forEach((v) => {
    xs[v] = xs[root[v]];
  });

  return xs;
};

export const buildBlockGraph = (
  g: DagreGraph,
  layering: ID[][],
  root: Record<ID, ID>,
  nodesep: number,
  edgesep: number,
  reverseSep?: boolean,
): DagreGraph<NodeData, EdgeData> => {
  const blockGraph = new DagreGraph<NodeData, EdgeData>();
  const sepFn = sep(nodesep, edgesep, reverseSep as boolean);

  layering?.forEach((layer) => {
    let u: ID;
    layer?.forEach((v) => {
      const vRoot = root[v];
      if (!blockGraph.hasNode(vRoot)) {
        blockGraph.addNode({
          id: vRoot,
          data: {},
        });
      }
      if (u) {
        const uRoot = root[u];

        const edge = blockGraph
          .getRelatedEdges(uRoot, 'out')
          .find((edge) => edge.target === vRoot);
        if (!edge) {
          blockGraph.addEdge({
            id: `e${Math.random()}`,
            source: uRoot,
            target: vRoot,
            data: {
              weight: Math.max(sepFn(g, v, u), 0),
            },
          });
        } else {
          blockGraph.updateEdgeData(edge.id, {
            ...edge.data,
            weight: Math.max(sepFn(g, v, u), edge.data.weight! || 0),
          });
        }
      }
      u = v;
    });
  });

  return blockGraph;
};
/*
 * Returns the alignment that has the smallest width of the given alignments.
 */
export const findSmallestWidthAlignment = (
  g: DagreGraph,
  xss: Record<string, Record<string, number>>,
) => {
  return minBy(Object.values(xss), (xs) => {
    let max = Number.NEGATIVE_INFINITY;
    let min = Number.POSITIVE_INFINITY;

    Object.keys(xs)?.forEach((v: string) => {
      const x = xs[v];
      const halfWidth = width(g, v) / 2;

      max = Math.max(x + halfWidth, max);
      min = Math.min(x - halfWidth, min);
    });

    return max - min;
  });
};

/*
 * Align the coordinates of each of the layout alignments such that
 * left-biased alignments have their minimum coordinate at the same point as
 * the minimum coordinate of the smallest width alignment and right-biased
 * alignments have their maximum coordinate at the same point as the maximum
 * coordinate of the smallest width alignment.
 */
export function alignCoordinates(
  xss: Record<string, Record<string, number>>,
  alignTo: Record<string, number>,
) {
  const alignToVals = Object.values(alignTo);
  const alignToMin = Math.min(...alignToVals);
  const alignToMax = Math.max(...alignToVals);

  ['u', 'd'].forEach((vert) => {
    ['l', 'r'].forEach((horiz) => {
      const alignment = vert + horiz;
      const xs = xss[alignment];
      let delta: number;
      if (xs === alignTo) return;

      const xsVals = Object.values(xs);
      delta =
        horiz === 'l'
          ? alignToMin - Math.min(...xsVals)
          : alignToMax - Math.max(...xsVals);

      if (delta) {
        xss[alignment] = {};
        Object.keys(xs).forEach((key) => {
          xss[alignment][key] = xs[key] + delta;
        });
      }
    });
  });
}

export const balance = (
  xss: Record<string, Record<string, number>>,
  align?: DagreAlign,
) => {
  const result: Record<string, number> = {};
  Object.keys(xss.ul).forEach((key) => {
    if (align) {
      result[key] = xss[align.toLowerCase()][key];
    } else {
      const values = Object.values(xss).map((x) => x[key]);
      result[key] = (values[0] + values[1]) / 2; // (ur + ul) / 2
    }
  });
  return result;
};

export const positionX = (
  g: DagreGraph,
  options?: Partial<{
    align: DagreAlign;
    nodesep: number;
    edgesep: number;
  }>,
) => {
  const { align: graphAlign, nodesep = 0, edgesep = 0 } = options || {};

  const layering = buildLayerMatrix(g);
  const conflicts = Object.assign(
    findType1Conflicts(g, layering),
    findType2Conflicts(g, layering),
  );

  const xss: Record<string, Record<ID, number>> = {};
  let adjustedLayering: ID[][];
  ['u', 'd'].forEach((vert) => {
    adjustedLayering =
      vert === 'u' ? layering : Object.values(layering).reverse();
    ['l', 'r'].forEach((horiz) => {
      if (horiz === 'r') {
        adjustedLayering = adjustedLayering.map((inner) =>
          Object.values(inner).reverse(),
        );
      }

      const neighborFn = (
        vert === 'u' ? g.getPredecessors : g.getSuccessors
      ).bind(g);
      const align = verticalAlignment(
        g,
        adjustedLayering,
        conflicts,
        neighborFn,
      );
      const xs = horizontalCompaction(
        g,
        adjustedLayering,
        align.root,
        align.align,
        nodesep,
        edgesep,
        horiz === 'r',
      );
      if (horiz === 'r') {
        Object.keys(xs).forEach((key) => {
          xs[key] = -xs[key];
        });
      }
      xss[vert + horiz] = xs;
    });
  });

  const smallestWidth = findSmallestWidthAlignment(g, xss);
  alignCoordinates(xss, smallestWidth);
  return balance(xss, graphAlign);
};

export const sep = (nodeSep: number, edgeSep: number, reverseSep: boolean) => {
  return (g: DagreGraph, v: ID, w: ID) => {
    const vLabel = g.getNode(v)!;
    const wLabel = g.getNode(w)!;
    let sum = 0;
    let delta: number = 0;

    sum += vLabel.data.width! / 2;
    if (vLabel.data.hasOwnProperty('labelpos')) {
      switch (((vLabel.data.labelpos as string) || '').toLowerCase()) {
        case 'l':
          delta = -vLabel.data.width! / 2;
          break;
        case 'r':
          delta = vLabel.data.width! / 2;
          break;
      }
    }
    if (delta) {
      sum += reverseSep ? delta : -delta;
    }
    delta = 0;

    sum += (vLabel.data.dummy ? edgeSep : nodeSep) / 2;
    sum += (wLabel.data.dummy ? edgeSep : nodeSep) / 2;

    sum += wLabel.data.width! / 2;
    if (wLabel.data.labelpos) {
      switch (((wLabel.data.labelpos as string) || '').toLowerCase()) {
        case 'l':
          delta = wLabel.data.width! / 2;
          break;
        case 'r':
          delta = -wLabel.data.width! / 2;
          break;
      }
    }
    if (delta) {
      sum += reverseSep ? delta : -delta;
    }
    delta = 0;

    return sum;
  };
};

export const width = (g: DagreGraph, v: ID) => g.getNode(v)!.data.width! || 0;
