package jetbrains.mps.vcs.changesmanager.tree;

/*Generated by MPS */

import jetbrains.mps.annotations.GeneratedClass;
import jetbrains.mps.logging.Logger;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import com.intellij.util.containers.BidirectionalMap;
import jetbrains.mps.vcs.changesmanager.tree.features.Feature;
import jetbrains.mps.vcs.diff.changes.ModelChange;
import java.util.Map;
import java.util.HashMap;
import org.jetbrains.annotations.NotNull;
import jetbrains.mps.vcs.changesmanager.tree.features.AbstractNodeFeature;
import java.util.function.BiFunction;
import java.util.Collection;
import java.util.Set;
import java.util.HashSet;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable;
import jetbrains.mps.vcs.diff.changes.ChangeType;

/**
 * This is a map from 'Feature'to model change.
 * Serves as a source of vcs change for different trees vcs highlighting (project tree, usages tree, etc.)
 */
@GeneratedClass(node = "r:0fdcfe58-6a3e-4b7d-bea2-685e5d104fd0(jetbrains.mps.vcs.changesmanager.tree)/3751911615136891847", model = "r:0fdcfe58-6a3e-4b7d-bea2-685e5d104fd0(jetbrains.mps.vcs.changesmanager.tree)")
public class FeaturesFromVcs {
  private static final Logger LOG = Logger.getLogger(FeaturesFromVcs.class);
  private static boolean ourLoggingEnabled;
  private final List<FeatureForestMapListener> myListeners = new CopyOnWriteArrayList<FeatureForestMapListener>();
  private final BidirectionalMap<Feature, ModelChange> myFeature2Change = new BidirectionalMap();
  private final Map<Feature, FeatureWithParent> myFeature2FWP = new HashMap<Feature, FeatureWithParent>();
  private final Map<FeatureWithParent, Integer> myFWP2RefCount = new HashMap<FeatureWithParent, Integer>();

  public FeaturesFromVcs() {
  }

  public void put(@NotNull List<Feature> features, @NotNull ModelChange change) {
    for (Feature feature : features) {
      if (myFeature2Change.containsKey(feature)) {
        log("Trying to put already present feature " + feature);
      }
      calcParentAndRecord(feature);
      myFeature2Change.put(feature, change);
    }
  }

  private FeatureWithParent calcParentAndRecord(@NotNull Feature feature) {
    FeatureWithParent result;
    if (myFeature2FWP.containsKey(feature)) {
      result = myFeature2FWP.get(feature);
      incRefCountsUpwards(result);
    } else {
      Feature parent = null;
      if (feature instanceof AbstractNodeFeature) {
        AbstractNodeFeature nodeFeature = (AbstractNodeFeature) feature;
        parent = nodeFeature.getParent();
      }
      FeatureWithParent parentWP = (parent != null ? calcParentAndRecord(parent) : null);
      result = new FeatureWithParent(feature, parentWP);
      myFeature2FWP.put(feature, result);
      incRefCounts(result);
    }
    return result;
  }

  private void incRefCountsUpwards(FeatureWithParent fwp) {
    while (fwp != null) {
      incRefCounts(fwp);
      fwp = fwp.getParent();
    }
  }

  private void incRefCounts(FeatureWithParent fwp) {
    if (myFeature2Change.containsKey(fwp.getFeature())) {
      assert myFWP2RefCount.get(fwp) > 0;
    }
    myFWP2RefCount.merge(fwp, 1, new BiFunction<Integer, Integer, Integer>() {
      @Override
      public Integer apply(Integer p1, Integer p2) {
        return p1 + p2;
      }
    });
  }

  private void decRefCountsUpwardsAndRemoveIfAny(FeatureWithParent fwp) {
    while (fwp != null) {
      boolean error = checkRefCountForConsistency(fwp);
      if (error) {
        break;
      }
      assert myFWP2RefCount.containsKey(fwp);
      myFWP2RefCount.merge(fwp, -1, new BiFunction<Integer, Integer, Integer>() {
        @Override
        public Integer apply(Integer p1, Integer p2) {
          return p1 + p2;
        }
      });
      if (myFWP2RefCount.get(fwp) <= 0) {
        if (myFeature2Change.containsKey(fwp.getFeature())) {
          if (LOG.isErrorLevel()) {
            LOG.error("This must not happen with " + fwp, new IllegalStateException());
          }
        } else {
          myFWP2RefCount.remove(fwp);
          myFeature2FWP.remove(fwp.getFeature(), fwp);
        }
      }
      fwp = fwp.getParent();
    }
  }

  private boolean checkRefCountForConsistency(FeatureWithParent fwp) {
    boolean error = false;
    if (myFWP2RefCount.containsKey(fwp)) {
      if (myFWP2RefCount.get(fwp) < 1) {
        if (LOG.isErrorLevel()) {
          LOG.error("Ref count for " + fwp + " is less than one", new IllegalStateException());
        }
        error = true;
      }
    } else {
      if (LOG.isErrorLevel()) {
        LOG.error("No key " + fwp + " was found in the map", new IllegalStateException());
      }
      error = true;
    }
    return error;
  }

  private void remove(@NotNull Feature feature, ModelChange change) {
    if (!(myFeature2Change.containsKey(feature))) {
      log("Trying to remove feature which is not added: " + feature);
    }
    if (!(myFeature2Change.get(feature).equals(change))) {
      log("The value for " + feature + " is " + myFeature2Change.get(feature) + ", while we expect " + change);
    }
    FeatureWithParent fwp = myFeature2FWP.get(feature);
    myFeature2Change.remove(feature, change);
    decRefCountsUpwardsAndRemoveIfAny(fwp);
  }

  public void removeChange(ModelChange change) {
    if (!(myFeature2Change.containsValue(change))) {
      log("Trying to remove model change which is not there: " + change);
    }
    List<Feature> keysByValue = myFeature2Change.getKeysByValue(change);
    if (keysByValue == null) {
      return;
    }
    for (Feature f : List.copyOf(keysByValue)) {
      remove(f, change);
    }
  }

  public void removeAll(Collection<ModelChange> changes) {
    for (ModelChange change : changes) {
      removeChange(change);
    }
  }

  public Set<Feature> withAncestors(Set<Feature> features) {
    Set<FeatureWithParent> result = new HashSet<FeatureWithParent>();
    for (Feature f : features) {
      FeatureWithParent fwp = myFeature2FWP.get(f);
      while (fwp != null && result.add(fwp)) {
        fwp = fwp.getParent();
      }
    }
    return (Set<Feature>) result.stream().map(new Function<FeatureWithParent, Feature>() {
      @Override
      public Feature apply(FeatureWithParent fwp) {
        return fwp.getFeature();
      }
    }).collect(Collectors.toSet());
  }

  public Set<Feature> getFeatures(List<ModelChange> changes) {
    return (Set<Feature>) changes.stream().flatMap(new Function<ModelChange, Stream<Feature>>() {
      @Override
      public Stream<Feature> apply(ModelChange change) {
        List<Feature> featuresForChange = myFeature2Change.getKeysByValue(change);
        if (featuresForChange == null) {
          return Stream.empty();
        }
        return featuresForChange.stream();
      }
    }).collect(Collectors.toSet());
  }

  @Nullable
  public ChangeType getChangeType(@NotNull Feature feature) {
    ModelChange change = myFeature2Change.get(feature);
    if (change == null) {
      if (myFeature2FWP.containsKey(feature)) {
        return ChangeType.CHANGE;
      }
      return null;
    }
    return change.getType();
  }

  public void addListener(@NotNull FeatureForestMapListener listener) {
    myListeners.add(listener);
  }

  public void removeListener(@NotNull FeatureForestMapListener listener) {
    myListeners.remove(listener);
  }

  public void fireFeaturesAdded(@NotNull final Collection<Feature> features) {
    for (final FeatureForestMapListener listener : myListeners) {
      strangeTryCatch(new Runnable() {
        @Override
        public void run() {
          listener.featuresAdded(features);
        }
      });
    }
  }

  public void fireFeaturesRemoved(@NotNull final Collection<Feature> features) {
    for (final FeatureForestMapListener listener : myListeners) {
      strangeTryCatch(new Runnable() {
        @Override
        public void run() {
          listener.featuresRemoving(features);
        }
      });
    }
  }

  private void strangeTryCatch(@NotNull Runnable runnable) {
    try {
      runnable.run();
    } catch (Throwable t) {
      if (t instanceof InterruptedException || getCauseSafe(t) instanceof InterruptedException) {
        // ignore
      } else {
        if (LOG.isErrorLevel()) {
          LOG.error("Exception on firing featureStateChanged event", t);
        }
      }
    }
  }

  public static void setLoggingEnabled(boolean loggingEnabled) {
    ourLoggingEnabled = loggingEnabled;
  }

  private static void log(String msg) {
    if (ourLoggingEnabled) {
      if (LOG.isErrorLevel()) {
        LOG.error(msg);
      }
    }
  }

  private static Throwable getCauseSafe(Throwable checkedDotOperand) {
    if (null != checkedDotOperand) {
      return checkedDotOperand.getCause();
    }
    return null;
  }

  @Nullable
  public FeatureWithParent resolveParentInfo(Feature f) {
    return myFeature2FWP.get(f);
  }
}
