// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as Common from '../../../core/common/common.js';
import * as Platform from '../../../core/platform/platform.js';

import {EdgeTypes, EdgeView, generateEdgePortIdsByData} from './EdgeView.js';
import type {
  NodeCreationData, NodeParamConnectionData, NodeParamDisconnectionData, NodesConnectionData, NodesDisconnectionData,
  NodesDisconnectionDataWithDestination, ParamCreationData} from './GraphStyle.js';
import {NodeLabelGenerator, NodeView} from './NodeView.js';

// A class that tracks all the nodes and edges of an audio graph.
export class GraphView extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
  contextId: string;
  private readonly nodes: Map<string, NodeView>;
  private readonly edges: Map<string, EdgeView>;
  private readonly outboundEdgeMap: Platform.MapUtilities.Multimap<string, string>;
  private readonly inboundEdgeMap: Platform.MapUtilities.Multimap<string, string>;
  private readonly nodeLabelGenerator: NodeLabelGenerator;
  private readonly paramIdToNodeIdMap: Map<string, string>;
  constructor(contextId: string) {
    super();

    this.contextId = contextId;

    this.nodes = new Map();
    this.edges = new Map();

    /**
     * For each node ID, keep a set of all out-bound edge IDs.
     */
    this.outboundEdgeMap = new Platform.MapUtilities.Multimap();

    /**
     * For each node ID, keep a set of all in-bound edge IDs.
     */
    this.inboundEdgeMap = new Platform.MapUtilities.Multimap();

    // Use concise node label to replace the long UUID.
    // Each graph has its own label generator so that the label starts from 0.
    this.nodeLabelGenerator = new NodeLabelGenerator();

    /**
     * For each param ID, save its corresponding node Id.
     */
    this.paramIdToNodeIdMap = new Map();
  }

  /**
   * Add a node to the graph.
   */
  addNode(data: NodeCreationData): void {
    const label = this.nodeLabelGenerator.generateLabel(data.nodeType);
    const node = new NodeView(data, label);
    this.nodes.set(data.nodeId, node);
    this.notifyShouldRedraw();
  }

  /**
   * Remove a node by id and all related edges.
   */
  removeNode(nodeId: string): void {
    this.outboundEdgeMap.get(nodeId).forEach(edgeId => this.removeEdge(edgeId));
    this.inboundEdgeMap.get(nodeId).forEach(edgeId => this.removeEdge(edgeId));
    this.nodes.delete(nodeId);
    this.notifyShouldRedraw();
  }

  /**
   * Add a param to the node.
   */
  addParam(data: ParamCreationData): void {
    const node = this.getNodeById(data.nodeId);
    if (!node) {
      console.error('AudioNode should be added before AudioParam');
      return;
    }
    node.addParamPort(data.paramId, data.paramType);
    this.paramIdToNodeIdMap.set(data.paramId, data.nodeId);
    this.notifyShouldRedraw();
  }

  /**
   * Remove a param.
   */
  removeParam(paramId: string): void {
    // Only need to delete the entry from the param id to node id map.
    this.paramIdToNodeIdMap.delete(paramId);
    // No need to remove the param port from the node because removeParam will always happen with
    // removeNode(). Since the whole Node will be gone, there is no need to remove port individually.
  }

  /**
   * Add a Node-to-Node connection to the graph.
   */
  addNodeToNodeConnection(edgeData: NodesConnectionData): void {
    const edge = new EdgeView(edgeData, EdgeTypes.NODE_TO_NODE);
    this.addEdge(edge);
  }

  /**
   * Remove a Node-to-Node connection from the graph.
   */
  removeNodeToNodeConnection(edgeData: NodesDisconnectionData): void {
    if (edgeData.destinationId) {
      // Remove a single edge if destinationId is specified.
      const edgePortIds =
          generateEdgePortIdsByData((edgeData as NodesDisconnectionDataWithDestination), EdgeTypes.NODE_TO_NODE);

      if (!edgePortIds) {
        throw new Error('Unable to generate edge port IDs');
      }
      const {edgeId} = edgePortIds;

      this.removeEdge(edgeId);
    } else {
      // Otherwise, remove all outgoing edges from source node.
      this.outboundEdgeMap.get(edgeData.sourceId).forEach(edgeId => this.removeEdge(edgeId));
    }
  }

  /**
   * Add a Node-to-Param connection to the graph.
   */
  addNodeToParamConnection(edgeData: NodeParamConnectionData): void {
    const edge = new EdgeView(edgeData, EdgeTypes.NODE_TO_PARAM);
    this.addEdge(edge);
  }

  /**
   * Remove a Node-to-Param connection from the graph.
   */
  removeNodeToParamConnection(edgeData: NodeParamDisconnectionData): void {
    const edgePortIds = generateEdgePortIdsByData(edgeData, EdgeTypes.NODE_TO_PARAM);
    if (!edgePortIds) {
      throw new Error('Unable to generate edge port IDs');
    }

    const {edgeId} = edgePortIds;
    this.removeEdge(edgeId);
  }

  getNodeById(nodeId: string): NodeView|null {
    return this.nodes.get(nodeId) || null;
  }

  getNodes(): Map<string, NodeView> {
    return this.nodes;
  }

  getEdges(): Map<string, EdgeView> {
    return this.edges;
  }

  getNodeIdByParamId(paramId: string): string|null {
    return this.paramIdToNodeIdMap.get(paramId) || null;
  }

  /**
   * Add an edge to the graph.
   */
  private addEdge(edge: EdgeView): void {
    const sourceId = edge.sourceId;
    // Do nothing if the edge already exists.
    if (this.outboundEdgeMap.hasValue(sourceId, edge.id)) {
      return;
    }

    this.edges.set(edge.id, edge);
    this.outboundEdgeMap.set(sourceId, edge.id);
    this.inboundEdgeMap.set(edge.destinationId, edge.id);

    this.notifyShouldRedraw();
  }

  /**
   * Given an edge id, remove the edge from the graph.
   * Also remove the edge from inbound and outbound edge maps.
   */
  private removeEdge(edgeId: string): void {
    const edge = this.edges.get(edgeId);
    if (!edge) {
      return;
    }

    this.outboundEdgeMap.delete(edge.sourceId, edgeId);
    this.inboundEdgeMap.delete(edge.destinationId, edgeId);

    this.edges.delete(edgeId);
    this.notifyShouldRedraw();
  }

  private notifyShouldRedraw(): void {
    this.dispatchEventToListeners(Events.SHOULD_REDRAW, this);
  }
}

export const enum Events {
  SHOULD_REDRAW = 'ShouldRedraw',
}

export interface EventTypes {
  [Events.SHOULD_REDRAW]: GraphView;
}
