/**
 * data-structure-typed
 *
 * @author Pablo Zeng
 * @copyright Copyright (c) 2022 Pablo Zeng <zrwusa@gmail.com>
 * @license MIT License
 */
import type { 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> {
  /**
   * 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 network.
   * @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 UndirectedEdge<E = number> extends AbstractEdge<E> {
  endpoints: [VertexKey, VertexKey];

  /**
   * The constructor function creates an instance of a class with two vertex IDs, an optional weight, and an optional
   * value.
   * @param {VertexKey} v1 - The first vertex ID of the edge.
   * @param {VertexKey} v2 - The parameter `v2` is a `VertexKey`, which represents the identifier of the second vertex in a
   * graph edge.
   * @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 is used to store a value associated
   * with the edge.
   */
  constructor(v1: VertexKey, v2: VertexKey, weight?: number, value?: E) {
    super(weight, value);
    this.endpoints = [v1, v2];
  }
}

/**
 *
 */
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>
{
  /**
   * The constructor initializes a new Map object to store edgeMap.
   */
  constructor() {
    super();
    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;
  }

  /**
   * 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 used to distinguish one
   * vertex from another in the graph.
   * @param [value] - The `value` parameter is an optional value that can be assigned to the vertex. If a value is provided,
   * it will be used as the value of the vertex. If no value is provided, the `key` parameter will be used as the value of
   * the vertex.
   * @returns The method is returning a new instance of the `UndirectedVertex` class, casted as type `VO`.
   */
  override createVertex(key: VertexKey, value?: VO['value']): VO {
    return new UndirectedVertex(key, value ?? key) as VO;
  }

  /**
   * The function creates an undirected edge between two vertexMap with an optional weight and value.
   * @param {VertexKey} v1 - The parameter `v1` represents the first vertex of the edge.
   * @param {VertexKey} v2 - The parameter `v2` represents the second vertex of 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 the `UndirectedEdge` class, which is casted as type `EO`.
   */
  override createEdge(v1: VertexKey, v2: VertexKey, weight?: number, value?: EO['value']): EO {
    return new UndirectedEdge(v1, v2, weight ?? 1, value) as EO;
  }

  /**
   * Time Complexity: O(|E|), where |E| is the number of edgeMap incident to the given vertex.
   * Space Complexity: O(1)
   *
   * The function `getEdge` returns the first edge that connects two endpoints, or undefined if no such edge exists.
   * @param {VO | VertexKey | undefined} v1 - The parameter `v1` represents a vertex or vertex ID. It can be of type `VO` (vertex
   * object), `undefined`, or `VertexKey` (a string or number representing the ID of a vertex).
   * @param {VO | VertexKey | undefined} v2 - The parameter `v2` represents a vertex or vertex ID. It can be of type `VO` (vertex
   * object), `undefined`, or `VertexKey` (vertex ID).
   * @returns an edge (EO) or undefined.
   */
  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;
  }

  /**
   * Time Complexity: O(|E|), where |E| is the number of edgeMap incident to the given vertex.
   * Space Complexity: O(1)
   *
   * The function removes an edge between two vertexMap in a graph and returns the removed edge.
   * @param {VO | VertexKey} v1 - The parameter `v1` represents either a vertex object (`VO`) or a vertex ID (`VertexKey`).
   * @param {VO | VertexKey} v2 - VO | VertexKey - This parameter can be either a vertex object (VO) or a vertex ID
   * (VertexKey). It represents the second vertex of the edge that needs to be removed.
   * @returns the removed edge (EO) if it exists, or undefined if either of the endpoints (VO) does not exist.
   */
  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;
  }

  /**
   * Time Complexity: O(E), where E is the number of edgeMap incident to the given vertex.
   * Space Complexity: O(1)
   *
   * The function `deleteEdge` deletes an edge between two endpoints in a graph.
   * @param {EO | VertexKey} edgeOrOneSideVertexKey - The parameter `edgeOrOneSideVertexKey` can be
   * either an edge object or a vertex key.
   * @param {VertexKey} [otherSideVertexKey] - The parameter `otherSideVertexKey` is an optional
   * parameter that represents the key of the vertex on the other side of the edge. It is used when the
   * `edgeOrOneSideVertexKey` parameter is a vertex key, and it specifies the key of the vertex on the
   * other side of the
   * @returns The `deleteEdge` function returns either the deleted edge object (EO) or `undefined`.
   */
  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;
    }
  }

  /**
   * 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);
    }

    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);
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function `degreeOf` returns the degree of a vertex in a graph, which is the number of edgeMap connected to that
   * vertex.
   * @param {VertexKey | VO} vertexOrKey - The parameter `vertexOrKey` can be either a `VertexKey` or a `VO`.
   * @returns The function `degreeOf` returns the degree of a vertex in a graph. The degree of a vertex is the number of
   * edgeMap connected to that vertex.
   */
  degreeOf(vertexOrKey: VertexKey | VO): number {
    const vertex = this._getVertex(vertexOrKey);
    if (vertex) {
      return this._edgeMap.get(vertex)?.length || 0;
    } else {
      return 0;
    }
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function returns the edgeMap of a given vertex or vertex ID.
   * @param {VertexKey | VO} vertexOrKey - The parameter `vertexOrKey` can be either a `VertexKey` or a `VO`. A `VertexKey` is a
   * unique identifier for a vertex in a graph, while `VO` represents the type of the vertex.
   * @returns an array of edgeMap.
   */
  edgesOf(vertexOrKey: VertexKey | VO): EO[] {
    const vertex = this._getVertex(vertexOrKey);
    if (vertex) {
      return this._edgeMap.get(vertex) || [];
    } else {
      return [];
    }
  }

  /**
   * Time Complexity: O(|V| + |E|), where |V| is the number of vertexMap and |E| is the number of edgeMap.
   * Space Complexity: O(|E|)
   *
   * The function "edgeSet" returns an array of unique edgeMap from a set of edgeMap.
   * @returns The method `edgeSet()` returns an array of type `EO[]`.
   */
  edgeSet(): EO[] {
    const edgeSet: Set<EO> = new Set();
    this._edgeMap.forEach(edgeMap => {
      edgeMap.forEach(edge => {
        edgeSet.add(edge);
      });
    });
    return [...edgeSet];
  }

  /**
   * Time Complexity: O(|V| + |E|), where |V| is the number of vertexMap and |E| is the number of edgeMap.
   * Space Complexity: O(|E|)
   *
   * The function "getNeighbors" returns an array of neighboring endpoints 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 an array of vertexMap (VO[]).
   */
  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;
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function "getEndsOfEdge" returns the endpoints at the ends of an edge if the edge 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 endpoints `[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.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;
    }
  }

  /**
   * The isEmpty function checks if the graph is empty.
   * @return True if the graph is empty and false otherwise
   */
  isEmpty(): boolean {
    return this.vertexMap.size === 0 && this.edgeMap.size === 0;
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The clear function resets the vertex and edge maps to empty maps.
   */
  clear() {
    this._vertexMap = new Map<VertexKey, VO>();
    this._edgeMap = new Map<VO, EO[]>();
  }

  /**
   * The clone function creates a new UndirectedGraph object and copies the
   * vertexMap and edgeMap from this graph to the new one. This is done by
   * assigning each of these properties to their respective counterparts in the
   * cloned graph. The clone function returns a reference to this newly created,
   * cloned UndirectedGraph object.
   *
   * @return A new instance of the undirectedgraph class
   */
  clone(): UndirectedGraph<V, E, VO, EO> {
    const cloned = new UndirectedGraph<V, E, VO, EO>();
    cloned.vertexMap = new Map<VertexKey, VO>(this.vertexMap);
    cloned.edgeMap = new Map<VO, EO[]>(this.edgeMap);
    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.
   *  1. Tarjan can find the articulation points and bridges(critical edgeMap) of undirected graphs in linear time
   *
   * The function `tarjan` implements the Tarjan's algorithm to find bridges and cut vertices in a
   * graph.
   * @returns The function `tarjan()` returns an object with the following properties:
   */
  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
    };
  }

  /**
   * The function "getBridges" returns an array of bridges in a graph using the Tarjan's algorithm.
   * @returns The function `getBridges()` is returning the bridges found using the Tarjan's algorithm.
   */
  getBridges() {
    return this.tarjan().bridges;
  }

  /**
   * The function "getCutVertices" returns an array of cut vertices using the Tarjan's algorithm.
   * @returns the cut vertices found using the Tarjan's algorithm.
   */
  getCutVertices() {
    return this.tarjan().cutVertices;
  }

  /**
   * The function returns the dfnMap property of the result of the tarjan() function.
   * @returns the `dfnMap` property of the result of calling the `tarjan()` function.
   */
  getDFNMap() {
    return this.tarjan().dfnMap;
  }

  /**
   * The function returns the lowMap property of the result of the tarjan() function.
   * @returns the lowMap property of the result of calling the tarjan() function.
   */
  getLowMap() {
    return this.tarjan().lowMap;
  }

  /**
   * Time Complexity: O(1)
   * Space Complexity: O(1)
   *
   * The function adds an edge to the graph by updating the adjacency list with the vertexMap of the edge.
   * @param {EO} edge - The parameter "edge" is of type EO, which represents an edge in a graph.
   * @returns a boolean value.
   */
  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;
  }
}
