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

import type { GraphOptions, VertexKey } from '../../types';
import { IGraph } from '../../interfaces';
import { AbstractEdge, AbstractGraph, AbstractVertex } from './abstract-graph';
import { arrayRemove } from '../../utils';

export class UndirectedVertex<V = any> extends AbstractVertex<V> {
  constructor(key: VertexKey, value?: V) {
    super(key, value);
  }
}

export class UndirectedEdge<E = number> extends AbstractEdge<E> {
  endpoints: [VertexKey, VertexKey];

  constructor(v1: VertexKey, v2: VertexKey, weight?: number, value?: E) {
    super(weight, value);
    this.endpoints = [v1, v2];
  }
}

/**
 * Undirected graph implementation.
 * @template V - Vertex value type.
 * @template E - Edge value type.
 * @template VO - Concrete vertex class (extends AbstractVertex<V>).
 * @template EO - Concrete edge class (extends AbstractEdge<E>).
 * @remarks Time O(1), Space O(1)
 * @example
 * // basic UndirectedGraph vertex and edge creation
 *  // Create a simple undirected graph
 *     const graph = new UndirectedGraph<string>();
 *
 *     // Add vertices
 *     graph.addVertex('A');
 *     graph.addVertex('B');
 *     graph.addVertex('C');
 *     graph.addVertex('D');
 *
 *     // Verify vertices exist
 *     console.log(graph.hasVertex('A')); // true;
 *     console.log(graph.hasVertex('B')); // true;
 *     console.log(graph.hasVertex('E')); // false;
 *
 *     // Check vertex count
 *     console.log(graph.size); // 4;
 * @example
 * // UndirectedGraph edge operations (bidirectional)
 *  const graph = new UndirectedGraph<string>();
 *
 *     // Add vertices
 *     graph.addVertex('A');
 *     graph.addVertex('B');
 *     graph.addVertex('C');
 *
 *     // Add undirected edges (both directions automatically)
 *     graph.addEdge('A', 'B', 1);
 *     graph.addEdge('B', 'C', 2);
 *     graph.addEdge('A', 'C', 3);
 *
 *     // Verify edges exist in both directions
 *     console.log(graph.hasEdge('A', 'B')); // true;
 *     console.log(graph.hasEdge('B', 'A')); // true; // Bidirectional!
 *
 *     console.log(graph.hasEdge('C', 'B')); // true;
 *     console.log(graph.hasEdge('B', 'C')); // true; // Bidirectional!
 *
 *     // Get neighbors of A
 *     const neighborsA = graph.getNeighbors('A');
 *     console.log(neighborsA[0].key); // 'B';
 *     console.log(neighborsA[1].key); // 'C';
 * @example
 * // UndirectedGraph for social network connectivity analysis
 *  interface Person {
 *       id: number;
 *       name: string;
 *       location: string;
 *     }
 *
 *     // UndirectedGraph is perfect for modeling symmetric relationships
 *     // (friendships, collaborations, partnerships)
 *     const socialNetwork = new UndirectedGraph<number, Person>();
 *
 *     // Add people as vertices
 *     const people: [number, Person][] = [
 *       [1, { id: 1, name: 'Alice', location: 'New York' }],
 *       [2, { id: 2, name: 'Bob', location: 'San Francisco' }],
 *       [3, { id: 3, name: 'Charlie', location: 'Boston' }],
 *       [4, { id: 4, name: 'Diana', location: 'New York' }],
 *       [5, { id: 5, name: 'Eve', location: 'Seattle' }]
 *     ];
 *
 *     for (const [id] of people) {
 *       socialNetwork.addVertex(id);
 *     }
 *
 *     // Add friendships (automatically bidirectional)
 *     socialNetwork.addEdge(1, 2, 1); // Alice <-> Bob
 *     socialNetwork.addEdge(1, 3, 1); // Alice <-> Charlie
 *     socialNetwork.addEdge(2, 4, 1); // Bob <-> Diana
 *     socialNetwork.addEdge(3, 5, 1); // Charlie <-> Eve
 *     socialNetwork.addEdge(4, 5, 1); // Diana <-> Eve
 *
 *     console.log(socialNetwork.size); // 5;
 *
 *     // Find direct connections for Alice
 *     const aliceConnections = socialNetwork.getNeighbors(1);
 *     console.log(aliceConnections[0].key); // 2;
 *     console.log(aliceConnections[1].key); // 3;
 *     console.log(aliceConnections.length); // 2;
 *
 *     // Verify bidirectional connections
 *     console.log(socialNetwork.hasEdge(1, 2)); // true;
 *     console.log(socialNetwork.hasEdge(2, 1)); // true; // Friendship works both ways!
 *
 *     // Remove a person from network
 *     socialNetwork.deleteVertex(2); // Bob leaves
 *     console.log(socialNetwork.hasVertex(2)); // false;
 *     console.log(socialNetwork.size); // 4;
 *
 *     // Alice loses Bob as a friend
 *     const updatedAliceConnections = socialNetwork.getNeighbors(1);
 *     console.log(updatedAliceConnections[0].key); // 3;
 *     console.log(updatedAliceConnections[1]); // undefined;
 *
 *     // Diana loses Bob as a friend
 *     const dianaConnections = socialNetwork.getNeighbors(4);
 *     console.log(dianaConnections[0].key); // 5;
 *     console.log(dianaConnections[1]); // undefined;
 */
export class UndirectedGraph<
  V = any,
  E = any,
  VO extends UndirectedVertex<V> = UndirectedVertex<V>,
  EO extends UndirectedEdge<E> = UndirectedEdge<E>
>
  extends AbstractGraph<V, E, VO, EO>
  implements IGraph<V, E, VO, EO>
{
  /**
   * Construct an undirected graph with runtime defaults.
   * @param options - `GraphOptions<V>` (e.g. `vertexValueInitializer`, `defaultEdgeWeight`).
   * @remarks Time O(1), Space O(1)
   */
  constructor(options?: Partial<GraphOptions<V>>) {
    super(options);
    this._edgeMap = new Map<VO, EO[]>();
  }

  protected _edgeMap: Map<VO, EO[]>;

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

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

  /**
   * Construct an undirected graph from keys with value initializer `v => v`.
   * @template K - Vertex key type.
   * @param keys - Iterable of vertex keys.
   * @returns UndirectedGraph with all keys added.
   * @remarks Time O(V), Space O(V)
   */
  static fromKeys<K extends VertexKey>(
    keys: Iterable<K>
  ): UndirectedGraph<K, undefined, UndirectedVertex<K>, UndirectedEdge<undefined>> {
    const g: UndirectedGraph<K, undefined, UndirectedVertex<K>, UndirectedEdge<undefined>> = new UndirectedGraph<K, undefined>({
      vertexValueInitializer: (k: VertexKey) => k as K
    });
    for (const k of keys) g.addVertex(k);
    return g;
  }

  /**
   * Construct an undirected graph from `[key, value]` entries.
   * @template V - Vertex value type.
   * @param entries - Iterable of `[key, value]` pairs.
   * @returns UndirectedGraph with all vertices added.
   * @remarks Time O(V), Space O(V)
   */
  static fromEntries<V>(
    entries: Iterable<[VertexKey, V]>
  ): UndirectedGraph<V, undefined, UndirectedVertex<V>, UndirectedEdge<undefined>> {
    const g: UndirectedGraph<V, undefined, UndirectedVertex<V>, UndirectedEdge<undefined>> = new UndirectedGraph<V, undefined>();
    for (const [k, v] of entries) g.addVertex(k, v);
    return g;
  }

  /**
   * Create an undirected vertex instance. Does not insert into the graph.
   * @param key - Vertex identifier.
   * @param value - Optional payload.
   * @returns Concrete vertex instance.
   * @remarks Time O(1), Space O(1)
   */
  createVertex(key: VertexKey, value?: VO['value']): VO {
    return new UndirectedVertex(key, value) as VO;
  }

  /**
   * Create an undirected edge instance. Does not insert into the graph.
   * @param v1 - One endpoint key.
   * @param v2 - The other endpoint key.
   * @param weight - Edge weight; defaults to `defaultEdgeWeight`.
   * @param value - Edge payload.
   * @returns Concrete edge instance.
   * @remarks Time O(1), Space O(1)
   */
  override createEdge(v1: VertexKey, v2: VertexKey, weight?: number, value?: EO['value']): EO {
    return new UndirectedEdge(v1, v2, weight ?? this.options.defaultEdgeWeight ?? 1, value) as EO;
  }

  /**
   * Get an undirected edge between two vertices, if present.
   * @param v1 - One vertex or key.
   * @param v2 - The other vertex or key.
   * @returns Edge instance or `undefined`.
   * @remarks Time O(1) avg, Space O(1)
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Get edge between vertices
 *  const g = new UndirectedGraph();
 *     g.addVertex('A');
 *     g.addVertex('B');
 *     g.addEdge('A', 'B', 7);
 *     console.log(g.getEdge('A', 'B')?.weight); // 7;
   */
  getEdge(v1: VO | VertexKey | undefined, v2: VO | VertexKey | undefined): EO | undefined {
    let edgeMap: EO[] | undefined = [];

    if (v1 !== undefined && v2 !== undefined) {
      const vertex1: VO | undefined = this._getVertex(v1);
      const vertex2: VO | undefined = this._getVertex(v2);

      if (vertex1 && vertex2) {
        edgeMap = this._edgeMap.get(vertex1)?.filter(e => e.endpoints.includes(vertex2.key));
      }
    }

    return edgeMap ? edgeMap[0] || undefined : undefined;
  }

  /**
   * Delete a single undirected edge between two vertices.
   * @param v1 - One vertex or key.
   * @param v2 - The other vertex or key.
   * @returns Removed edge or `undefined`.
   * @remarks Time O(1) avg, Space O(1)
   */
  deleteEdgeBetween(v1: VO | VertexKey, v2: VO | VertexKey): EO | undefined {
    const vertex1: VO | undefined = this._getVertex(v1);
    const vertex2: VO | undefined = this._getVertex(v2);

    if (!vertex1 || !vertex2) {
      return undefined;
    }

    const v1Edges = this._edgeMap.get(vertex1);
    let removed: EO | undefined = undefined;
    if (v1Edges) {
      removed = arrayRemove<EO>(v1Edges, (e: EO) => e.endpoints.includes(vertex2.key))[0] || undefined;
    }
    const v2Edges = this._edgeMap.get(vertex2);
    if (v2Edges) {
      arrayRemove<EO>(v2Edges, (e: EO) => e.endpoints.includes(vertex1.key));
    }
    return removed;
  }

  /**
   * Delete an edge by instance or by a pair of keys.
   * @param edgeOrOneSideVertexKey - Edge instance or one endpoint vertex/key.
   * @param otherSideVertexKey - Required second endpoint when deleting by pair.
   * @returns Removed edge or `undefined`.
   * @remarks Time O(1) avg, Space O(1)
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // UndirectedGraph deleteEdge and vertex operations
 *  const graph = new UndirectedGraph<string>();
 *
 *     // Build a simple undirected graph
 *     graph.addVertex('X');
 *     graph.addVertex('Y');
 *     graph.addVertex('Z');
 *     graph.addEdge('X', 'Y', 1);
 *     graph.addEdge('Y', 'Z', 2);
 *     graph.addEdge('X', 'Z', 3);
 *
 *     // Delete an edge
 *     graph.deleteEdge('X', 'Y');
 *     console.log(graph.hasEdge('X', 'Y')); // false;
 *
 *     // Bidirectional deletion confirmed
 *     console.log(graph.hasEdge('Y', 'X')); // false;
 *
 *     // Other edges should remain
 *     console.log(graph.hasEdge('Y', 'Z')); // true;
 *     console.log(graph.hasEdge('Z', 'Y')); // true;
 *
 *     // Delete a vertex
 *     graph.deleteVertex('Y');
 *     console.log(graph.hasVertex('Y')); // false;
 *     console.log(graph.size); // 2;
   */
  deleteEdge(edgeOrOneSideVertexKey: EO | VertexKey, otherSideVertexKey?: VertexKey): EO | undefined {
    let oneSide: VO | undefined, otherSide: VO | undefined;
    if (this.isVertexKey(edgeOrOneSideVertexKey)) {
      if (this.isVertexKey(otherSideVertexKey)) {
        oneSide = this._getVertex(edgeOrOneSideVertexKey);
        otherSide = this._getVertex(otherSideVertexKey);
      } else {
        return;
      }
    } else {
      oneSide = this._getVertex(edgeOrOneSideVertexKey.endpoints[0]);
      otherSide = this._getVertex(edgeOrOneSideVertexKey.endpoints[1]);
    }

    if (oneSide && otherSide) {
      return this.deleteEdgeBetween(oneSide, otherSide);
    } else {
      return;
    }
  }

  /**
   * Delete a vertex and remove it from all neighbor lists.
   * @param vertexOrKey - Vertex or key.
   * @returns `true` if removed; otherwise `false`.
   * @remarks Time O(deg), Space O(1)
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Remove vertex and edges
 *  const g = new UndirectedGraph();
 *     g.addVertex('A');
 *     g.addVertex('B');
 *     g.addEdge('A', 'B');
 *     g.deleteVertex('A');
 *     console.log(g.hasVertex('A')); // false;
   */
  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);
    }

    /**
     * All neighbors connected via undirected edges.
     * @param vertexOrKey - Vertex or key.
     * @returns Array of neighbor vertices.
     * @remarks Time O(deg), Space O(deg)
     */
    const neighbors = this.getNeighbors(vertexOrKey);

    if (vertex) {
      neighbors.forEach(neighbor => {
        const neighborEdges = this._edgeMap.get(neighbor);
        if (neighborEdges) {
          const restEdges = neighborEdges.filter(edge => {
            return !edge.endpoints.includes(vertexKey);
          });
          this._edgeMap.set(neighbor, restEdges);
        }
      });
      this._edgeMap.delete(vertex);
    }

    return this._vertexMap.delete(vertexKey);
  }

  /**
   * Degree of a vertex (# of incident undirected edges).
   * @param vertexOrKey - Vertex or key.
   * @returns Non-negative integer.
   * @remarks Time O(1) avg, Space O(1)
   */
  degreeOf(vertexOrKey: VertexKey | VO): number {
    const vertex = this._getVertex(vertexOrKey);
    if (vertex) {
      return this._edgeMap.get(vertex)?.length || 0;
    } else {
      return 0;
    }
  }

  /**
   * Incident undirected edges of a vertex.
   * @param vertexOrKey - Vertex or key.
   * @returns Array of incident edges.
   * @remarks Time O(deg), Space O(deg)
   */
  edgesOf(vertexOrKey: VertexKey | VO): EO[] {
    const vertex = this._getVertex(vertexOrKey);
    if (vertex) {
      return this._edgeMap.get(vertex) || [];
    } else {
      return [];
    }
  }

  /**
   * Unique set of undirected edges across endpoints.
   * @returns Array of edges.
   * @remarks Time O(E), Space O(E)
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Get all edges
 *  const g = new UndirectedGraph();
 *     g.addVertex('A');
 *     g.addVertex('B');
 *     g.addEdge('A', 'B');
 *     console.log(g.edgeSet().length); // 1;
   */
  edgeSet(): EO[] {
    const edgeSet: Set<EO> = new Set();
    this._edgeMap.forEach(edgeMap => {
      edgeMap.forEach(edge => {
        edgeSet.add(edge);
      });
    });
    return [...edgeSet];
  }

    /**
   * UndirectedGraph connectivity and neighbors
  
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // UndirectedGraph connectivity and neighbors
 *  const graph = new UndirectedGraph<string>();
 *
 *     // Build a friendship network
 *     const people = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'];
 *     for (const person of people) {
 *       graph.addVertex(person);
 *     }
 *
 *     // Add friendships (undirected edges)
 *     graph.addEdge('Alice', 'Bob', 1);
 *     graph.addEdge('Alice', 'Charlie', 1);
 *     graph.addEdge('Bob', 'Diana', 1);
 *     graph.addEdge('Charlie', 'Eve', 1);
 *     graph.addEdge('Diana', 'Eve', 1);
 *
 *     // Get friends of each person
 *     const aliceFriends = graph.getNeighbors('Alice');
 *     console.log(aliceFriends[0].key); // 'Bob';
 *     console.log(aliceFriends[1].key); // 'Charlie';
 *     console.log(aliceFriends.length); // 2;
 *
 *     const dianaFriends = graph.getNeighbors('Diana');
 *     console.log(dianaFriends[0].key); // 'Bob';
 *     console.log(dianaFriends[1].key); // 'Eve';
 *     console.log(dianaFriends.length); // 2;
 *
 *     // Verify bidirectional friendship
 *     const bobFriends = graph.getNeighbors('Bob');
 *     console.log(bobFriends[0].key); // 'Alice'; // Alice -> Bob -> Alice ✓
 *     console.log(bobFriends[1].key); // 'Diana';
   */
  getNeighbors(vertexOrKey: VO | VertexKey): VO[] {
    const neighbors: VO[] = [];
    const vertex = this._getVertex(vertexOrKey);
    if (vertex) {
      const neighborEdges = this.edgesOf(vertex);
      for (const edge of neighborEdges) {
        const neighbor = this._getVertex(edge.endpoints.filter(e => e !== vertex.key)[0]);
        if (neighbor) {
          neighbors.push(neighbor);
        }
      }
    }
    return neighbors;
  }

  /**
   * Resolve an edge's two endpoints to vertex instances.
   * @param edge - Edge instance.
   * @returns `[v1, v2]` or `undefined` if either endpoint is missing.
   * @remarks Time O(1), Space O(1)
   */
  getEndsOfEdge(edge: EO): [VO, VO] | undefined {
    if (!this.hasEdge(edge.endpoints[0], edge.endpoints[1])) {
      return undefined;
    }
    const v1 = this._getVertex(edge.endpoints[0]);
    const v2 = this._getVertex(edge.endpoints[1]);
    if (v1 && v2) {
      return [v1, v2];
    } else {
      return undefined;
    }
  }

  /**
   * Whether the graph has no vertices and no edges.
   * @remarks Time O(1), Space O(1)
   */
  isEmpty(): boolean {
    return this.vertexMap.size === 0 && this.edgeMap.size === 0;
  }

  /**
   * Remove all vertices and edges.
   * @remarks Time O(V + E), Space O(1)
   */
  clear(): void {
    this._vertexMap = new Map<VertexKey, VO>();
    this._edgeMap = new Map<VO, EO[]>();
  }

  /**
   * Deep clone as the same concrete class.
   * @returns A new graph of the same concrete class (`this` type).
   * @remarks Time O(V + E), Space O(V + E)
   */
  override clone(): this {
    return super.clone();
  }

  /**
   * Tarjan-based bridge and articulation point detection.
   * @returns `{ dfnMap, lowMap, bridges, cutVertices }`.
   * @remarks Time O(V + E), Space O(V + E)
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Find articulation points and bridges
 *  const g = new UndirectedGraph();
 *     g.addVertex('A');
 *     g.addVertex('B');
 *     g.addVertex('C');
 *     g.addEdge('A', 'B');
 *     g.addEdge('B', 'C');
 *     const result = g.tarjan();
 *     console.log(result); // defined;
   */
  tarjan(): { dfnMap: Map<VO, number>; lowMap: Map<VO, number>; bridges: EO[]; cutVertices: VO[] } {
    const dfnMap = new Map<VO, number>();
    const lowMap = new Map<VO, number>();
    const bridges: EO[] = [];
    const cutVertices: VO[] = [];

    let time = 0;

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

      const neighbors = this.getNeighbors(vertex);
      let childCount = 0;

      for (const neighbor of neighbors) {
        if (!dfnMap.has(neighbor)) {
          childCount++;
          dfs(neighbor, vertex);
          lowMap.set(vertex, Math.min(lowMap.get(vertex)!, lowMap.get(neighbor)!));

          if (lowMap.get(neighbor)! > dfnMap.get(vertex)!) {
            // Found a bridge
            const edge = this.getEdge(vertex, neighbor);
            if (edge) {
              bridges.push(edge);
            }
          }

          if (parent !== undefined && lowMap.get(neighbor)! >= dfnMap.get(vertex)!) {
            // Found an articulation point
            cutVertices.push(vertex);
          }
        } else if (neighbor !== parent) {
          lowMap.set(vertex, Math.min(lowMap.get(vertex)!, dfnMap.get(neighbor)!));
        }
      }

      if (parent === undefined && childCount > 1) {
        // Special case for root in DFS tree
        cutVertices.push(vertex);
      }
    };

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

    return {
      dfnMap,
      lowMap,
      bridges,
      cutVertices
    };
  }

  /**
   * Find biconnected components using edge-stack Tarjan variant.
   * A biconnected component is a maximal biconnected subgraph.
   * @returns Array of edge arrays, each representing a biconnected component.
   * @remarks Time O(V + E), Space O(V + E)
   */
  getBiconnectedComponents(): EO[][] {
    const dfn = new Map<VO, number>();
    const low = new Map<VO, number>();
    const edgeStack: EO[] = [];
    const components: EO[][] = [];
    let time = 0;

    const dfs = (vertex: VO, parent: VO | undefined) => {
      dfn.set(vertex, time);
      low.set(vertex, time);
      time++;

      const neighbors = this.getNeighbors(vertex);
      let childCount = 0;

      for (const neighbor of neighbors) {
        const edge = this.getEdge(vertex, neighbor);
        if (!edge) continue;

        if (!dfn.has(neighbor)) {
          childCount++;
          edgeStack.push(edge);
          dfs(neighbor, vertex);
          low.set(vertex, Math.min(low.get(vertex)!, low.get(neighbor)!));

          // Articulation point found — pop edges to form a component
          if (
            (parent === undefined && childCount > 1) ||
            (parent !== undefined && low.get(neighbor)! >= dfn.get(vertex)!)
          ) {
            const component: EO[] = [];
            let e: EO | undefined;
            do {
              e = edgeStack.pop();
              if (e) component.push(e);
            } while (e && e !== edge);
            if (component.length > 0) components.push(component);
          }
        } else if (neighbor !== parent && dfn.get(neighbor)! < dfn.get(vertex)!) {
          // Back edge (only push once per undirected edge)
          edgeStack.push(edge);
          low.set(vertex, Math.min(low.get(vertex)!, dfn.get(neighbor)!));
        }
      }
    };

    for (const vertex of this.vertexMap.values()) {
      if (!dfn.has(vertex)) {
        dfs(vertex, undefined);
        // Remaining edges form a component
        if (edgeStack.length > 0) {
          components.push([...edgeStack]);
          edgeStack.length = 0;
        }
      }
    }

    return components;
  }

  /**
   * Detect whether the graph contains a cycle.
   * Uses DFS with parent tracking.
   * @returns `true` if a cycle exists, `false` otherwise.
   * @remarks Time O(V + E), Space O(V)
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Detect cycle
 *  const g = new UndirectedGraph();
 *     g.addVertex('A');
 *     g.addVertex('B');
 *     g.addVertex('C');
 *     g.addEdge('A', 'B');
 *     g.addEdge('B', 'C');
 *     console.log(g.hasCycle()); // false;
 *     g.addEdge('C', 'A');
 *     console.log(g.hasCycle()); // true;
   */
  hasCycle(): boolean {
    const visited = new Set<VO>();

    const dfs = (vertex: VO, parent: VO | undefined): boolean => {
      visited.add(vertex);
      for (const neighbor of this.getNeighbors(vertex)) {
        if (!visited.has(neighbor)) {
          if (dfs(neighbor, vertex)) return true;
        } else if (neighbor !== parent) {
          return true; // back edge = cycle
        }
      }
      return false;
    };

    for (const vertex of this.vertexMap.values()) {
      if (!visited.has(vertex)) {
        if (dfs(vertex, undefined)) return true;
      }
    }
    return false;
  }

  /**
   * Get bridges discovered by `tarjan()`.
   * @returns Array of edges that are bridges.
   * @remarks Time O(B), Space O(1)
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Find bridge edges
 *  const g = new UndirectedGraph();
 *     g.addVertex('A');
 *     g.addVertex('B');
 *     g.addVertex('C');
 *     g.addEdge('A', 'B');
 *     g.addEdge('B', 'C');
 *     const bridges = g.getBridges();
 *     console.log(bridges.length); // 2;
   */
  getBridges() {
    return this.tarjan().bridges;
  }

  /**
   * Get articulation points discovered by `tarjan()`.
   * @returns Array of cut vertices.
   * @remarks Time O(C), Space O(1)
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
   
    * @example
 * // Find articulation points
 *  const g = new UndirectedGraph();
 *     g.addVertex('A');
 *     g.addVertex('B');
 *     g.addVertex('C');
 *     g.addEdge('A', 'B');
 *     g.addEdge('B', 'C');
 *     const cuts = g.getCutVertices();
 *     console.log(cuts.length); // 1;
 *     console.log(cuts[0].key); // 'B';
   */
  getCutVertices() {
    return this.tarjan().cutVertices;
  }

  /**
   * DFN index map computed by `tarjan()`.
   * @returns Map from vertex to DFN index.
   * @remarks Time O(V), Space O(V)
   */
  getDFNMap() {
    return this.tarjan().dfnMap;
  }

  /**
   * LOW link map computed by `tarjan()`.
   * @returns Map from vertex to LOW value.
   * @remarks Time O(V), Space O(V)
   */
  getLowMap() {
    return this.tarjan().lowMap;
  }

  /**
   * Internal hook to attach an undirected edge into adjacency maps.
   * @param edge - Edge instance.
   * @returns `true` if both endpoints exist; otherwise `false`.
   * @remarks Time O(1) avg, Space O(1)
   */
  protected _addEdge(edge: EO): boolean {
    for (const end of edge.endpoints) {
      const endVertex = this._getVertex(end);
      if (endVertex === undefined) return false;
      if (endVertex) {
        const edgeMap = this._edgeMap.get(endVertex);
        if (edgeMap) {
          edgeMap.push(edge);
        } else {
          this._edgeMap.set(endVertex, [edge]);
        }
      }
    }
    return true;
  }
}
