/**
 * data-structure-typed
 *
 * @author Pablo Zeng
 * @copyright Copyright (c) 2022 Pablo Zeng <zrwusa@gmail.com>
 * @license MIT License
 */

import type { DijkstraResult, EntryCallback, GraphOptions, VertexKey } from '../../types';
import { uuidV4 } from '../../utils';
import { ERR, raise } from '../../common';
import { IterableEntryBase } from '../base';
import { IGraph } from '../../interfaces';
import { Heap } from '../heap';
import { Queue } from '../queue';

export abstract class AbstractVertex<V = any> {
  key: VertexKey;
  value: V | undefined;

  protected constructor(key: VertexKey, value?: V) {
    this.key = key;
    this.value = value;
  }
}

export abstract class AbstractEdge<E = any> {
  value: E | undefined;
  weight: number;

  protected constructor(weight?: number, value?: E) {
    this.weight = weight !== undefined ? weight : 1;
    this.value = value;
    this._hashCode = uuidV4();
  }

  protected _hashCode: string;

  get hashCode(): string {
    return this._hashCode;
  }
}

/**
 * Abstract graph over vertices and edges.
 * @template V - Vertex value type.
 * @template E - Edge value type.
 * @template VO - Concrete vertex subclass (extends AbstractVertex<V>).
 * @template EO - Concrete edge subclass (extends AbstractEdge<E>).
 * @remarks Time O(1), Space O(1)
 * @example examples will be generated by unit test
 */
export abstract class AbstractGraph<
  V = any,
  E = any,
  VO extends AbstractVertex<V> = AbstractVertex<V>,
  EO extends AbstractEdge<E> = AbstractEdge<E>
>
  extends IterableEntryBase<VertexKey, V | undefined>
  implements IGraph<V, E, VO, EO>
{
  /**
   * Construct a graph with runtime defaults.
   * @param options - `GraphOptions<V>` in `options.graph` (e.g. `vertexValueInitializer`, `defaultEdgeWeight`).
   * @remarks Time O(1), Space O(1)
   */
  constructor(options?: Partial<Record<string, unknown>>) {
    super();
    const graph = (options as { graph?: GraphOptions<V> })?.graph;
    this._options = { defaultEdgeWeight: 1, ...(graph ?? {}) };
  }

  protected _options: GraphOptions<V> = { defaultEdgeWeight: 1 };

  get options(): Readonly<GraphOptions<V>> {
    return this._options;
  }

  protected _vertexMap: Map<VertexKey, VO> = new Map<VertexKey, VO>();

  get vertexMap(): Map<VertexKey, VO> {
    return this._vertexMap;
  }

  set vertexMap(v: Map<VertexKey, VO>) {
    this._vertexMap = v;
  }

  get size(): number {
    return this._vertexMap.size;
  }

  /**
   * Create a new vertex instance (implementation specific).
   * @param key - Vertex identifier.
   * @param value - Optional payload.
   * @returns Concrete vertex instance.
   * @remarks Time O(1), Space O(1)
   */
  abstract createVertex(key: VertexKey, value?: V): VO;

  /**
   * Create a new edge instance (implementation specific).
   * @param srcOrV1 - Source/endpoint A key.
   * @param destOrV2 - Destination/endpoint B key.
   * @param weight - Edge weight (defaults may apply).
   * @param value - Edge payload.
   * @returns Concrete edge instance.
   * @remarks Time O(1), Space O(1)
   */
  abstract createEdge(srcOrV1: VertexKey, destOrV2: VertexKey, weight?: number, value?: E): EO;

  /**
   * Delete an edge by instance.
   * @param edge - Edge instance.
   * @returns Removed edge or `undefined`.
   * @remarks Time O(1) avg, Space O(1)
   */
  abstract deleteEdge(edge: EO): EO | undefined;

  /**
   * Get an edge between two vertices if present.
   * @param srcOrKey - Source/endpoint A vertex or key.
   * @param destOrKey - Destination/endpoint B vertex or key.
   * @returns Edge instance or `undefined`.
   * @remarks Time O(1) avg, Space O(1)
   */
  abstract getEdge(srcOrKey: VO | VertexKey, destOrKey: VO | VertexKey): EO | undefined;

  /**
   * Degree of a vertex in this graph model.
   * @param vertexOrKey - Vertex or key.
   * @returns Non-negative integer degree.
   * @remarks Time O(1) avg, Space O(1)
   */
  abstract degreeOf(vertexOrKey: VO | VertexKey): number;

  /**
   * All edges in the graph (unique, order not guaranteed).
   * @returns Array of edges.
   * @remarks Time O(E), Space O(E)
   */
  abstract edgeSet(): EO[];

  /**
   * Incident edges of a vertex.
   * @param vertexOrKey - Vertex or key.
   * @returns Array of incident edges.
   * @remarks Time O(deg), Space O(deg)
   */
  abstract edgesOf(vertexOrKey: VO | VertexKey): EO[];

  /**
   * One-step neighbors of a vertex.
   * @param vertexOrKey - Vertex or key.
   * @returns Array of neighbor vertices.
   * @remarks Time O(deg), Space O(deg)
   */
  abstract getNeighbors(vertexOrKey: VO | VertexKey): VO[];

  /**
   * Resolve endpoints of an edge to vertex instances.
   * @param edge - Edge instance.
   * @returns `[v1, v2]` or `undefined` if missing.
   * @remarks Time O(1), Space O(1)
   */
  abstract getEndsOfEdge(edge: EO): [VO, VO] | undefined;

  /**
   * Get vertex instance by key.
   * @param vertexKey - Vertex key.
   * @returns Vertex instance or `undefined`.
   * @remarks Time O(1), Space O(1)
   */
  getVertex(vertexKey: VertexKey): VO | undefined {
    return this._vertexMap.get(vertexKey) || undefined;
  }

  /**
   * Whether a vertex exists.
   * @param vertexOrKey - Vertex or key.
   * @returns `true` if present, otherwise `false`.
   * @remarks Time O(1) avg, Space O(1)
   */
  hasVertex(vertexOrKey: VO | VertexKey): boolean {
    return this._vertexMap.has(this._getVertexKey(vertexOrKey));
  }

  addVertex(vertex: VO): boolean;

  addVertex(key: VertexKey, value?: V): boolean;

  /**
   * Add a vertex by key/value or by pre-built vertex.
   * @param keyOrVertex - Vertex key or existing vertex instance.
   * @param value - Optional payload.
   * @returns `true` if inserted; `false` when key already exists.
   * @remarks Time O(1) avg, Space O(1)
   */
  addVertex(keyOrVertex: VertexKey | VO, value?: V): boolean {
    if (keyOrVertex instanceof AbstractVertex) {
      return this._addVertex(keyOrVertex);
    } else {
      const newVertex = this.createVertex(keyOrVertex, value);
      return this._addVertex(newVertex);
    }
  }

  /**
   * Type guard: check if a value is a valid vertex key.
   * @param potentialKey - Value to test.
   * @returns `true` if string/number; else `false`.
   * @remarks Time O(1), Space O(1)
   */
  isVertexKey(potentialKey: unknown): potentialKey is VertexKey {
    const potentialKeyType = typeof potentialKey;
    return potentialKeyType === 'string' || potentialKeyType === 'number';
  }

  /**
   * Delete a vertex and its incident edges.
   * @param vertexOrKey - Vertex or key.
   * @returns `true` if removed; otherwise `false`.
   * @remarks Time O(deg), Space O(1)
   */
  abstract deleteVertex(vertexOrKey: VO | VertexKey): boolean;

  /**
   * Delete multiple vertices.
   * @param vertexMap - Array of vertices or keys.
   * @returns `true` if any vertex was removed.
   * @remarks Time O(sum(deg)), Space O(1)
   */
  removeManyVertices(vertexMap: VO[] | VertexKey[]): boolean {
    const removed: boolean[] = [];
    for (const v of vertexMap) {
      removed.push(this.deleteVertex(v));
    }
    return removed.length > 0;
  }

  /**
   * Whether an edge exists between two vertices.
   * @param v1 - Endpoint A vertex or key.
   * @param v2 - Endpoint B vertex or key.
   * @returns `true` if present; otherwise `false`.
   * @remarks Time O(1) avg, Space O(1)
   */
  hasEdge(v1: VertexKey | VO, v2: VertexKey | VO): boolean {
    const edge = this.getEdge(v1, v2);
    return !!edge;
  }

  addEdge(edge: EO): boolean;

  addEdge(src: VO | VertexKey, dest: VO | VertexKey, weight?: number, value?: E): boolean;

  /**
   * Add an edge by instance or by `(src, dest, weight?, value?)`.
   * @param srcOrEdge - Edge instance or source vertex/key.
   * @param dest - Destination vertex/key (when adding by pair).
   * @param weight - Edge weight.
   * @param value - Edge payload.
   * @returns `true` if inserted; otherwise `false`.
   * @remarks Time O(1) avg, Space O(1)
   */
  addEdge(srcOrEdge: VO | VertexKey | EO, dest?: VO | VertexKey, weight?: number, value?: E): boolean {
    if (srcOrEdge instanceof AbstractEdge) {
      return this._addEdge(srcOrEdge);
    } else {
      if (dest instanceof AbstractVertex || typeof dest === 'string' || typeof dest === 'number') {
        if (!(this.hasVertex(srcOrEdge) && this.hasVertex(dest))) return false;
        if (srcOrEdge instanceof AbstractVertex) srcOrEdge = srcOrEdge.key;
        if (dest instanceof AbstractVertex) dest = dest.key;
        const newEdge = this.createEdge(srcOrEdge, dest, weight, value);
        return this._addEdge(newEdge);
      } else {
        raise(TypeError, ERR.invalidArgument('dest must be a Vertex or vertex key when srcOrEdge is an Edge.', 'Graph'));
      }
    }
  }

  /**
   * Set the weight of an existing edge.
   * @param srcOrKey - Source vertex or key.
   * @param destOrKey - Destination vertex or key.
   * @param weight - New weight.
   * @returns `true` if updated; otherwise `false`.
   * @remarks Time O(1) avg, Space O(1)
   */
  setEdgeWeight(srcOrKey: VertexKey | VO, destOrKey: VertexKey | VO, weight: number): boolean {
    const edge = this.getEdge(srcOrKey, destOrKey);
    if (edge) {
      edge.weight = weight;
      return true;
    } else {
      return false;
    }
  }

  /**
   * Enumerate simple paths up to a limit.
   * @param v1 - Source vertex or key.
   * @param v2 - Destination vertex or key.
   * @param limit - Maximum number of paths to collect.
   * @returns Array of paths (each path is an array of vertices).
   * @remarks Time O(paths) worst-case exponential, Space O(V + paths)
   */
  getAllPathsBetween(v1: VO | VertexKey, v2: VO | VertexKey, limit = 1000): VO[][] {
    const paths: VO[][] = [];
    const vertex1 = this._getVertex(v1);
    const vertex2 = this._getVertex(v2);

    if (!(vertex1 && vertex2)) {
      return [];
    }

    const stack: { vertex: VO; path: VO[] }[] = [];
    stack.push({ vertex: vertex1, path: [vertex1] });

    while (stack.length > 0) {
      const { vertex, path } = stack.pop()!;

      if (vertex === vertex2) {
        paths.push(path);
        if (paths.length >= limit) return paths;
      }

      const neighbors = this.getNeighbors(vertex);
      for (const neighbor of neighbors) {
        if (!path.includes(neighbor)) {
          const newPath = [...path, neighbor];
          stack.push({ vertex: neighbor, path: newPath });
        }
      }
    }
    return paths;
  }

  /**
   * Sum the weights along a vertex path.
   * @param path - Sequence of vertices.
   * @returns Path weight sum (0 if empty or edge missing).
   * @remarks Time O(L), Space O(1) where L is path length
   */
  getPathSumWeight(path: VO[]): number {
    let sum = 0;
    for (let i = 0; i < path.length; i++) {
      sum += this.getEdge(path[i], path[i + 1])?.weight || 0;
    }
    return sum;
  }

  /**
   * Minimum hops/weight between two vertices.
   * @param v1 - Source vertex or key.
   * @param v2 - Destination vertex or key.
   * @param isWeight - If `true`, compare by path weight; otherwise by hop count.
   * @returns Minimum cost or `undefined` if missing/unreachable.
   * @remarks Time O((V + E) log V) weighted / O(V + E) unweighted, Space O(V + E)
   */
  getMinCostBetween(v1: VO | VertexKey, v2: VO | VertexKey, isWeight?: boolean): number | undefined {
    if (isWeight === undefined) isWeight = false;

    if (isWeight) {
      const allPaths = this.getAllPathsBetween(v1, v2);
      let min = Number.MAX_SAFE_INTEGER;
      for (const path of allPaths) {
        min = Math.min(this.getPathSumWeight(path), min);
      }
      return min;
    } else {
      const vertex2 = this._getVertex(v2);
      const vertex1 = this._getVertex(v1);
      if (!(vertex1 && vertex2)) {
        return undefined;
      }

      const visited: Map<VO, boolean> = new Map();
      const queue = new Queue<VO>([vertex1]);
      visited.set(vertex1, true);
      let cost = 0;
      while (queue.length > 0) {
        for (let i = 0, layerSize = queue.length; i < layerSize; i++) {
          const cur = queue.shift();
          if (cur === vertex2) {
            return cost;
          }

          if (cur !== undefined) {
            const neighbors = this.getNeighbors(cur);
            for (const neighbor of neighbors) {
              if (!visited.has(neighbor)) {
                visited.set(neighbor, true);
                queue.push(neighbor);
              }
            }
          }
        }
        cost++;
      }
      return undefined;
    }
  }

  /**
   * Minimum path (as vertex sequence) between two vertices.
   * @param v1 - Source vertex or key.
   * @param v2 - Destination vertex or key.
   * @param isWeight - If `true`, compare by path weight; otherwise by hop count.
   * @param isDFS - For weighted mode only: if `true`, brute-force all paths; if `false`, use Dijkstra.
   * @returns Vertex sequence, or `undefined`/empty when unreachable depending on branch.
   * @remarks Time O((V + E) log V) weighted / O(V + E) unweighted, Space O(V + E)
   */
  getMinPathBetween(v1: VO | VertexKey, v2: VO | VertexKey, isWeight?: boolean, isDFS = false): VO[] | undefined {
    if (isWeight === undefined) isWeight = false;

    if (isWeight) {
      if (isDFS) {
        const allPaths = this.getAllPathsBetween(v1, v2, 10000);
        let min = Number.MAX_SAFE_INTEGER;
        let minIndex = -1;
        let index = 0;
        for (const path of allPaths) {
          const pathSumWeight = this.getPathSumWeight(path);
          if (pathSumWeight < min) {
            min = pathSumWeight;
            minIndex = index;
          }
          index++;
        }
        return allPaths[minIndex] || undefined;
      } else {
        /**
         * Dijkstra (binary-heap) shortest paths for non-negative weights.
         * @param src - Source vertex or key.
         * @param dest - Optional destination for early stop.
         * @param getMinDist - If `true`, compute global minimum distance.
         * @param genPaths - If `true`, also generate path arrays.
         * @returns Result bag or `undefined` if source missing.
         * @remarks Time O((V + E) log V), Space O(V + E)
         */
        return this.dijkstra(v1, v2, true, true)?.minPath ?? [];
      }
    } else {
      let minPath: VO[] = [];
      const vertex1 = this._getVertex(v1);
      const vertex2 = this._getVertex(v2);
      if (!(vertex1 && vertex2)) return [];

      const dfs = (cur: VO, dest: VO, visiting: Set<VO>, path: VO[]) => {
        visiting.add(cur);
        if (cur === dest) {
          minPath = [vertex1, ...path];
          return;
        }

        const neighbors = this.getNeighbors(cur);
        for (const neighbor of neighbors) {
          if (!visiting.has(neighbor)) {
            path.push(neighbor);
            dfs(neighbor, dest, visiting, path);
            path.pop();
          }
        }

        visiting.delete(cur);
      };

      dfs(vertex1, vertex2, new Set<VO>(), []);
      return minPath;
    }
  }

  /**
   * Dijkstra without heap (array-based selection).
   * @param src - Source vertex or key.
   * @param dest - Optional destination for early stop.
   * @param getMinDist - If `true`, compute global minimum distance.
   * @param genPaths - If `true`, also generate path arrays.
   * @returns Result bag or `undefined` if source missing.
   * @remarks Time O(V^2 + E), Space O(V + E)
   */
  dijkstraWithoutHeap(
    src: VO | VertexKey,
    dest: VO | VertexKey | undefined = undefined,
    getMinDist: boolean = false,
    genPaths: boolean = false
  ): DijkstraResult<VO> | undefined {
    let minDist = Number.MAX_SAFE_INTEGER;
    let minDest: VO | undefined = undefined;
    let minPath: VO[] = [];
    const paths: VO[][] = [];

    const vertexMap = this._vertexMap;
    const distMap: Map<VO, number> = new Map();
    const seen: Set<VO> = new Set();
    const preMap: Map<VO, VO | undefined> = new Map();
    const srcVertex = this._getVertex(src);

    const destVertex = dest ? this._getVertex(dest) : undefined;

    if (!srcVertex) {
      return undefined;
    }

    for (const vertex of vertexMap) {
      const vertexOrKey = vertex[1];
      if (vertexOrKey instanceof AbstractVertex) distMap.set(vertexOrKey, Number.MAX_SAFE_INTEGER);
    }
    distMap.set(srcVertex, 0);
    preMap.set(srcVertex, undefined);

    const getMinOfNoSeen = () => {
      let min = Number.MAX_SAFE_INTEGER;
      let minV: VO | undefined = undefined;
      for (const [key, value] of distMap) {
        if (!seen.has(key)) {
          if (value < min) {
            min = value;
            minV = key;
          }
        }
      }
      return minV;
    };

    const getPaths = (minV: VO | undefined) => {
      for (const vertex of vertexMap) {
        const vertexOrKey = vertex[1];

        if (vertexOrKey instanceof AbstractVertex) {
          const path: VO[] = [vertexOrKey];
          let parent = preMap.get(vertexOrKey);
          while (parent) {
            path.push(parent);
            parent = preMap.get(parent);
          }
          const reversed = path.reverse();
          if (vertex[1] === minV) minPath = reversed;
          paths.push(reversed);
        }
      }
    };

    for (let i = 1; i < vertexMap.size; i++) {
      const cur = getMinOfNoSeen();
      if (cur) {
        seen.add(cur);
        if (destVertex && destVertex === cur) {
          if (getMinDist) {
            minDist = distMap.get(destVertex) || Number.MAX_SAFE_INTEGER;
          }
          if (genPaths) {
            getPaths(destVertex);
          }
          return { distMap, preMap, seen, paths, minDist, minPath };
        }
        const neighbors = this.getNeighbors(cur);
        for (const neighbor of neighbors) {
          if (!seen.has(neighbor)) {
            const edge = this.getEdge(cur, neighbor);
            if (edge) {
              const curFromMap = distMap.get(cur);
              const neighborFromMap = distMap.get(neighbor);

              if (curFromMap !== undefined && neighborFromMap !== undefined) {
                if (edge.weight + curFromMap < neighborFromMap) {
                  distMap.set(neighbor, edge.weight + curFromMap);
                  preMap.set(neighbor, cur);
                }
              }
            }
          }
        }
      }
    }

    if (getMinDist)
      distMap.forEach((d, v) => {
        if (v !== srcVertex) {
          if (d < minDist) {
            minDist = d;
            if (genPaths) minDest = v;
          }
        }
      });

    if (genPaths) getPaths(minDest);

    return { distMap, preMap, seen, paths, minDist, minPath };
  }

  dijkstra(
    src: VO | VertexKey,
    dest: VO | VertexKey | undefined = undefined,
    getMinDist: boolean = false,
    genPaths: boolean = false
  ): DijkstraResult<VO> | undefined {
    let minDist = Number.MAX_SAFE_INTEGER;
    let minDest: VO | undefined = undefined;
    let minPath: VO[] = [];
    const paths: VO[][] = [];
    const vertexMap = this._vertexMap;
    const distMap: Map<VO, number> = new Map();
    const seen: Set<VO> = new Set();
    const preMap: Map<VO, VO | undefined> = new Map();

    const srcVertex = this._getVertex(src);
    const destVertex = dest ? this._getVertex(dest) : undefined;

    if (!srcVertex) return undefined;

    for (const vertex of vertexMap) {
      const vertexOrKey = vertex[1];
      if (vertexOrKey instanceof AbstractVertex) distMap.set(vertexOrKey, Number.MAX_SAFE_INTEGER);
    }

    const heap = new Heap<{ key: number; value: VO }>([], { comparator: (a, b) => a.key - b.key });
    heap.add({ key: 0, value: srcVertex });

    distMap.set(srcVertex, 0);
    preMap.set(srcVertex, undefined);

    const getPaths = (minV: VO | undefined) => {
      for (const vertex of vertexMap) {
        const vertexOrKey = vertex[1];
        if (vertexOrKey instanceof AbstractVertex) {
          const path: VO[] = [vertexOrKey];
          let parent = preMap.get(vertexOrKey);
          while (parent) {
            path.push(parent);
            parent = preMap.get(parent);
          }
          const reversed = path.reverse();
          if (vertex[1] === minV) minPath = reversed;
          paths.push(reversed);
        }
      }
    };

    while (heap.size > 0) {
      const curHeapNode = heap.poll();
      const dist = curHeapNode?.key;
      const cur = curHeapNode?.value;
      if (dist !== undefined) {
        if (cur) {
          seen.add(cur);
          if (destVertex && destVertex === cur) {
            if (getMinDist) {
              minDist = distMap.get(destVertex) || Number.MAX_SAFE_INTEGER;
            }
            if (genPaths) {
              getPaths(destVertex);
            }
            return { distMap, preMap, seen, paths, minDist, minPath };
          }
          const neighbors = this.getNeighbors(cur);
          for (const neighbor of neighbors) {
            if (!seen.has(neighbor)) {
              const weight = this.getEdge(cur, neighbor)?.weight;
              if (typeof weight === 'number') {
                const distSrcToNeighbor = distMap.get(neighbor);
                if (distSrcToNeighbor !== undefined) {
                  if (dist + weight < distSrcToNeighbor) {
                    heap.add({ key: dist + weight, value: neighbor });
                    preMap.set(neighbor, cur);
                    distMap.set(neighbor, dist + weight);
                  }
                }
              }
            }
          }
        }
      }
    }

    if (getMinDist) {
      distMap.forEach((d, v) => {
        if (v !== srcVertex) {
          if (d < minDist) {
            minDist = d;
            if (genPaths) minDest = v;
          }
        }
      });
    }

    if (genPaths) {
      getPaths(minDest);
    }

    return { distMap, preMap, seen, paths, minDist, minPath };
  }

  /**
   * Bellman-Ford single-source shortest paths with option to scan negative cycles.
   * @param src - Source vertex or key.
   * @param scanNegativeCycle - If `true`, also detect negative cycles.
   * @param getMin - If `true`, compute global minimum distance.
   * @param genPath - If `true`, generate path arrays via predecessor map.
   * @returns Result bag including distances, predecessors, and optional cycle flag.
   * @remarks Time O(V * E), Space O(V + E)
   */
  bellmanFord(src: VO | VertexKey, scanNegativeCycle?: boolean, getMin?: boolean, genPath?: boolean) {
    if (getMin === undefined) getMin = false;
    if (genPath === undefined) genPath = false;

    const srcVertex = this._getVertex(src);
    const paths: VO[][] = [];
    const distMap: Map<VO, number> = new Map();
    const preMap: Map<VO, VO> = new Map();
    let min = Number.MAX_SAFE_INTEGER;
    let minPath: VO[] = [];

    let hasNegativeCycle: boolean | undefined;
    if (scanNegativeCycle) hasNegativeCycle = false;
    if (!srcVertex) return { hasNegativeCycle, distMap, preMap, paths, min, minPath };

    const vertexMap = this._vertexMap;
    const numOfVertices = vertexMap.size;
    const edgeMap = this.edgeSet();
    const numOfEdges = edgeMap.length;

    this._vertexMap.forEach(vertex => {
      distMap.set(vertex, Number.MAX_SAFE_INTEGER);
    });

    distMap.set(srcVertex, 0);

    for (let i = 1; i < numOfVertices; ++i) {
      for (let j = 0; j < numOfEdges; ++j) {
        const ends = this.getEndsOfEdge(edgeMap[j]);
        if (ends) {
          const [s, d] = ends;
          const weight = edgeMap[j].weight;
          const sWeight = distMap.get(s);
          const dWeight = distMap.get(d);
          if (sWeight !== undefined && dWeight !== undefined) {
            if (distMap.get(s) !== Number.MAX_SAFE_INTEGER && sWeight + weight < dWeight) {
              distMap.set(d, sWeight + weight);
              if (genPath) preMap.set(d, s);
            }
          }
        }
      }
    }

    let minDest: VO | undefined = undefined;
    if (getMin) {
      distMap.forEach((d, v) => {
        if (v !== srcVertex) {
          if (d < min) {
            min = d;
            if (genPath) minDest = v;
          }
        }
      });
    }

    if (genPath) {
      for (const vertex of vertexMap) {
        const vertexOrKey = vertex[1];
        if (vertexOrKey instanceof AbstractVertex) {
          const path: VO[] = [vertexOrKey];
          let parent = preMap.get(vertexOrKey);
          while (parent !== undefined) {
            path.push(parent);
            parent = preMap.get(parent);
          }
          const reversed = path.reverse();
          if (vertex[1] === minDest) minPath = reversed;
          paths.push(reversed);
        }
      }
    }

    for (let j = 0; j < numOfEdges; ++j) {
      const ends = this.getEndsOfEdge(edgeMap[j]);
      if (ends) {
        const [s] = ends;
        const weight = edgeMap[j].weight;
        const sWeight = distMap.get(s);
        if (sWeight) {
          if (sWeight !== Number.MAX_SAFE_INTEGER && sWeight + weight < sWeight) hasNegativeCycle = true;
        }
      }
    }

    return { hasNegativeCycle, distMap, preMap, paths, min, minPath };
  }

  /**
   * Floyd–Warshall all-pairs shortest paths.
   * @returns `{ costs, predecessor }` matrices.
   * @remarks Time O(V^3), Space O(V^2)
   */
  floydWarshall(): { costs: number[][]; predecessor: (VO | undefined)[][] } {
    const idAndVertices = [...this._vertexMap];
    const n = idAndVertices.length;

    const costs: number[][] = [];
    const predecessor: (VO | undefined)[][] = [];

    for (let i = 0; i < n; i++) {
      costs[i] = [];
      predecessor[i] = [];
      for (let j = 0; j < n; j++) {
        predecessor[i][j] = undefined;
      }
    }

    for (let i = 0; i < n; i++) {
      for (let j = 0; j < n; j++) {
        costs[i][j] = this.getEdge(idAndVertices[i][1], idAndVertices[j][1])?.weight || Number.MAX_SAFE_INTEGER;
      }
    }

    for (let k = 0; k < n; k++) {
      for (let i = 0; i < n; i++) {
        for (let j = 0; j < n; j++) {
          if (costs[i][j] > costs[i][k] + costs[k][j]) {
            costs[i][j] = costs[i][k] + costs[k][j];
            predecessor[i][j] = idAndVertices[k][1];
          }
        }
      }
    }
    return { costs, predecessor };
  }

  /**
   * Enumerate simple cycles (may be expensive).
   * @param isInclude2Cycle - If `true`, include 2-cycles when graph semantics allow.
   * @returns Array of cycles (each as array of vertex keys).
   * @remarks Time exponential in worst-case, Space O(V + E)
   */
  getCycles(isInclude2Cycle: boolean = false): VertexKey[][] {
    const cycles: VertexKey[][] = [];
    const visited: Set<VO> = new Set();

    const dfs = (vertex: VO, currentPath: VertexKey[], visited: Set<VO>) => {
      if (visited.has(vertex)) {
        if (
          ((!isInclude2Cycle && currentPath.length > 2) || (isInclude2Cycle && currentPath.length >= 2)) &&
          currentPath[0] === vertex.key
        ) {
          cycles.push([...currentPath]);
        }
        return;
      }

      visited.add(vertex);
      currentPath.push(vertex.key);

      for (const neighbor of this.getNeighbors(vertex)) {
        if (neighbor) dfs(neighbor, currentPath, visited);
      }

      visited.delete(vertex);
      currentPath.pop();
    };

    for (const vertex of this.vertexMap.values()) {
      dfs(vertex, [], visited);
    }

    const uniqueCycles = new Map<string, VertexKey[]>();

    for (const cycle of cycles) {
      const sorted = [...cycle].sort().toString();

      if (uniqueCycles.has(sorted)) continue;
      else {
        uniqueCycles.set(sorted, cycle);
      }
    }

    /**
     * Map entries to an array via callback.
     * @param callback - `(key, value, index, self) => T`.
     * @param thisArg - Optional `this` for callback.
     * @returns Mapped results.
     * @remarks Time O(V), Space O(V)
     */
    return [...uniqueCycles].map(cycleString => cycleString[1]);
  }

  /**
   * Induced-subgraph filter: keep vertices where `predicate(key, value)` is true,
   * and only keep edges whose endpoints both survive.
   * @param predicate - `(key, value, index, self) => boolean`.
   * @param thisArg - Optional `this` for callback.
   * @returns A new graph of the same concrete class (`this` type).
   * @remarks Time O(V + E), Space O(V + E)
   */
  filter(predicate: EntryCallback<VertexKey, V | undefined, boolean>, thisArg?: unknown): this {
    const filtered: [VertexKey, V | undefined][] = [];
    let index = 0;
    for (const [key, value] of this) {
      if (predicate.call(thisArg, value, key, index, this)) {
        filtered.push([key, value]);
      }
      index++;
    }
    return this._createLike(filtered, this._snapshotOptions());
  }

  /**
   * Preserve the old behavior: return filtered entries as an array.
   * @remarks Time O(V), Space O(V)
   */
  filterEntries(
    predicate: EntryCallback<VertexKey, V | undefined, boolean>,
    thisArg?: unknown
  ): [VertexKey, V | undefined][] {
    const filtered: [VertexKey, V | undefined][] = [];
    let index = 0;
    for (const [key, value] of this) {
      if (predicate.call(thisArg, value, key, index, this)) {
        filtered.push([key, value]);
      }
      index++;
    }
    return filtered;
  }

  map<T>(callback: EntryCallback<VertexKey, V | undefined, T>, thisArg?: unknown): T[] {
    const mapped: T[] = [];
    let index = 0;
    for (const [key, value] of this) {
      mapped.push(callback.call(thisArg, value, key, index, this));
      index++;
    }
    return mapped;
  }

  /**
   * Create a deep clone of the graph with the same species.
   * @remarks Time O(V + E), Space O(V + E)
   */
  /**
   * Create a deep clone of the graph with the same species.
   * @returns A new graph of the same concrete class (`this` type).
   * @remarks Time O(V + E), Space O(V + E)
   */
  clone(): this {
    return this._createLike(undefined, this._snapshotOptions());
  }

  // ===== Same-species factory & cloning helpers =====

  /**
   * Internal iterator over `[key, value]` entries in insertion order.
   * @returns Iterator of `[VertexKey, V | undefined]`.
   * @remarks Time O(V), Space O(1)
   */
  protected *_getIterator(): IterableIterator<[VertexKey, V | undefined]> {
    for (const vertex of this._vertexMap.values()) {
      yield [vertex.key, vertex.value];
    }
  }

  /**
   * Capture configuration needed to reproduce the current graph.
   * Currently the graph has no runtime options, so we return an empty object.
   */
  /**
   * Capture configuration needed to reproduce the current graph.
   * @returns Options bag (opaque to callers).
   * @remarks Time O(1), Space O(1)
   */
  protected _snapshotOptions(): Record<string, unknown> {
    return { graph: { ...this._options } };
  }

  /**
   * Create an empty graph instance of the same concrete species (Directed/Undirected/etc).
   * @remarks Time O(1), Space O(1)
   */
  /**
   * Create an empty graph instance of the same concrete species.
   * @param _options - Snapshot options from `_snapshotOptions()`.
   * @returns A new empty graph instance of `this` type.
   * @remarks Time O(1), Space O(1)
   */
  protected _createInstance(_options?: Partial<Record<string, unknown>>): this {
    const Ctor = this.constructor as new () => this;
    const instance = new Ctor();
    const graph = (_options as { graph?: GraphOptions<V> })?.graph;
    // Use bracket notation for protected field access on dynamically created instance
    if (graph) instance['_options'] = { ...instance['_options'], ...graph };
    else instance['_options'] = { ...instance['_options'], ...this._options };
    return instance;
  }

  /**
   * Create a same-species graph populated with the given entries.
   * Also preserves edges between kept vertices from the source graph.
   * @remarks Time O(V + E), Space O(V + E)
   */
  /**
   * Create a same-species graph populated with entries; preserves edges among kept vertices.
   * @param iter - Optional entries to seed the new graph.
   * @param options - Snapshot options.
   * @returns A new graph of `this` type.
   * @remarks Time O(V + E), Space O(V + E)
   */
  protected _createLike(iter?: Iterable<[VertexKey, V | undefined]>, options?: Partial<Record<string, unknown>>): this {
    const g = this._createInstance(options);
    // 1) Add vertices
    if (iter) {
      for (const [k, v] of iter) {
        g.addVertex(k as VertexKey, v as V | undefined);
      }
    } else {
      for (const [k, v] of this) {
        g.addVertex(k as VertexKey, v as V | undefined);
      }
    }
    // 2) Add edges whose endpoints exist in the new graph
    const edges = this.edgeSet();
    for (const e of edges) {
      const ends = this.getEndsOfEdge(e);
      if (!ends) continue;
      const [va, vb] = ends;
      const ka = va.key;
      const kb = vb.key;
      // Defensive check for edge cases where hasVertex may be overridden/undefined
      const hasA = typeof g.hasVertex === 'function' ? g.hasVertex(ka) : false;
      const hasB = typeof g.hasVertex === 'function' ? g.hasVertex(kb) : false;
      if (hasA && hasB) {
        const newEdge = g.createEdge(ka, kb, e.weight, e.value);
        (g as this & { _addEdge(edge: EO): boolean })._addEdge(newEdge);
      }
    }
    return g;
  }

  /**
   * Internal hook to attach an edge into adjacency structures.
   * @param edge - Edge instance.
   * @returns `true` if inserted; otherwise `false`.
   * @remarks Time O(1) avg, Space O(1)
   */
  protected abstract _addEdge(edge: EO): boolean;

  /**
   * Insert a pre-built vertex into the graph.
   * @param newVertex - Concrete vertex instance.
   * @returns `true` if inserted; `false` if key already exists.
   * @remarks Time O(1) avg, Space O(1)
   */
  protected _addVertex(newVertex: VO): boolean {
    if (this.hasVertex(newVertex)) {
      return false;
    }
    this._vertexMap.set(newVertex.key, newVertex);
    return true;
  }

  /**
   * Resolve a vertex key or instance to the concrete vertex instance.
   * @param vertexOrKey - Vertex key or existing vertex.
   * @returns Vertex instance or `undefined`.
   * @remarks Time O(1), Space O(1)
   */
  protected _getVertex(vertexOrKey: VertexKey | VO): VO | undefined {
    const vertexKey = this._getVertexKey(vertexOrKey);
    return this._vertexMap.get(vertexKey) || undefined;
  }

  /**
   * Resolve a vertex key from a key or vertex instance.
   * @param vertexOrKey - Vertex key or existing vertex.
   * @returns The vertex key.
   * @remarks Time O(1), Space O(1)
   */
  protected _getVertexKey(vertexOrKey: VO | VertexKey): VertexKey {
    return vertexOrKey instanceof AbstractVertex ? vertexOrKey.key : vertexOrKey;
  }

  /**
   * The edge connector string used in visual output.
   * Override in subclasses (e.g., '--' for undirected, '->' for directed).
   */
  protected get _edgeConnector(): string {
    return '--';
  }

  /**
   * Generate a text-based visual representation of the graph.
   *
   * **Adjacency list format:**
   * ```
   * Graph (5 vertices, 6 edges):
   * A -> B (1), C (2)
   * B -> D (3)
   * C -> (no outgoing edges)
   * D -> A (1)
   * E (isolated)
   * ```
   *
   * @param options - Optional display settings.
   * @param options.showWeight - Whether to show edge weights (default: true).
   * @returns The visual string.
   */
  override toVisual(options?: { showWeight?: boolean }): string {
    const showWeight = options?.showWeight ?? true;
    const vertices = [...this._vertexMap.values()];
    const vertexCount = vertices.length;
    const edgeCount = this.edgeSet().length;

    const lines: string[] = [`Graph (${vertexCount} vertices, ${edgeCount} edges):`];

    for (const vertex of vertices) {
      const neighbors = this.getNeighbors(vertex);
      if (neighbors.length === 0) {
        lines.push(`  ${vertex.key} (isolated)`);
      } else {
        const edgeStrs = neighbors.map(neighbor => {
          const edge = this.getEdge(vertex, neighbor);
          if (edge && showWeight && edge.weight !== undefined && edge.weight !== 1) {
            return `${neighbor.key} (${edge.weight})`;
          }
          return `${neighbor.key}`;
        });
        lines.push(`  ${vertex.key} ${this._edgeConnector} ${edgeStrs.join(', ')}`);
      }
    }

    return lines.join('\n');
  }

  /**
   * Generate DOT language representation for Graphviz.
   *
   * @param options - Optional display settings.
   * @param options.name - Graph name (default: 'G').
   * @param options.showWeight - Whether to label edges with weight (default: true).
   * @returns DOT format string.
   */
  toDot(options?: { name?: string; showWeight?: boolean }): string {
    const name = options?.name ?? 'G';
    const showWeight = options?.showWeight ?? true;
    const isDirected = this._edgeConnector === '->';
    const graphType = isDirected ? 'digraph' : 'graph';
    const edgeOp = isDirected ? '->' : '--';

    const lines: string[] = [`${graphType} ${name} {`];

    // Add all vertices (ensures isolated vertices appear)
    for (const vertex of this._vertexMap.values()) {
      lines.push(`  "${vertex.key}";`);
    }

    // Add edges
    const visited = new Set<string>();
    for (const vertex of this._vertexMap.values()) {
      for (const neighbor of this.getNeighbors(vertex)) {
        const edgeId = isDirected
          ? `${vertex.key}->${neighbor.key}`
          : [vertex.key, neighbor.key].sort().join('--');
        if (visited.has(edgeId)) continue;
        visited.add(edgeId);

        const edge = this.getEdge(vertex, neighbor);
        const label = edge && showWeight && edge.weight !== undefined && edge.weight !== 1
          ? ` [label="${edge.weight}"]`
          : '';
        lines.push(`  "${vertex.key}" ${edgeOp} "${neighbor.key}"${label};`);
      }
    }

    lines.push('}');
    return lines.join('\n');
  }

  /**
   * Print the graph to console.
   * @param options - Display settings passed to `toVisual`.
   */
  override print(options?: { showWeight?: boolean }): void {
    console.log(this.toVisual(options));
  }
}
