/**
 * data-structure-typed
 *
 * @author Pablo Zeng
 * @copyright Copyright (c) 2022 Pablo Zeng <zrwusa@gmail.com>
 * @license MIT License
 */
import type { TopologicalStatus, VertexKey } from '../../types';
import { AbstractEdge, AbstractGraph, AbstractVertex } from './abstract-graph';
import { IGraph } from '../../interfaces';
import { arrayRemove } from '../../utils';

export class DirectedVertex<V = any> extends AbstractVertex<V> {
  /**
   * The constructor function initializes a vertex with an optional value.
   * @param {VertexKey} key - The `key` parameter is of type `VertexKey` and represents the identifier of the vertex. It is
   * used to uniquely identify the vertex within a graph or data structure.
   * @param {V} [value] - The "value" parameter is an optional parameter of type V. It is used to initialize the value of the
   * vertex. If no value is provided, the vertex will be initialized with a default value.
   */
  constructor(key: VertexKey, value?: V) {
    super(key, value);
  }
}

export class DirectedEdge<E = any> extends AbstractEdge<E> {
  src: VertexKey;
  dest: VertexKey;

  /**
   * The constructor function initializes the source and destination vertexMap of an edge, along with an optional weight
   * and value.
   * @param {VertexKey} src - The `src` parameter is the source vertex ID. It represents the starting point of an edge in
   * a graph.
   * @param {VertexKey} dest - The `dest` parameter represents the destination vertex of an edge. It is of type
   * `VertexKey`, which is likely a unique identifier for a vertex in a graph.
   * @param {number} [weight] - The weight parameter is an optional number that represents the weight of the edge.
   * @param {E} [value] - The `value` parameter is an optional parameter of type `E`. It represents the value associated with
   * the edge.
   */
  constructor(src: VertexKey, dest: VertexKey, weight?: number, value?: E) {
    super(weight, value);
    this.src = src;
    this.dest = dest;
  }
}

/**
 *
 */
export class DirectedGraph<
    V = any,
    E = any,
    VO extends DirectedVertex<V> = DirectedVertex<V>,
    EO extends DirectedEdge<E> = DirectedEdge<E>
  >
  extends AbstractGraph<V, E, VO, EO>
  implements IGraph<V, E, VO, EO>
{
  /**
   * The constructor function initializes an instance of a class.
   */
  constructor() {
    super();
  }

  protected _outEdgeMap: Map<VO, EO[]> = new Map<VO, EO[]>();

  get outEdgeMap(): Map<VO, EO[]> {
    return this._outEdgeMap;
  }

  set outEdgeMap(v: Map<VO, EO[]>) {
    this._outEdgeMap = v;
  }

  protected _inEdgeMap: Map<VO, EO[]> = new Map<VO, EO[]>();

  get inEdgeMap(): Map<VO, EO[]> {
    return this._inEdgeMap;
  }

  set inEdgeMap(v: Map<VO, EO[]>) {
    this._inEdgeMap = v;
  }

  /**
   * The function creates a new vertex with an optional value and returns it.
   * @param {VertexKey} key - The `key` parameter is the unique identifier for the vertex. It is of type `VertexKey`, which
   * could be a number or a string depending on how you want to identify your vertexMap.
   * @param [value] - The 'value' parameter is an optional value that can be assigned to the vertex. If a value is provided,
   * it will be assigned to the 'value' property of the vertex. If no value is provided, the 'value' property will be
   * assigned the same value as the 'key' parameter
   * @returns a new instance of a DirectedVertex object, casted as type VO.
   */
  createVertex(key: VertexKey, value?: V): VO {
    return new DirectedVertex(key, value) as VO;
  }

  /**
   * The function creates a directed edge between two vertexMap with an optional weight and value.
   * @param {VertexKey} src - The source vertex ID of the edge. It represents the starting point of the edge.
   * @param {VertexKey} dest - The `dest` parameter is the identifier of the destination vertex for the edge.
   * @param {number} [weight] - The weight parameter is an optional number that represents the weight of the edge. If no
   * weight is provided, it defaults to 1.
   * @param [value] - The 'value' parameter is an optional value that can be assigned to the edge. It can be of any type and
   * is used to store additional information or data associated with the edge.
   * @returns a new instance of a DirectedEdge object, casted as type EO.
   */
  createEdge(src: VertexKey, dest: VertexKey, weight?: number, value?: E): EO {
    return new DirectedEdge(src, dest, weight ?? 1, value) as EO;
  }

  /**
   * Time Complexity: O(|V|) where |V| is the number of vertexMap
   * Space Complexity: O(1)
   *
   * The `getEdge` function retrieves an edge between two vertexMap based on their source and destination IDs.
   * @param {VO | VertexKey | undefined} srcOrKey - The source vertex or its ID. It can be either a vertex object or a vertex ID.
   * @param {VO | VertexKey | undefined} destOrKey - The `destOrKey` parameter in the `getEdge` function represents the
   * destination vertex of the edge. It can be either a vertex object (`VO`), a vertex ID (`VertexKey`), or `undefined` if the
   * destination is not specified.
   * @returns the first edge found between the source and destination vertexMap, or undefined if no such edge is found.
   */
  getEdge(srcOrKey: VO | VertexKey | undefined, destOrKey: VO | VertexKey | undefined): EO | undefined {
    let edgeMap: EO[] = [];

    if (srcOrKey !== undefined && destOrKey !== undefined) {
      const src: VO | undefined = this._getVertex(srcOrKey);
      const dest: VO | undefined = this._getVertex(destOrKey);

      if (src && dest) {
        const srcOutEdges = this._outEdgeMap.get(src);
        if (srcOutEdges) {
          edgeMap = srcOutEdges.filter(edge => edge.dest === dest.key);
        }
      }
    }

    return edgeMap[0] || undefined;
  }

  /**
   * Time Complexity: O(|E|) where |E| is the number of edgeMap
   * Space Complexity: O(1)
   *
   * The function removes an edge between two vertexMap in a graph and returns the removed edge.
   * @param {VO | VertexKey} srcOrKey - The source vertex or its ID.
   * @param {VO | VertexKey} destOrKey - The `destOrKey` parameter represents the destination vertex or its ID.
   * @returns the removed edge (EO) if it exists, or undefined if either the source or destination vertex does not exist.
   */
  deleteEdgeSrcToDest(srcOrKey: VO | VertexKey, destOrKey: VO | VertexKey): EO | undefined {
    const src: VO | undefined = this._getVertex(srcOrKey);
    const dest: VO | undefined = this._getVertex(destOrKey);
    let removed: EO | undefined = undefined;
    if (!src || !dest) {
      return undefined;
    }

    const srcOutEdges = this._outEdgeMap.get(src);
    if (srcOutEdges) {
      arrayRemove<EO>(srcOutEdges, (edge: EO) => edge.dest === dest.key);
    }

    const destInEdges = this._inEdgeMap.get(dest);
    if (destInEdges) {
      removed = arrayRemove<EO>(destInEdges, (edge: EO) => edge.src === src.key)[0] || undefined;
    }
    return removed;
  }

  /**
   * Time Complexity: O(E) where E is the number of edgeMap
   * Space Complexity: O(1)
   *
   * The `deleteEdge` function removes an edge from a graph and returns the removed edge.
   * @param {EO | VertexKey} edgeOrSrcVertexKey - The `edge` parameter can be either an `EO` object (edge object) or
   * a `VertexKey` (key of a vertex).
   * @param {VertexKey} [destVertexKey] - The `destVertexKey` parameter is an optional parameter that
   * represents the key of the destination vertex of the edge. It is used to specify the destination
   * vertex when the `edge` parameter is a vertex key. If `destVertexKey` is not provided, the function
   * assumes that the `edge`
   * @returns the removed edge (EO) or undefined if no edge was removed.
   */
  deleteEdge(edgeOrSrcVertexKey: EO | VertexKey, destVertexKey?: VertexKey): EO | undefined {
    let removed: EO | undefined = undefined;
    let src: VO | undefined, dest: VO | undefined;
    if (this.isVertexKey(edgeOrSrcVertexKey)) {
      if (this.isVertexKey(destVertexKey)) {
        src = this._getVertex(edgeOrSrcVertexKey);
        dest = this._getVertex(destVertexKey);
      } else {
        return;
      }
    } else {
      src = this._getVertex(edgeOrSrcVertexKey.src);
      dest = this._getVertex(edgeOrSrcVertexKey.dest);
    }

    if (src && dest) {
      const srcOutEdges = this._outEdgeMap.get(src);
      if (srcOutEdges && srcOutEdges.length > 0) {
        arrayRemove(srcOutEdges, (edge: EO) => edge.src === src!.key && edge.dest === dest?.key);
      }

      const destInEdges = this._inEdgeMap.get(dest);
      if (destInEdges && destInEdges.length > 0) {
        removed = arrayRemove(destInEdges, (edge: EO) => edge.src === src!.key && edge.dest === dest!.key)[0];
      }
    }

    return removed;
  }

  /**
   * Time Complexity: O(1) - Constant time for Map operations.
   * Space Complexity: O(1) - Constant space, as it creates only a few variables.
   *
   * The `deleteVertex` function removes a vertex from a graph by its ID or by the vertex object itself.
   * @param {VO | VertexKey} vertexOrKey - The parameter `vertexOrKey` can be either a vertex object (`VO`) or a vertex ID
   * (`VertexKey`).
   * @returns The method is returning a boolean value.
   */
  deleteVertex(vertexOrKey: VO | VertexKey): boolean {
    let vertexKey: VertexKey;
    let vertex: VO | undefined;
    if (this.isVertexKey(vertexOrKey)) {
      vertex = this.getVertex(vertexOrKey);
      vertexKey = vertexOrKey;
    } else {
      vertex = vertexOrKey;
      vertexKey = this._getVertexKey(vertexOrKey);
    }

    if (vertex) {
      const neighbors = this.getNeighbors(vertex);
      for (const neighbor of neighbors) {
        // this._inEdgeMap.delete(neighbor);
        this.deleteEdgeSrcToDest(vertex, neighbor);
      }
      this._outEdgeMap.delete(vertex);
      this._inEdgeMap.delete(vertex);
    }

    return this._vertexMap.delete(vertexKey);
  }

  /**
   * Time Complexity: O(|E|) where |E| is the number of edgeMap
   * Space Complexity: O(1)
   *
   * The function removes edgeMap between two vertexMap and returns the removed edgeMap.
   * @param {VertexKey | VO} v1 - The parameter `v1` can be either a `VertexKey` or a `VO`. A `VertexKey` represents the
   * unique identifier of a vertex in a graph, while `VO` represents the actual vertex object.
   * @param {VertexKey | VO} v2 - The parameter `v2` represents either a `VertexKey` or a `VO` object. It is used to specify
   * the second vertex in the edge that needs to be removed.
   * @returns an array of removed edgeMap (EO[]).
   */
  deleteEdgesBetween(v1: VertexKey | VO, v2: VertexKey | VO): EO[] {
    const removed: EO[] = [];

    if (v1 && v2) {
      const v1ToV2 = this.deleteEdgeSrcToDest(v1, v2);
      const v2ToV1 = this.deleteEdgeSrcToDest(v2, v1);

      if (v1ToV2) removed.push(v1ToV2);
      if (v2ToV1) removed.push(v2ToV1);
    }

    return removed;
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function `incomingEdgesOf` returns an array of incoming edgeMap for a given vertex or vertex ID.
   * @param {VO | VertexKey} vertexOrKey - The parameter `vertexOrKey` can be either a vertex object (`VO`) or a vertex ID
   * (`VertexKey`).
   * @returns The method `incomingEdgesOf` returns an array of edgeMap (`EO[]`).
   */
  incomingEdgesOf(vertexOrKey: VO | VertexKey): EO[] {
    const target = this._getVertex(vertexOrKey);
    if (target) {
      return this.inEdgeMap.get(target) || [];
    }
    return [];
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function `outgoingEdgesOf` returns an array of outgoing edgeMap from a given vertex or vertex ID.
   * @param {VO | VertexKey} vertexOrKey - The parameter `vertexOrKey` can accept either a vertex object (`VO`) or a vertex ID
   * (`VertexKey`).
   * @returns The method `outgoingEdgesOf` returns an array of edgeMap (`EO[]`).
   */
  outgoingEdgesOf(vertexOrKey: VO | VertexKey): EO[] {
    const target = this._getVertex(vertexOrKey);
    if (target) {
      return this._outEdgeMap.get(target) || [];
    }
    return [];
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function "degreeOf" returns the total degree of a vertex, which is the sum of its out-degree and in-degree.
   * @param {VertexKey | VO} vertexOrKey - The parameter `vertexOrKey` can be either a `VertexKey` or a `VO`.
   * @returns The sum of the out-degree and in-degree of the specified vertex or vertex ID.
   */
  degreeOf(vertexOrKey: VertexKey | VO): number {
    return this.outDegreeOf(vertexOrKey) + this.inDegreeOf(vertexOrKey);
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function "inDegreeOf" returns the number of incoming edgeMap for a given vertex.
   * @param {VertexKey | VO} vertexOrKey - The parameter `vertexOrKey` can be either a `VertexKey` or a `VO`.
   * @returns The number of incoming edgeMap of the specified vertex or vertex ID.
   */
  inDegreeOf(vertexOrKey: VertexKey | VO): number {
    return this.incomingEdgesOf(vertexOrKey).length;
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function `outDegreeOf` returns the number of outgoing edgeMap from a given vertex.
   * @param {VertexKey | VO} vertexOrKey - The parameter `vertexOrKey` can be either a `VertexKey` or a `VO`.
   * @returns The number of outgoing edgeMap from the specified vertex or vertex ID.
   */
  outDegreeOf(vertexOrKey: VertexKey | VO): number {
    return this.outgoingEdgesOf(vertexOrKey).length;
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function "edgesOf" returns an array of both outgoing and incoming edgeMap of a given vertex or vertex ID.
   * @param {VertexKey | VO} vertexOrKey - The parameter `vertexOrKey` can be either a `VertexKey` or a `VO`.
   * @returns The function `edgesOf` returns an array of edgeMap.
   */
  edgesOf(vertexOrKey: VertexKey | VO): EO[] {
    return [...this.outgoingEdgesOf(vertexOrKey), ...this.incomingEdgesOf(vertexOrKey)];
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function "getEdgeSrc" returns the source vertex of an edge, or undefined if the edge does not exist.
   * @param {EO} e - The parameter "e" is of type EO, which represents an edge in a graph.
   * @returns either a vertex object (VO) or undefined.
   */
  getEdgeSrc(e: EO): VO | undefined {
    return this._getVertex(e.src);
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function "getEdgeDest" returns the destination vertex of an edge.
   * @param {EO} e - The parameter "e" is of type "EO", which represents an edge in a graph.
   * @returns either a vertex object of type VO or undefined.
   */
  getEdgeDest(e: EO): VO | undefined {
    return this._getVertex(e.dest);
  }

  /**
   * Time Complexity: O(|E|) where |E| is the number of edgeMap
   * Space Complexity: O(1)
   *
   * The function `getDestinations` returns an array of destination vertexMap connected to a given vertex.
   * @param {VO | VertexKey | undefined} vertex - The `vertex` parameter represents the starting vertex from which we want to
   * find the destinations. It can be either a `VO` object, a `VertexKey` value, or `undefined`.
   * @returns an array of vertexMap (VO[]).
   */
  getDestinations(vertex: VO | VertexKey | undefined): VO[] {
    if (vertex === undefined) {
      return [];
    }
    const destinations: VO[] = [];
    const outgoingEdges = this.outgoingEdgesOf(vertex);
    for (const outEdge of outgoingEdges) {
      const child = this.getEdgeDest(outEdge);
      if (child) {
        destinations.push(child);
      }
    }
    return destinations;
  }

  /**
   * Time Complexity: O(|V| + |E|) where |V| is the number of vertexMap and |E| is the number of edgeMap
   * Space Complexity: O(|V|)
   *
   * The `topologicalSort` function performs a topological sort on a graph and returns an array of vertexMap or vertex IDs
   * in the sorted order, or undefined if the graph contains a cycle.
   * @param {'vertex' | 'key'} [propertyName] - The `propertyName` parameter is an optional parameter that specifies the
   * property to use for sorting the vertexMap. It can have two possible values: 'vertex' or 'key'. If 'vertex' is
   * specified, the vertexMap themselves will be used for sorting. If 'key' is specified, the ids of
   * @returns an array of vertexMap or vertex IDs in topological order. If there is a cycle in the graph, it returns undefined.
   */
  topologicalSort(propertyName?: 'vertex' | 'key'): Array<VO | VertexKey> | undefined {
    propertyName = propertyName ?? 'key';
    // When judging whether there is a cycle in the undirected graph, all nodes with degree of **<= 1** are enqueued
    // When judging whether there is a cycle in the directed graph, all nodes with **in degree = 0** are enqueued
    const statusMap: Map<VO | VertexKey, TopologicalStatus> = new Map<VO | VertexKey, TopologicalStatus>();
    for (const entry of this.vertexMap) {
      statusMap.set(entry[1], 0);
    }

    let sorted: (VO | VertexKey)[] = [];
    let hasCycle = false;
    const dfs = (cur: VO | VertexKey) => {
      statusMap.set(cur, 1);
      const children = this.getDestinations(cur);
      for (const child of children) {
        const childStatus = statusMap.get(child);
        if (childStatus === 0) {
          dfs(child);
        } else if (childStatus === 1) {
          hasCycle = true;
        }
      }
      statusMap.set(cur, 2);
      sorted.push(cur);
    };

    for (const entry of this.vertexMap) {
      if (statusMap.get(entry[1]) === 0) {
        dfs(entry[1]);
      }
    }

    if (hasCycle) return undefined;

    if (propertyName === 'key') sorted = sorted.map(vertex => (vertex instanceof DirectedVertex ? vertex.key : vertex));
    return sorted.reverse();
  }

  /**
   * Time Complexity: O(|E|) where |E| is the number of edgeMap
   * Space Complexity: O(|E|)
   *
   * The `edgeSet` function returns an array of all the edgeMap in the graph.
   * @returns The `edgeSet()` method returns an array of edgeMap (`EO[]`).
   */
  edgeSet(): EO[] {
    let edgeMap: EO[] = [];
    this._outEdgeMap.forEach(outEdges => {
      edgeMap = [...edgeMap, ...outEdges];
    });
    return edgeMap;
  }

  /**
   * Time Complexity: O(|E|) where |E| is the number of edgeMap
   * Space Complexity: O(1)
   *
   * The function `getNeighbors` returns an array of neighboring vertexMap of a given vertex or vertex ID in a graph.
   * @param {VO | VertexKey} vertexOrKey - The parameter `vertexOrKey` can be either a vertex object (`VO`) or a vertex ID
   * (`VertexKey`).
   * @returns an array of vertexMap (VO[]).
   */
  getNeighbors(vertexOrKey: VO | VertexKey): VO[] {
    const neighbors: VO[] = [];
    const vertex = this._getVertex(vertexOrKey);
    if (vertex) {
      const outEdges = this.outgoingEdgesOf(vertex);
      for (const outEdge of outEdges) {
        const neighbor = this._getVertex(outEdge.dest);
        // TODO after no-non-undefined-assertion not ensure the logic
        if (neighbor) {
          neighbors.push(neighbor);
        }
      }
    }
    return neighbors;
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function "getEndsOfEdge" returns the source and destination vertexMap of an edge if it exists in the graph,
   * otherwise it returns undefined.
   * @param {EO} edge - The parameter `edge` is of type `EO`, which represents an edge in a graph.
   * @returns The function `getEndsOfEdge` returns an array containing two vertexMap `[VO, VO]` if the edge exists in the
   * graph. If the edge does not exist, it returns `undefined`.
   */
  getEndsOfEdge(edge: EO): [VO, VO] | undefined {
    if (!this.hasEdge(edge.src, edge.dest)) {
      return undefined;
    }
    const v1 = this._getVertex(edge.src);
    const v2 = this._getVertex(edge.dest);
    if (v1 && v2) {
      return [v1, v2];
    } else {
      return undefined;
    }
  }

  /**
   * The isEmpty function checks if the graph is empty.
   *
   * @return A boolean value
   */
  isEmpty(): boolean {
    return this.vertexMap.size === 0 && this.inEdgeMap.size === 0 && this.outEdgeMap.size === 0;
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The clear function resets the vertex map, in-edge map, and out-edge map.
   */
  clear() {
    this._vertexMap = new Map<VertexKey, VO>();
    this._inEdgeMap = new Map<VO, EO[]>();
    this._outEdgeMap = new Map<VO, EO[]>();
  }

  /**
   * The clone function creates a new DirectedGraph object with the same vertices and edges as the original.
   *
   * @return A new instance of the directedgraph class
   */
  clone(): DirectedGraph<V, E, VO, EO> {
    const cloned = new DirectedGraph<V, E, VO, EO>();
    cloned.vertexMap = new Map<VertexKey, VO>(this.vertexMap);
    cloned.inEdgeMap = new Map<VO, EO[]>(this.inEdgeMap);
    cloned.outEdgeMap = new Map<VO, EO[]>(this.outEdgeMap);
    return cloned;
  }

  /**
   *  Time Complexity: O(V + E)
   *  Space Complexity: O(V)
   *  Tarjan is an algorithm based on dfs,which is used to solve the connectivity problem of graphs.
   *  Tarjan can find the SSC(strongly connected components), articulation points, and bridges of directed graphs.
   *
   * The function `tarjan` implements the Tarjan's algorithm to find strongly connected components in a
   * graph.
   * @returns The function `tarjan()` returns an object with three properties: `dfnMap`, `lowMap`, and
   * `SCCs`.
   */
  tarjan(): { dfnMap: Map<VO, number>; lowMap: Map<VO, number>; SCCs: Map<number, VO[]> } {
    const dfnMap = new Map<VO, number>();
    const lowMap = new Map<VO, number>();
    const SCCs = new Map<number, VO[]>();

    let time = 0;

    const stack: VO[] = [];
    const inStack: Set<VO> = new Set();

    const dfs = (vertex: VO) => {
      dfnMap.set(vertex, time);
      lowMap.set(vertex, time);
      time++;

      stack.push(vertex);
      inStack.add(vertex);

      const neighbors = this.getNeighbors(vertex);
      for (const neighbor of neighbors) {
        if (!dfnMap.has(neighbor)) {
          dfs(neighbor);
          lowMap.set(vertex, Math.min(lowMap.get(vertex)!, lowMap.get(neighbor)!));
        } else if (inStack.has(neighbor)) {
          lowMap.set(vertex, Math.min(lowMap.get(vertex)!, dfnMap.get(neighbor)!));
        }
      }

      if (dfnMap.get(vertex) === lowMap.get(vertex)) {
        const SCC: VO[] = [];
        let poppedVertex: VO | undefined;

        do {
          poppedVertex = stack.pop();
          inStack.delete(poppedVertex!);
          SCC.push(poppedVertex!);
        } while (poppedVertex !== vertex);

        SCCs.set(SCCs.size, SCC);
      }
    };

    for (const vertex of this.vertexMap.values()) {
      if (!dfnMap.has(vertex)) {
        dfs(vertex);
      }
    }

    return { dfnMap, lowMap, SCCs };
  }

  /**
   * Time Complexity: O(V + E) - Depends on the implementation (Tarjan's algorithm).
   * Space Complexity: O(V) - Depends on the implementation (Tarjan's algorithm).
   *
   * The function returns a map that associates each vertex object with its corresponding depth-first
   * number.
   * @returns A Map object with keys of type VO and values of type number.
   */
  getDFNMap(): Map<VO, number> {
    return this.tarjan().dfnMap;
  }

  /**
   * The function returns a Map object that contains the low values of each vertex in a Tarjan
   * algorithm.
   * @returns The method `getLowMap()` is returning a `Map` object with keys of type `VO` and values of
   * type `number`.
   */
  getLowMap(): Map<VO, number> {
    return this.tarjan().lowMap;
  }

  /**
   * The function "getSCCs" returns a map of strongly connected components (SCCs) using the Tarjan
   * algorithm.
   * @returns a map where the keys are numbers and the values are arrays of VO objects.
   */
  getSCCs(): Map<number, VO[]> {
    return this.tarjan().SCCs;
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function `_addEdge` adds an edge to a graph if the source and destination vertexMap exist.
   * @param {EO} edge - The parameter `edge` is of type `EO`, which represents an edge in a graph. It is the edge that
   * needs to be added to the graph.
   * @returns a boolean value. It returns true if the edge was successfully added to the graph, and false if either the
   * source or destination vertex does not exist in the graph.
   */
  protected _addEdge(edge: EO): boolean {
    if (!(this.hasVertex(edge.src) && this.hasVertex(edge.dest))) {
      return false;
    }

    const srcVertex = this._getVertex(edge.src);
    const destVertex = this._getVertex(edge.dest);

    // TODO after no-non-undefined-assertion not ensure the logic
    if (srcVertex && destVertex) {
      const srcOutEdges = this._outEdgeMap.get(srcVertex);
      if (srcOutEdges) {
        srcOutEdges.push(edge);
      } else {
        this._outEdgeMap.set(srcVertex, [edge]);
      }

      const destInEdges = this._inEdgeMap.get(destVertex);
      if (destInEdges) {
        destInEdges.push(edge);
      } else {
        this._inEdgeMap.set(destVertex, [edge]);
      }
      return true;
    } else {
      return false;
    }
  }
}
