/*
 * Copyright 2003-2016 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.java.stub;

import jetbrains.mps.project.dependency.GlobalModuleDependenciesManager;
import jetbrains.mps.project.dependency.GlobalModuleDependenciesManager.Deptype;
import jetbrains.mps.smodel.DynamicReference;
import jetbrains.mps.smodel.SModelStereotype;
import jetbrains.mps.util.IterableUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SReferenceLink;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelName;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.SModule;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Look up models with same name among all visible models for a given module.
 * Has nothing to do with stubs except for the fact its only use is in java stub resolution mechanism.
 * @author Artem Tikhomirov
 */
public final class StubReferenceFactory implements ReferenceFactory {
  private final SModule myModule;
  private final VisibleModel myModel;
  private final String myModelLongName;
  private final SModelReference myModelReference;

  // 1. we used to keep this cache separately, in StubModelsResolver, which might be better approach
  // if we decide to re-use this cache throughout all models loaded within a module. We didn't use this cache,
  // and created a new one for each SReferenceCreator, and it didn't cause any performance issue, thus moved into single class and not reused.
  // 2. We wrap SModel into VisibleModel to keep set of known model roots along with the model, to avoid attempt to load model again and again from model.getNode()
  private final Map<SModelName, List<VisibleModel>> myName2Models = new HashMap<SModelName, List<VisibleModel>>();

  private final Set<SModelReference> myModelImports = new HashSet<SModelReference>();

  /**
   * @param module module we try to resolve references in, provides dependencies
   * @param model model we try to resolve references in, ensures priority of local nodes over those from dependencies
   */
  public StubReferenceFactory(@NotNull SModule module, @NotNull SModel model) {
    myModule = module;
    myModel = new VisibleModel(model);
    myModelReference = model.getReference();
    myModelLongName = model.getName().getLongName();
  }

  @NotNull
  @Override
  public SReference create(SNode source, String pack, SNodeId targetNodeId, SReferenceLink role, String resolveInfo, SNodeId targetTopClassifier) {
    if (pack.equals(myModelLongName)) {
      if (myModel.isKnownRoot(targetTopClassifier)) {
        return jetbrains.mps.smodel.SReference.create(role, source, myModelReference, targetNodeId, resolveInfo);
      }
    }

    Collection<VisibleModel> possibleModels = findModels(new SModelName(pack, SModelStereotype.JAVA_STUB));

    if (possibleModels.isEmpty()) {
      return jetbrains.mps.smodel.SReference.create(role, source, null, targetNodeId, resolveInfo);
    }

    // first, try to find match

    for (VisibleModel vm : possibleModels) {
      final SModelReference modelRef = vm.getModelReference();
      if (myModelReference.equals(modelRef)) {
        continue;
      }
      if (vm.isKnownRoot(targetTopClassifier)) {
        addImport(modelRef);
        return jetbrains.mps.smodel.SReference.create(role, source, modelRef, targetNodeId, resolveInfo);
      }
    }

    // ok, there are matching models, and none knows the node with targetNodeId
    if (possibleModels.size() == 1) {
      // only one possible model
      SModelReference targetModel = possibleModels.iterator().next().getModelReference();
      addImport(targetModel);

      return jetbrains.mps.smodel.SReference.create(role, source, targetModel, targetNodeId, resolveInfo);
    } else {
      // XXX not quite sure if dynamic reference is reasonable here
      // anyway, this is the way it was
      for (VisibleModel m : possibleModels) {
        addImport(m.getModelReference());
      }
      return DynamicReference.createDynamicReference(role, source, pack, resolveInfo);
    }

  }

  private void addImport(SModelReference mr) {
    myModelImports.add(mr);
  }

  public Collection<SModelReference> getImports() {
    myModelImports.remove(myModelReference); // just in case it's there
    return myModelImports;
  }

  /**
   * FIXME use SModelId, once have switched to package id without module id (now need module reference to build stub ModuleId)
   * Also, shall use myModule.resolveInDependencies() then, to keep GMDM knowledge private to module implementation
   * @param modelName qualified name including stereotype (if any), not <code>null</code>
   * @return ordered collection, first come local matches, if any; never <code>null</code>
   */
  private List<VisibleModel> findModels(SModelName modelName) {
    if (myName2Models.isEmpty()) {
      ensureInitialized();
    }
    final List<VisibleModel> rv = myName2Models.get(modelName);
    return rv == null ? Collections.<VisibleModel>emptyList() : Collections.unmodifiableList(rv);
  }


  private void ensureInitialized() {
    LinkedHashSet<SModel> visibleModels = new LinkedHashSet<SModel>();
    // local models get precedence over those from imports
    visibleModels.addAll(IterableUtil.asCollection(myModule.getModels()));
    for (SModule visibleModule : new GlobalModuleDependenciesManager(myModule).getModules(Deptype.VISIBLE)) {
      visibleModels.addAll(IterableUtil.asCollection(visibleModule.getModels()));
    }

    for (SModel model : visibleModels) {
      final SModelName modelName = model.getName();
      List<VisibleModel> modelsFromCache = myName2Models.get(modelName);
      if (modelsFromCache == null) {
        myName2Models.put(modelName, modelsFromCache = new ArrayList<VisibleModel>(3));
      }
      modelsFromCache.add(new VisibleModel(model));
    }
  }

  /**
   * In addition to model, keep its root ids, to avoid deadlock when
   * two models in parallel reads simultaneously are in update model and resolve references to each other with getNode().
   * Since all we need is to check presence of top classifier, and not interested in full model load, we just cache
   * known classifiers and proceed gracefully if no match found.
   * <p/>
   * Indeed, this is sort of a hack, were we rely on hidden knowledge java stub models are loaded (or could be safely loaded
   * under given state) at least to the level of top classifiers.
   */
  private static class VisibleModel {
    private final SModel myModel;
    private final Set<SNodeId> myKnownRoots;

    public VisibleModel(SModel model) {
      myModel = model;
      myKnownRoots = new HashSet<SNodeId>();
      for (SNode n : model.getRootNodes()) {
        myKnownRoots.add(n.getNodeId());
      }
    }

    public boolean isKnownRoot(SNodeId nodeId) {
      return myKnownRoots.contains(nodeId);
    }

    public SModelReference getModelReference() {
      return myModel.getReference();
    }
  }
}
