/*
 * Copyright 2003-2017 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 jetbrains.mps.extapi.model.SModelBase;
import jetbrains.mps.extapi.model.SModelData;
import jetbrains.mps.project.dependency.ModelDependenciesManager;
import jetbrains.mps.project.structure.modules.ModuleReference;
import jetbrains.mps.smodel.event.SModelChildEvent;
import jetbrains.mps.smodel.event.SModelDevKitEvent;
import jetbrains.mps.smodel.event.SModelImportEvent;
import jetbrains.mps.smodel.event.SModelLanguageEvent;
import jetbrains.mps.smodel.event.SModelListener;
import jetbrains.mps.smodel.event.SModelPropertyEvent;
import jetbrains.mps.smodel.event.SModelReferenceEvent;
import jetbrains.mps.smodel.event.SModelRootEvent;
import jetbrains.mps.smodel.loading.UpdateModeSupport;
import jetbrains.mps.smodel.nodeidmap.INodeIdToNodeMap;
import jetbrains.mps.smodel.nodeidmap.UniversalOptimizedNodeIdMap;
import jetbrains.mps.util.annotation.ToRemove;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SContainmentLink;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.language.SProperty;
import org.jetbrains.mps.openapi.model.EditableSModel;
import org.jetbrains.mps.openapi.model.SModelId;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;

/**
 * An internal model implementation which (!) does not implement the {@link org.jetbrains.mps.openapi.model.SModel}.
 * Instead the SModel implementations such as {@link}
 */
public class SModel implements SModelData, UpdateModeSupport {
  private static final Logger LOG = LogManager.getLogger(SModel.class);
  private static AtomicLong ourCounter = new AtomicLong();

  static {
    resetIdCounter();
  }

  protected SModelBase myModelDescriptor;
  private Set<SNode> myRoots = new LinkedHashSet<SNode>();
  private SModelReference myReference;
  private boolean myDisposed;
  private List<SLanguage> myLanguagesEngagedOnGeneration = new ArrayList<>();
  private Map<SLanguage, Integer> myLanguagesIds = new LinkedHashMap<>();
  // FIXME introduce UniqueList or ArraySet to avoid "if (!myList.contains && myList.add())" on change
  //       for both myDevKits and myImports. There's intellij's ArrayListSet, but it's dull and introduces unnecessary dependency
  private List<SModuleReference> myDevKits = new ArrayList<>();
  private List<ImportElement> myImports = new ArrayList<>();
  private INodeIdToNodeMap myIdToNodeMap;
  private StackTraceElement[] myDisposedStacktrace = null;
  private ModelDependenciesManager myModelDependenciesManager;
  private ImplicitImportsLegacyHolder myLegacyImplicitImports;
  /**
   * update mode, aka full load mode, is the state we are attaching newly loaded children to a model loaded partially
   * since it could happen during model read, we can't rely on model read/write action mechanism.
   * It used to be a mere boolean flag, but it doesn't help to deal with multiple getNode(SNodeId) calls, one of which triggered model load.
   * Existing implementation used to hack around with synchronized access to model data (in overridden methods SModelBase#geSModelInternal(),
   * e.g. LazyEditableSModelBase#getSModelInternal synchronize on myModel:UpdatableModel field, and UpdatableModel does synchronize on this when
   * replacing model nodes in update mode). I could have used the same approach for my model, however, don't want to
   * (a) build my model on top of LazyEditableSModelBase
   * (b) keep this synchronized code (future work, though)
   *
   * Lock is not the best primitive to use here. With given API (setUpdateMode(boolean) I need smth like a semaphore I could check for being locked,
   * and wait for release (i.e. without further acquire) if necessary. Neither java's Semaphore, nor Lock are good for this, picked latter just
   * by tossing a coin (well, it respects the thread, its API is more streamlined with the activity, lock() sounds better than acquire())
   */
  private final ReentrantLock myFullLoadMode = new ReentrantLock();
  // nodes from this model communicate with it through this owner instance.
  @NotNull
  private final AttachedNodeOwner myNodeOwner;

  public SModel(@NotNull SModelReference modelReference) {
    this(modelReference, new UniversalOptimizedNodeIdMap());
  }

  public SModel(@NotNull SModelReference modelReference, INodeIdToNodeMap map) {
    myReference = modelReference;
    myIdToNodeMap = map;
    myNodeOwner = new AttachedNodeOwner(this);
  }

  static void resetIdCounter() {
    ourCounter.set(Math.abs(new SecureRandom().nextLong()));
  }

  public static SNodeId generateUniqueId() {
    long id = Math.abs(ourCounter.incrementAndGet());
    return new jetbrains.mps.smodel.SNodeId.Regular(id);
  }


  public SModelId getModelId() {
    return myReference.getModelId();
  }

  @NotNull
  public SModelReference getReference() {
    return myReference;
  }

  @Override
  public Iterable<org.jetbrains.mps.openapi.model.SNode> getRootNodes() {
    fireModelNodesReadAccess();
    return new Iterable<org.jetbrains.mps.openapi.model.SNode>() {
      @Override
      public Iterator<org.jetbrains.mps.openapi.model.SNode> iterator() {
        return new Iterator<org.jetbrains.mps.openapi.model.SNode>() {
          private final Iterator<SNode> myIterator = myRoots.iterator();

          @Override
          public boolean hasNext() {
            return myIterator.hasNext();
          }

          @Override
          public org.jetbrains.mps.openapi.model.SNode next() {
            SNode res = myIterator.next();
            if (res != null) {
              res.assertCanRead();
              res.getNodeOwner().fireNodeRead(res, true);
            }

            return res;
          }

          @Override
          public void remove() {
            throw new UnsupportedOperationException("can't change model roots through roots iterator");
          }
        };
      }
    };
  }

  public boolean isRoot(@Nullable org.jetbrains.mps.openapi.model.SNode node) {
    return myRoots.contains(node);
  }

  //--------------IMPLEMENTATION-------------------

  @Override
  public void addRootNode(final org.jetbrains.mps.openapi.model.SNode node) {
    assert node instanceof SNode;
    enforceFullLoad();
    if (myRoots.contains(node)) {
      // why not warn?
      return;
    }
    org.jetbrains.mps.openapi.model.SModel model = node.getModel();
    // FIXME why on earth we remove new root from original location, but don't do the same for insertChild?
    if (model != null && model != myModelDescriptor && node.getParent() == null) {
      model.removeRootNode(node);
    } else {
      org.jetbrains.mps.openapi.model.SNode parent = node.getParent();
      if (parent != null) {
        parent.removeChild(node);
      }
    }

    SNode sn = (SNode) node;
    myRoots.add(sn);
    sn.attach(myNodeOwner);
    myNodeOwner.performUndoableAction(node, new AddRootUndoableAction(node));
    myNodeOwner.fireNodeAdd(null, null, sn, null);
  }

  @Override
  public void removeRootNode(final org.jetbrains.mps.openapi.model.SNode node) {
    assert node instanceof SNode;
    enforceFullLoad();
    if (myRoots.contains(node)) {
      myNodeOwner.fireBeforeNodeRemove(null, null, (SNode) node, null);
      myRoots.remove(node);
      SNode sn = (SNode) node;
      sn.detach(new DetachedNodeOwner(this));
      myNodeOwner.performUndoableAction(node, new RemoveRootUndoableAction(node, myModelDescriptor));
      myNodeOwner.fireNodeRemove(null, null, sn, null);
    }
  }

  @Override
  @Nullable
  public SNode getNode(@NotNull org.jetbrains.mps.openapi.model.SNodeId nodeId) {
    SNode res = getNode_(nodeId);
    if (res != null) {
      res.assertCanRead();
      myNodeOwner.fireNodeRead(res, true);
    }
    return res;
  }


  protected final void waitUpdateModeIsOver() {
    if (isUpdateMode()) {
      // there's a thread that updates the model, and myIdToNodeMap might get updated
      // thus we shall wait once update is over, and lock()/unlock() pair merely ensures update mode if over.
      // This is not a decent solution, we'd rather get separate SModelData implementation that is update-aware, and use locks/guards
      // when accessing internal fields.
      // Note, if the current thread is the one that performs the update, lock()/unlock() is fine, as the lock is re-enterable
      myFullLoadMode.lock();
      myFullLoadMode.unlock();
    }
  }

  private SNode getNode_(org.jetbrains.mps.openapi.model.SNodeId nodeId) {
    checkNotDisposed();
    if (myDisposed) {
      return null;
    }

    waitUpdateModeIsOver();

    org.jetbrains.mps.openapi.model.SNode node = myIdToNodeMap.get(nodeId);
    if (node != null) {
      return ((SNode) node);
    }
    enforceFullLoad();
    return ((SNode) myIdToNodeMap.get(nodeId));
  }

  @NotNull
  public String toString() {
    return myReference.toString();
  }

  //todo get rid of, try to cast, show an error if not casted
  public boolean isDisposed() {
    return myDisposed;
  }

  //todo cast if can be
  public StackTraceElement[] getDisposedStacktrace() {
    return myDisposedStacktrace;
  }

  @Deprecated
  public void addModelListener(@NotNull SModelListener listener) {
    getModelDescriptor().addModelListener(listener);
  }

  @Deprecated
  public void removeModelListener(@NotNull SModelListener listener) {
    getModelDescriptor().removeModelListener(listener);
  }

  /**
   * FIXME it looks we can use openapi.SModel as return value now (i.e. not to restrain to SModelBase use)
   */
  public SModelBase getModelDescriptor() {
    return myModelDescriptor;
  }

  // FIXME (1) synchronized, really? (2) do we really care to have SModelBase here? (3) if yes, why it's not argument type?
  public synchronized void setModelDescriptor(org.jetbrains.mps.openapi.model.SModel modelDescriptor) {
    if (myModelDescriptor == modelDescriptor) {
      // just to guard against accidental second assignment
      return;
    }
    myModelDescriptor = ((SModelBase) modelDescriptor);
    myNodeOwner.setEventDispatch(modelDescriptor == null ? null : myModelDescriptor.getNodeEventDispatch());
  }

  protected void enforceFullLoad() {
    org.jetbrains.mps.openapi.model.SModel md = myModelDescriptor;
    if (md != null) {
      md.load();
    }
  }

  private void fireModelNodesReadAccess() {
    if (!canFireReadEvent()) return;
    if (myModelDescriptor != null) {
      NodeReadEventsCaster.fireModelNodesReadAccess(myModelDescriptor);
    }
  }

//---------listeners--------

  /**
   * Name clash with {@link SNodeOwner#performUndoableAction(org.jetbrains.mps.openapi.model.SNode, SNodeUndoableAction)} is unfortunate.
   * This one is rather 'registerActionWithUndo'.
   */
  protected void performUndoableAction(@NotNull SNodeUndoableAction action) {
    if (!canFireEvent()) {
      return;
    }
    UndoHelper.getInstance().addUndoableAction(action);
  }

    //todo code in the following methods should be written w/o duplication

  public boolean canFireEvent() {
    return myModelDescriptor != null && jetbrains.mps.util.SNodeOperations.isRegistered(myModelDescriptor) && !isUpdateMode();
  }

  public boolean canFireReadEvent() {
    return canFireEvent();
  }

  public void dispose() {
    if (myDisposed) {
      return;
    }

    myDisposed = true;
    myDisposedStacktrace = new Throwable().getStackTrace();
    myIdToNodeMap = null;
    myRoots.clear();
    if (myModelDependenciesManager != null) {
      myModelDependenciesManager.dispose();
      myModelDependenciesManager = null;
    }
  }

  private void checkNotDisposed() {
    if (myDisposed) {
      LOG.error(new IllegalModelAccessError("accessing disposed model"));
    }
  }

  private List<SModelListener> getModelListeners() {
    if (myModelDescriptor == null) {
      return Collections.emptyList();
    }
    return myModelDescriptor.getModelListeners();
  }

  private void fireDevKitAddedEvent(@NotNull SModuleReference ref) {
    if (!canFireEvent()) {
      return;
    }
    final SModelDevKitEvent event = new SModelDevKitEvent(getModelDescriptor(), ref, true);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.devkitAdded(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  private void fireDevKitRemovedEvent(@NotNull SModuleReference ref) {
    if (!canFireEvent()) {
      return;
    }
    final SModelDevKitEvent event = new SModelDevKitEvent(getModelDescriptor(), ref, false);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.devkitRemoved(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  private void fireLanguageAddedEvent(@NotNull SLanguage ref) {
    if (!canFireEvent()) {
      return;
    }
    final SModelLanguageEvent event = new SModelLanguageEvent(getModelDescriptor(), ref, true);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.languageAdded(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  private void fireLanguageRemovedEvent(@NotNull SLanguage ref) {
    if (!canFireEvent()) {
      return;
    }
    final SModelLanguageEvent event = new SModelLanguageEvent(getModelDescriptor(), ref, false);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.languageRemoved(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  private void fireImportAddedEvent(@NotNull SModelReference modelReference) {
    if (!canFireEvent()) return;
    final SModelImportEvent event = new SModelImportEvent(getModelDescriptor(), modelReference, true);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.importAdded(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  private void fireImportRemovedEvent(@NotNull SModelReference modelReference) {
    if (!canFireEvent()) return;
    final SModelImportEvent event = new SModelImportEvent(getModelDescriptor(), modelReference, false);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.importRemoved(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  /*package*/ void fireRootAddedEvent(@NotNull SNode root) {
    if (!canFireEvent()) {
      return;
    }
    final SModelRootEvent event = new SModelRootEvent(getModelDescriptor(), root, true);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.rootAdded(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  /*package*/ void fireRootRemovedEvent(@NotNull SNode root) {
    if (!canFireEvent()) {
      return;
    }
    final SModelRootEvent event = new SModelRootEvent(getModelDescriptor(), root, false);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.rootRemoved(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  /*package*/ void fireBeforeRootRemovedEvent(org.jetbrains.mps.openapi.model.SNode node) {
    if (!canFireEvent()) {
      return;
    }
    final SModelRootEvent event = new SModelRootEvent(getModelDescriptor(), node, false);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.beforeRootRemoved(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  void firePropertyChangedEvent(@NotNull SNode node, @NotNull SProperty property, @Nullable String oldValue, @Nullable String newValue) {
    if (!canFireEvent()) {
      return;
    }
    final SModelPropertyEvent event = new SModelPropertyEvent(getModelDescriptor(), property, node, oldValue, newValue);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.propertyChanged(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  void fireChildAddedEvent(@NotNull SNode parent, @NotNull SContainmentLink role, @NotNull org.jetbrains.mps.openapi.model.SNode child, org.jetbrains.mps.openapi.model.SNode anchor) {
    if (!canFireEvent()) {
      return;
    }
    int childIndex = anchor == null ? 0 : parent.getChildren().indexOf(anchor) + 1;
    final SModelChildEvent event = new SModelChildEvent(getModelDescriptor(), true, parent, role, childIndex, child);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.childAdded(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  void fireChildRemovedEvent(@NotNull SNode parent, @NotNull SContainmentLink role, @NotNull org.jetbrains.mps.openapi.model.SNode child, org.jetbrains.mps.openapi.model.SNode anchor) {
    if (!canFireEvent()) {
      return;
    }
    int childIndex = anchor == null ? 0 : parent.getChildren().indexOf(anchor) + 1;
    final SModelChildEvent event = new SModelChildEvent(getModelDescriptor(), false, parent, role, childIndex, child);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.childRemoved(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  void fireBeforeChildRemovedEvent(@NotNull SNode parent, @NotNull SContainmentLink role, @NotNull org.jetbrains.mps.openapi.model.SNode child, org.jetbrains.mps.openapi.model.SNode anchor) {
    if (!canFireEvent()) {
      return;
    }
    int childIndex = anchor == null ? 0 : parent.getChildren().indexOf(anchor) + 1;
    final SModelChildEvent event = new SModelChildEvent(getModelDescriptor(), false, parent, role, childIndex, child);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.beforeChildRemoved(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  void fireReferenceAddedEvent(@NotNull SReference reference) {
    if (!canFireEvent()) {
      return;
    }
    final SModelReferenceEvent event = new SModelReferenceEvent(getModelDescriptor(), reference, true);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.referenceAdded(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  void fireReferenceRemovedEvent(@NotNull SReference reference) {
    if (!canFireEvent()) {
      return;
    }
    final SModelReferenceEvent event = new SModelReferenceEvent(getModelDescriptor(), reference, false);
    for (SModelListener sModelListener : getModelListeners()) {
      try {
        sModelListener.referenceRemoved(event);
      } catch (Throwable t) {
        LOG.error(null, t);
      }
    }
  }

  public FastNodeFinder createFastNodeFinder() {
    return new DefaultFastNodeFinder(getModelDescriptor());
  }

  //---------node registration--------

  void registerNode(@NotNull SNode node) {
    checkNotDisposed();
    if (myDisposed) return;

    enforceFullLoad(); // FIXME dubious need to perform full load if all we do is populating id map
    org.jetbrains.mps.openapi.model.SNodeId id = node.getNodeId();
    if (id == null) {
      assignNewId(node);
      return;
    }

    org.jetbrains.mps.openapi.model.SNode existingNode = myIdToNodeMap.get(id);
    if (existingNode == null) {
      myIdToNodeMap.put(node.getNodeId(), node);
    }

    if (existingNode != null && existingNode != node) {
      assignNewId(node);
    }
  }

  private void assignNewId(SNode node) {
    SNodeId id;
    id = generateUniqueId();
    while (myIdToNodeMap.containsKey(id)) {
      resetIdCounter();
      id = generateUniqueId();
    }
    node.setId(id);
    myIdToNodeMap.put(id, node);
  }

  //---------imports manipulation--------

  void unregisterNode(@NotNull SNode node) {
    checkNotDisposed();

    enforceFullLoad(); // FIXME see registerNode. What's the purpose?
    org.jetbrains.mps.openapi.model.SNodeId id = node.getNodeId();
    if (myDisposed || id == null) return;
    myIdToNodeMap.remove(id);
  }

  public ModelDependenciesManager getModelDepsManager() {
    if (myModelDependenciesManager == null) {
      myModelDependenciesManager = new ModelDependenciesManager(getModelDescriptor());
      // we do not need to track model changes as we are invalidating dep manager right away on any change
      SRepository repo = getRepository();
      if (repo != null) {
        myModelDependenciesManager.trackRepositoryChanges(repo);
      }
    }
    return myModelDependenciesManager;
  }

  private void invalidateModelDepsManager() {
    if (myModelDependenciesManager != null) {
      myModelDependenciesManager.invalidate();
    }
  }

  //language

  public Collection<SLanguage> usedLanguages() {
    return Collections.unmodifiableSet(myLanguagesIds.keySet());
  }

  public int getLanguageImportVersion(SLanguage lang) {
    Integer res = myLanguagesIds.get(lang);
    if (res == null) {
      LOG.error("Model " + getModelDescriptor().getModelName() + ": version for language " + lang.getQualifiedName() + " not found. Using last version instead.");
      return lang.getLanguageVersion();
    }
    return res;
  }

  public void deleteLanguage(@NotNull SLanguage id) {
    if (myLanguagesIds.remove(id) != null) {
      invalidateModelDepsManager();
      fireLanguageRemovedEvent(id);
      markChanged();
    }
  }

  public void addLanguage(@NotNull SLanguage language) {
    // FIXME where to take version value to put into myLanguagesIds if not from deprecated method???
    final int version = language.getLanguageVersion();
    Integer existingVersion = myLanguagesIds.get(language);
    if (existingVersion != null) {
      if (version == -1 || existingVersion == version) {
        return;
      }
      if (existingVersion != -1) {
        throw new IllegalStateException("Can't add language import with different version. Old version: " + existingVersion + "; new version: " + version + "; model: " + getModelDescriptor().getModelName() + "; language: " + language.getQualifiedName());
      }
    }

    setLanguageVersionInternal(language, version);
  }

  public void setLanguageImportVersion(SLanguage language, int version) {
    if (!myLanguagesIds.containsKey(language)) {
      throw new IllegalStateException("Can't change version for non-existing language import. Model: " + getModelDescriptor() + "; language: " + language);
    }

    setLanguageVersionInternal(language, version);
  }

  private void setLanguageVersionInternal(SLanguage language, int version) {
    myLanguagesIds.put(language, version);
    invalidateModelDepsManager();
    fireLanguageAddedEvent(language);
    markChanged();
  }

  //devkit

  public List<SModuleReference> importedDevkits() {
    return Collections.unmodifiableList(myDevKits);
  }

  public void addDevKit(SModuleReference ref) {
    if (!myDevKits.contains(ref) && myDevKits.add(ref)) {
      invalidateModelDepsManager();
      fireDevKitAddedEvent(ref);
      markChanged();
    }
  }

  public void deleteDevKit(@NotNull SModuleReference ref) {
    if (myDevKits.remove(ref)) {
      invalidateModelDepsManager();
      fireDevKitRemovedEvent(ref);
      markChanged();
    }
  }

  //model

  public List<ImportElement> importedModels() {
    return Collections.unmodifiableList(myImports);
  }

  public void addModelImport(ImportElement importElement) {
    // myImports is ArrayList, AL.add() doesn't check for presence.
    if (!myImports.contains(importElement) && myImports.add(importElement)) {
      fireImportAddedEvent(importElement.getModelReference());
      markChanged();
    }
  }

  public void deleteModelImport(ImportElement importElement) {
    if (myImports.remove(importElement)) {
      if (myLegacyImplicitImports != null) {
        // shall keep only if we do track implicit imports
        myLegacyImplicitImports.addAdditionalModelVersion(importElement);  // to save version and ID if model was imported implicitly
      }
      fireImportRemovedEvent(importElement.getModelReference());
      markChanged();
    }
  }

  /**
   * This is compatibility method with legacy persistence mechanism, unless used, no implicit imports are tracked.
   * Drop once we no longer need to support serialization of old persistence formats (there's no reason to track
   * implicit imports if we aren't going to serialize them afterwards)
   *
   * It looks that there's no longer consumer of implicit imports. There's code to update them, but no code to read values, except
   * for clients of #getAllImportElements()
   */
  @NotNull
  @ToRemove(version = 3.4)
  public ImplicitImportsLegacyHolder getImplicitImportsSupport() {
    if (myLegacyImplicitImports == null) {
      myLegacyImplicitImports = new ImplicitImportsLegacyHolder(this);
    }
    return myLegacyImplicitImports;
  }

  /**
   * @deprecated though it's our internal API, there's 1 use in mbeddr of this exact method we need to fix first.
   *             Once mbeddr use and 2 uses in our model persistence gone, remove the method
   */
  @Deprecated
  @ToRemove(version = 0)
  public List<SModuleReference> engagedOnGenerationLanguages() {
    return new SModelLegacy(this).engagedOnGenerationLanguages();
  }

  private void markChanged() {
    if (myModelDescriptor == null) return;
    org.jetbrains.mps.openapi.model.SModel model = getModelDescriptor();
    if (model instanceof EditableSModel) {
      ((EditableSModel) model).setChanged(true);
    }
  }

  public Collection<SLanguage> getLanguagesEngagedOnGeneration() {
    return myLanguagesEngagedOnGeneration;
  }

  public void addEngagedOnGenerationLanguage(SLanguage ref) {
    if (!myLanguagesEngagedOnGeneration.contains(ref)) {
      myLanguagesEngagedOnGeneration.add(ref);
      // don't send event but mark model as changed
      if (canFireEvent()) {
        markChanged();
      }
    }
  }

  public void removeEngagedOnGenerationLanguage(SLanguage ref) {
    if (myLanguagesEngagedOnGeneration.contains(ref)) {
      myLanguagesEngagedOnGeneration.remove(ref);
      // don't send event but mark model as changed
      if (canFireEvent()) {
        markChanged();
      }
    }
  }

  //aspects / additional

  /**
   * update mode means we are attaching newly loaded children
   */
  public boolean isUpdateMode() {
    return myFullLoadMode.isLocked();
  }

  @Override
  public void enterUpdateMode() {
    myFullLoadMode.lock();
  }

  @Override
  public void leaveUpdateMode() {
    myFullLoadMode.unlock();
  }

  //to use only from SNode
  protected SRepository getRepository() {
    return myModelDescriptor == null ? null : myModelDescriptor.getRepository();
  }

  //---------refactorings--------

  // To allow update of models not yet attached to a repo (e.g. when we read a model and are going to attach it in with
  // refreshed state, rather than attach first and then refresh), we pass repository from outside rather than using this.getRepository()
  public boolean updateExternalReferences(@NotNull SRepository repository) {
    enforceFullLoad();

    boolean changed = false;
    for (org.jetbrains.mps.openapi.model.SNode node : myIdToNodeMap.values()) {
      for (SReference reference : node.getReferences()) {
        SModelReference oldReference = reference.getTargetSModelReference();
        if (oldReference == null || !(reference instanceof SReferenceBase)) {
          continue;
        }
        // there's RefUpdateUtil.updateModelRef() that could have been used here, it it was in [smodel].
        // But it's in [project] now, and needs refactoring to relocate.
        final org.jetbrains.mps.openapi.model.SModel resolved = oldReference.resolve(repository);
        if (resolved != null && jetbrains.mps.smodel.SModelReference.differs(resolved.getReference(), oldReference)) {
          changed = true;
          ((SReferenceBase) reference).setTargetSModelReference(resolved.getReference());
        }
      }
    }

    for (ImportElement e : myImports) {
      final org.jetbrains.mps.openapi.model.SModel resolved = e.getModelReference().resolve(repository);
      if (resolved != null && jetbrains.mps.smodel.SModelReference.differs(resolved.getReference(), e.getModelReference())) {
        changed = true;
        e.myModelReference = resolved.getReference();
      }
    }

    if (updateRefs(myDevKits)) {
      changed = true;
    }
    return changed;
  }

  public void changeModelReference(SModelReference newModelReference) {
    enforceFullLoad();
    SModelReference oldReference = myReference;
    myReference = newModelReference;
    for (org.jetbrains.mps.openapi.model.SNode node : myIdToNodeMap.values()) {
      for (SReference reference : node.getReferences()) {
        if (oldReference.equals(reference.getTargetSModelReference())) {
          ((jetbrains.mps.smodel.SReference) reference).setTargetSModelReference(newModelReference);
        }
      }
    }
  }

  private boolean updateRefs(List<SModuleReference> refs) {
    boolean changed = false;
    for (int i = 0; i < refs.size(); i++) {
      SModuleReference ref = refs.get(i);
      SModule module = ModuleRepositoryFacade.getInstance().getModule(ref);
      if (module != null) {
        SModuleReference newRef = module.getModuleReference();
        refs.set(i, newRef);
        changed = changed || ModuleReference.differs(ref, newRef);
      }
    }
    return changed;
  }

  public SModel createEmptyCopy() {
    return new jetbrains.mps.smodel.SModel(getReference());
  }

  public void copyPropertiesTo(SModel to) {

    if (myLegacyImplicitImports != null) {
      for (ImportElement ie : myLegacyImplicitImports.getAdditionalModelVersions()) {
        to.getImplicitImportsSupport().addAdditionalModelVersion(ie.copy());
      }
    }
    for (ImportElement ie : importedModels()) {
      to.addModelImport(ie.copy());
    }
    for (SModuleReference mr : importedDevkits()) {
      to.addDevKit(mr);
    }
    for (SLanguage lang : usedLanguages()) {
      to.addLanguage(lang);
    }
    for (SLanguage mr : getLanguagesEngagedOnGeneration()) {
      to.addEngagedOnGenerationLanguage(mr);
    }
  }

  public static final class ImportElement {
    private SModelReference myModelReference;
    private int myReferenceID;  // persistence related index
    private int myUsedVersion;

    @Deprecated
    public ImportElement(SModelReference modelReference, int referenceID) {
      this(modelReference, referenceID, -1);
    }

    @Deprecated
    public ImportElement(SModelReference modelReference, int referenceID, int usedVersion) {
      myModelReference = modelReference;
      myReferenceID = referenceID;
      myUsedVersion = usedVersion;
    }

    public ImportElement(SModelReference modelReference) {
      myModelReference = modelReference;
      myReferenceID = 0;
      myUsedVersion = -1;
    }

    public SModelReference getModelReference() {
      return myModelReference;
    }

    public void setModelReference(SModelReference modelReference) {
      myModelReference = modelReference;
    }

    public int getReferenceID() {
      return myReferenceID;
    }

    public void setReferenceID(int referenceID) {
      myReferenceID = referenceID;
    }

    public int getUsedVersion() {
      return myUsedVersion;
    }

    protected ImportElement copy() {
      return new ImportElement(myModelReference, myReferenceID, myUsedVersion);
    }

    public String toString() {
      return "ImportElement(" +
          "uid=" + myModelReference + ", " +
          "referenceId=" + myReferenceID + ", " +
          "usedVersion=" + myUsedVersion + ")";
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) return true;
      if (o == null || getClass() != o.getClass()) return false;

      ImportElement that = (ImportElement) o;

      if (myReferenceID != that.myReferenceID) return false;
      if (myUsedVersion != that.myUsedVersion) return false;
      if (myModelReference != null ? !myModelReference.equals(that.myModelReference) : that.myModelReference != null)
        return false;

      return true;
    }

    @Override
    public int hashCode() {
      int result = myModelReference != null ? myModelReference.hashCode() : 0;
      result = 31 * result + myReferenceID;
      result = 31 * result + myUsedVersion;
      return result;
    }
  }

}
