/*
 * 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.smodel;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.model.SReference;

import java.util.HashSet;
import java.util.Set;

/**
 * Facility to collect actual external dependencies, like languages, concepts, and other models for a given model or set of nodes.
 * Walks a model and gathers information about nodes according to configuration.
 * By default, collects languages in use and references to external models.
 *
 * @author Artem Tikhomirov
 */
public final class ModelDependencyScanner {

  private final Set<SLanguage> myUsedLanguages;
  private final Set<org.jetbrains.mps.openapi.model.SModelReference> myCrossModelReferences;
  private final Set<SConcept> myConcepts;
  private boolean myNeedLanguages = true;
  private boolean myNeedCrossModel = true;
  private boolean myNeedConcepts = false;

  public ModelDependencyScanner() {
    myUsedLanguages = new HashSet<SLanguage>();
    myCrossModelReferences = new HashSet<org.jetbrains.mps.openapi.model.SModelReference>();
    myConcepts = new HashSet<SConcept>();
  }

  /**
   * Default: <code>true</code>
   */
  public ModelDependencyScanner usedLanguages(boolean collectUsedLanguages) {
    myNeedLanguages = collectUsedLanguages;
    return this;
  }

  /**
   * Default: <code>true</code>
   */
  public ModelDependencyScanner crossModelReferences(boolean crossModelReferences) {
    myNeedCrossModel = crossModelReferences;
    return this;
  }

  /**
   * Default: <code>false</code>
   */
  public ModelDependencyScanner usedConcepts(boolean collectConcepts) {
    myNeedConcepts = collectConcepts;
    return this;
  }

  @NotNull
  public Set<SLanguage> getUsedLanguages() {
    return myUsedLanguages;
  }

  @NotNull
  public Set<org.jetbrains.mps.openapi.model.SModelReference> getCrossModelReferences() {
    return myCrossModelReferences;
  }

  @NotNull
  public Set<SConcept> getConcepts() {
    return myConcepts;
  }

  /**
   * Iterate over all nodes of the given model
   * @return <code>this</code> for convenience
   */
  public ModelDependencyScanner walk(@NotNull SModel model) {
    return walk(SNodeUtil.getDescendants(model));
  }

  /**
   * Iterate over given nodes.
   * IMPORTANT! This method doesn't look into children of supplied nodes, it takes parameter 'literally' and visits
   * only nodes specified. To visit complete tree, consider using {@link SNodeUtil#getDescendants(SNode)}.
   *
   * @return <code>this</code> for convenience
   */
  public ModelDependencyScanner walk(@NotNull Iterable<org.jetbrains.mps.openapi.model.SNode> nodes) {
    HashSet<org.jetbrains.mps.openapi.model.SModelReference> allRefTargets = new HashSet<org.jetbrains.mps.openapi.model.SModelReference>();
    // collection of input nodes is not restricted to come from a single model,
    // hence we track models of nodes we iterate through to exclude them later from the set of cross-model.
    HashSet<org.jetbrains.mps.openapi.model.SModelReference> sourceModels = new HashSet<org.jetbrains.mps.openapi.model.SModelReference>();
    for (SNode n : nodes) {
      if (myNeedConcepts) {
        myConcepts.add(n.getConcept());
      }
      if (myNeedLanguages) {
        myUsedLanguages.add(n.getConcept().getLanguage());
      }
      if (myNeedCrossModel) {
        final org.jetbrains.mps.openapi.model.SModel sourceModel = n.getModel();
        if (sourceModel != null) {
          sourceModels.add(sourceModel.getReference());
        }
        for (SReference ref : n.getReferences()) {
          final org.jetbrains.mps.openapi.model.SModelReference targetModel = ref.getTargetSModelReference();
          if (targetModel != null) {
            allRefTargets.add(targetModel);
          }
        }
      }
    }
    if (myNeedCrossModel) {
      allRefTargets.removeAll(sourceModels);
      myCrossModelReferences.addAll(allRefTargets);
    }
    return this;
  }
}
