/*
 * Copyright 2003-2014 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jetbrains.mps.classloading;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

public class GraphHolder<V> {
  private static final Logger LOG = LogManager.getLogger(GraphHolder.class);
  private final Graph<V> myGraph;
  private final Graph<V> myConjugateGraph; // transposed graph

  public GraphHolder() {
    myGraph = new Graph<V>();
    myConjugateGraph = new Graph<V>();
  }

  public int getEdgesCount() {
    checkGraphsCorrectness();
    return myGraph.getEdgesCount();
  }

  public int getVerticesCount() {
    checkGraphsCorrectness();
    return myGraph.getVerticesCount();
  }

  public void checkGraphsCorrectness() {
    if (myGraph.getVerticesCount() != myConjugateGraph.getVerticesCount()) {
      throw new GraphsInconsistencyException("Difference in vertices' count");
    }
    if (myGraph.getEdgesCount() != myConjugateGraph.getEdgesCount()) {
      throw new GraphsInconsistencyException("Difference in edges' count");
    }
  }

  public Collection<V> getVertices() {
    checkGraphsCorrectness();
    return Collections.unmodifiableCollection(myGraph.getVertices());
  }

  public void add(V v) {
    if (getVertices().contains(v)) {
      LOG.debug("Already watching vertex " + v);
      return;
    }
    myGraph.addVertex(v);
    myConjugateGraph.addVertex(v);
  }


  /**
   * removes vertex with all its outs and ins
   * also updates its disposedDeps cache
   */
  public void remove(V v) {
    Collection<? extends V> outs = myGraph.getOuts(v);
    Collection<? extends V> backOuts = myConjugateGraph.getOuts(v);
    myGraph.removeVertex(v);
    myConjugateGraph.removeVertex(v);
    for (V v1 : outs) myConjugateGraph.removeEdge(v1, v);
    for (V v1 : backOuts) myGraph.removeEdge(v1, v);
  }

  public boolean addEdge(V v1, V v2) {
    boolean edgeAdded = myGraph.addEdge(v1, v2);
    myConjugateGraph.addEdge(v2, v1);
    return edgeAdded;
  }

  public boolean removeEdge(V v1, V v2) {
    boolean edgeRemoved = myGraph.removeEdge(v1, v2);
    myConjugateGraph.removeEdge(v2, v1);
    return edgeRemoved;
  }

  public Graph<V> getGraph() {
    return myGraph;
  }

  public Graph<V> getConjugateGraph() {
    return myConjugateGraph;
  }

  public boolean contains(V v) {
    checkGraphsCorrectness();
    return getVertices().contains(v);
  }

  public Collection<? extends V> getOutgoingEdges(V v) {
    checkGraphsCorrectness();
    return myGraph.getOuts(v);
  }

  // TODO : merge with jetbrains.mps.util.Graph (mps.util.Graph needs to be modified for a bit)
  static class Graph<V> {
    private final Map<V, Set<V>> myOuts = new LinkedHashMap<V, Set<V>>();
    private int myEdgesCount;

    public int getEdgesCount() {
      return myEdgesCount;
    }

    public int getVerticesCount() {
      return myOuts.keySet().size();
    }

    private boolean containsVertex(V v) {
      return myOuts.containsKey(v);
    }

    public boolean addVertex(V v) {
      if (containsVertex(v)) return false;
      myOuts.put(v, new LinkedHashSet<V>());
      return true;
    }

    public boolean removeVertex(V v) {
      Set<V> removed = myOuts.remove(v);
      if (removed != null) {
        myEdgesCount -= removed.size();
      }
      return removed != null;
    }

    public boolean addEdge(V v1, V v2) {
      if (!containsVertex(v1) || !containsVertex(v2)) {
        throw new IllegalArgumentException("Trying to add an edge between nonexistent vertices");
      }
      Collection<V> vs = myOuts.get(v1);
      assert vs != null;
      if (vs.add(v2)) {
        ++myEdgesCount;
        return true;
      }
      return false;
    }

    public boolean removeEdge(V v1, V v2) {
      Collection<V> vs = myOuts.get(v1);
      if (vs == null) return false;
      if (vs.remove(v2)) {
        --myEdgesCount;
        return true;
      }
      return false;
    }

    @NotNull
    public Collection<? extends V> getOuts(V v) {
      return myOuts.get(v);
    }

    public void dfs(Iterable<? extends V> starts, VertexVisitor<V> visitor) {
      new DfsTraversal<V>(this, starts, visitor).dfs();
    }

    public Collection<V> getVertices() {
      return myOuts.keySet();
    }

    private static class DfsTraversal<V> {
      private final Graph<V> myGraph;
      private final Set<V> myVisited = new HashSet<V>();
      private final Iterable<? extends V> myStartVs;
      private final VertexVisitor<V> myVisitor;

      public DfsTraversal(Graph<V> graph, Iterable<? extends V> startVs, VertexVisitor<V> visitor) {
        myGraph = graph;
        myStartVs = startVs;
        myVisitor = visitor;
      }

      public void dfs() {
        for (V v : myStartVs) {
          if (myVisited.contains(v)) continue;
          dfs0(v);
        }
      }

      private void dfs0(V v) {
        assert myGraph.containsVertex(v) : "Graph does not contain vertex " + v;
        myVisited.add(v);
        myVisitor.visit(v);
        for (V vOut : myGraph.getOuts(v)) {
          if (myVisited.contains(vOut)) continue;
          dfs0(vOut);
        }
      }
    }

    public interface VertexVisitor<V> {
      void visit(V v);
    }
  }

  private static class GraphsInconsistencyException extends RuntimeException {
    public GraphsInconsistencyException(String msg) {
      super(msg);
    }
  }
}
