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

import jetbrains.mps.extapi.module.SModuleBase;
import jetbrains.mps.smodel.IllegalModelAccessException;
import jetbrains.mps.smodel.InvalidSModel;
import jetbrains.mps.smodel.MPSModuleRepository;
import jetbrains.mps.smodel.event.ModelEventDispatch;
import jetbrains.mps.smodel.event.ModelListenerDispatch;
import jetbrains.mps.smodel.loading.ModelLoadingState;
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.SConcept;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelAccessListener;
import org.jetbrains.mps.openapi.model.SModelId;
import org.jetbrains.mps.openapi.model.SModelListener;
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.SNodeAccessListener;
import org.jetbrains.mps.openapi.model.SNodeChangeListener;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.persistence.DataSource;
import org.jetbrains.mps.openapi.persistence.ModelRoot;

import java.util.Collections;

/**
 * Base implementation of {@link org.jetbrains.mps.openapi.model.SModel}, with actual
 * {@link jetbrains.mps.extapi.model.SModelData model data} kept separately, ready for e.g. re-load.
 *
 * This implementation tracks load state of the model data ({@link #getLoadingState()}) and expects
 * subclasses to {@link #setLoadingState(ModelLoadingState) update} this state appropriately.
 *
 * {@link #getModelData()} provides access to actual node storage.
 *
 * TODO relocate to [smodel]
 */
public abstract class SModelBase extends SModelDescriptorStub implements SModel {
  private static final Logger LOG = LogManager.getLogger(SModelBase.class);

  private final ModelEventDispatch myNodeEventDispatch;
  // XXX when necessary, shall get exposed with protected accessor. fire* methods kept for now as some of them do delegation to legacy
  // listeners as well, could get removed once smodel.SModelListener gone. Besides, I'm not yet sure multi-cast approach of
  // ModelListenerDispatch shall prevail, to limit future changes, let fire* method stay non deprecated for now.
  private final ModelListenerDispatch myModelEventDispatch;

  @NotNull private final DataSource mySource;
  @NotNull private SModelReference myModelReference;
  @Nullable private ModelRoot myModelRoot;

  private SModule myModule;
  private volatile SRepository myRepository = null;

  /**
   * model is treated {@link #isLoaded() loaded} when the state == FULLY_LOADED.
   * There are model implementations with simple NOT_LOADED -- FULLY_LOADED cycle,
   * and more complex with NOT_LOADED -- INTERFACE_LOADED -- FULLY_LOADED.
   */
  private ModelLoadingState myModelLoadState = ModelLoadingState.NOT_LOADED;

  protected SModelBase(@NotNull SModelReference modelReference, @NotNull DataSource source) {
    myModelReference = modelReference;
    mySource = source;
    myNodeEventDispatch = new ModelEventDispatch(this);
    myModelEventDispatch = new ModelListenerDispatch();
  }

  @Override
  public SRepository getRepository() {
//    assertCanRead(); we don't require write lock when myRepo is assigned, why would require read to get?
    return myRepository;
  }

  @Override
  public SNode createNode(@NotNull SConcept concept) {
    // nodeId should be model's responsibility, not SNode's as we shall migrate towards model-local node ids, preferably int instead of long,
    // and at least not random
    return new jetbrains.mps.smodel.SNode(concept, jetbrains.mps.smodel.SModel.generateUniqueId());
  }

  @Override
  public SNode createNode(@NotNull SConcept concept, @Nullable SNodeId nodeId) {
    if (nodeId == null) {
      nodeId = jetbrains.mps.smodel.SModel.generateUniqueId();
    }
    return new jetbrains.mps.smodel.SNode(concept, nodeId);
  }

  public void attach(@NotNull SRepository repo) {
    if (myRepository == repo) {
      LOG.warn("The model " + this + " is already attached to the repository " + repo);
      return;
    }
    if (myRepository != null) {
      throw new IllegalModelAccessException("Model is already attached to a repository, can't attach to another one");
    }
    repo.getModelAccess().checkReadAccess();
    myRepository = repo;
    myModelEventDispatch.modelAttached(this, repo);
  }

  public void detach() {
    assertCanChange();
    if (myRepository != null) {
      myModelEventDispatch.modelDetached(this, myRepository);
      myRepository = null;
    }
    fireBeforeModelDisposed(this);
    jetbrains.mps.smodel.SModel model = getCurrentModelInternal();
    if (model != null) {
      model.dispose();
    }
    clearListeners();
  }

  @Override
  public Iterable<SNode> getRootNodes() {
    assertCanRead();
    return getModelData().getRootNodes();
  }

  @Override
  public SNode getNode(SNodeId id) {
    assertCanRead();
    return getModelData().getNode(id);
  }


  @Override
  @NotNull
  public SModelReference getReference() {
//    assertCanRead(); model reference is read-only attribute, why care about read lock?
    return myModelReference;
  }

  @NotNull
  @Override
  public SModelId getModelId() {
//    assertCanRead(); model reference is read-only attribute, why care about read lock?
    return myModelReference.getModelId();
  }

  @Override
  @Deprecated
  public String getModelName() {
//    assertCanRead(); model reference is read-only attribute, why care about read lock?
    return myModelReference.getModelName();
  }

  @NotNull
  @Override
  public SModelName getName() {
    return myModelReference.getName();
  }

  @Override
  @NotNull
  public DataSource getSource() {
//    assertCanRead(); Is source access truly read operation over model?
    return mySource;
  }

  public void setModule(SModule module) {
    assertCanRead(); // FIXME why not write?
    myModule = module;
  }

  /**
   * TODO make final
   */
  @Override
  @Nullable
  public SModule getModule() {
    // FIXME provided setModule() requires read lock, another read lock here doesn't prevent from
    // myModule being modified in a parallel read, and the reason to have read check here eludes from me.
    // Code like SModuleOperations.getOutputRoot(SModel) fails with assert enabled, and
    // it's not obvious whether it's the client code to fix (to obtain read lock) or
    // this method shall not check for read access at all.
//    assertCanRead();
    return myModule;
  }

  public void setModelRoot(@Nullable ModelRoot modelRoot) {
    assertCanChange();
//    if (myModelRoot != null && modelRoot != null) {
//      LOG.error("Duplicate model roots for model " + getLongName() + " in module " + modelRoot.getModule() + ": \n" +
//        "1. " + myModelRoot.getPresentation() + "\n" +
//        "2. " + modelRoot.getPresentation()
//      );
//    }
    myModelRoot = modelRoot;
  }

  @Override
  @Nullable
  public ModelRoot getModelRoot() {
    assertCanRead();
    return myModelRoot;
  }

  @Override
  public void addRootNode(@NotNull SNode node) {
    throw new UnsupportedOperationException();
  }

  @Override
  public void removeRootNode(@NotNull SNode node) {
    throw new UnsupportedOperationException();
  }

  @Override
  public boolean isReadOnly() {
//    assertCanRead(); no apparent reason why we shall demand read lock here. Few subclasses, that override the method, do not check access at all.
    return true;
  }

  public boolean isRegistered() {
    SModule copy = myModule;
    return copy != null && copy.getRepository() != null;
  }

  /**
   * Access actual node storage. Might trigger model load if model is not yet loaded.
   * XXX perhaps, this method shall live in SModelDescriptorStub?
   * @return node storage. Generally, shall not return <code>null</code> (FIXME revisit contract, enforce)
   */
  public SModelData getModelData() {
    return getSModel();
  }

  /**
   * Likely, shall return SModelData eventually
   *
   * @return actual model data or <code>null</code> if not initialized yet
   */
  @Nullable
  protected abstract jetbrains.mps.smodel.SModel getCurrentModelInternal();

  @NotNull
  @Override
  public Iterable<Problem> getProblems() {
    assertCanRead();
    jetbrains.mps.smodel.SModel sModelInternal = getSModelInternal();
    if (sModelInternal instanceof InvalidSModel) {
      return ((InvalidSModel) sModelInternal).getProblems();
    }
    return Collections.emptySet();
  }

  @Override
  public void load() {
    // perhaps, both load() and unload() shall be left to implementors?
    getModelData();
  }

  /**
   * Dispose model data, change model's {@link #getLoadingState() loading state} and
   * dispatch {@link #fireModelStateChanged(ModelLoadingState, ModelLoadingState) state change event}.
   * Base implementation does nothing if there's no {@link #getCurrentModelInternal() initialized model data}.
   * Generally, subclasses shall override {@link #doUnload()} to perform actual cleanup of instance fields.
   */
  @Override
  public void unload() {
    assertCanChange();

    if (getCurrentModelInternal() == null) {
      return;
    }

    final ModelLoadingState oldState = getLoadingState();
    doUnload();
    fireModelStateChanged(oldState, getLoadingState());

  }

  /**
   * Perform actual dispose of model data and {@link #setLoadingState(ModelLoadingState)} changes loading state}.
   * No loading state event is sent (responsibility of {@link #unload()}. Subclasses shall override to
   * clean instance fields and generally shall delegate to this implementation first to dispose model data.
   */
  protected void doUnload() {
    jetbrains.mps.smodel.SModel modelData = getCurrentModelInternal();
    if (modelData == null) {
      return;
    }
    modelData.setModelDescriptor(null);
    modelData.dispose();
    setLoadingState(ModelLoadingState.NOT_LOADED);
  }

  @Override
  public boolean isLoaded() {
    return getLoadingState() == ModelLoadingState.FULLY_LOADED;
  }

  @Override
  public void addModelListener(SModelListener l) {
    myModelEventDispatch.addListener(l);
  }

  @Override
  public void removeModelListener(SModelListener l) {
    myModelEventDispatch.removeListener(l);
  }

  @Override
  public void addAccessListener(SModelAccessListener l) {
    myNodeEventDispatch.addAccessListener(l);
  }

  @Override
  public void removeAccessListener(SModelAccessListener l) {
    myNodeEventDispatch.removeAccessListener(l);
  }

  @Override
  public void addAccessListener(SNodeAccessListener l) {
    myNodeEventDispatch.addAccessListener(l);
  }

  @Override
  public void removeAccessListener(SNodeAccessListener l) {
    myNodeEventDispatch.removeAccessListener(l);
  }

  /**
   * This class doesn't dispatch change events, no listeners are tracked.
   */
  @Override
  public void addChangeListener(SNodeChangeListener l) {
    // intentionally no-op
  }

  /**
   * This class doesn't dispatch change events, no listeners are tracked.
   */
  @Override
  public void removeChangeListener(SNodeChangeListener l) {
    // intentionally no-op
  }

  protected final void fireBeforeModelRenamed(SModelReference newName) {
    SModule module = getModule();
    if (module instanceof SModuleBase) {
      ((SModuleBase) module).fireBeforeModelRenamed(this, newName);
    }
  }

  protected final void fireModelRenamed(SModelReference oldName) {
    SModule module = getModule();
    if (module instanceof SModuleBase) {
      ((SModuleBase) module).fireModelRenamed(this, oldName);
    }
  }

  /**
   * This method sends out proper notifications unless old and new state values are the same.
   * Note, it's not this method's responsibility to do actual change of the state, do it with {@link #setLoadingState(ModelLoadingState)}
   */
  protected void fireModelStateChanged(ModelLoadingState oldState, ModelLoadingState newState) {
    if (oldState == newState) {
      return;
    }
    super.fireModelStateChanged(newState);
    if (newState == ModelLoadingState.NOT_LOADED) {
      myModelEventDispatch.modelUnloaded(this);
    } else {
      myModelEventDispatch.modelLoaded(this, newState == ModelLoadingState.INTERFACE_LOADED);
    }
  }

  @Override
  protected void fireModelSaved() {
    super.fireModelSaved();
    myModelEventDispatch.modelSaved(this);
  }

  protected void fireConflictDetected() {
    myModelEventDispatch.conflictDetected(this);
  }

  protected void fireProblemsDetected(Iterable<Problem> problems) {
    myModelEventDispatch.problemsDetected(this, problems);
  }

  protected void fireModelReplaced() {
    myModelEventDispatch.modelReplaced(this);
  }

  @Override
  public void changeModelReference(SModelReference newModelReference) {
    super.changeModelReference(newModelReference);
    myModelReference = newModelReference;
  }

  /**
   * This method does nothing about model load state, it updates model descriptor of the models passed and dispatches a notification.
   * Seems reasonable to dispatch proper modelUnloaded/modelLoaded events in addition to modelReplaced as there are listeners that
   * expect either, not both. Especially, in case if load level is changed due to replacement (i.e. was FULL, became INTERFACE)
   * FIXME it's synchronized, do we still need that (with RegularModelDescriptor using distinct lock object)
   * XXX there are two uses in subclasses of not-so-nice EditableSModelBase (lazy and custom) that can't get replaced readily with
   * nice and convenient RegularModelDescriptor.replace() call.
   */
  protected synchronized void replaceModelAndFireEvent(jetbrains.mps.smodel.SModel oldModel, jetbrains.mps.smodel.SModel newModel) {
    if (oldModel != null) {
      oldModel.setModelDescriptor(null);
    }
    if (newModel != null) {
      newModel.setModelDescriptor(this);
    }
    if (oldModel != null) {
      notifyModelReplaced(oldModel);
      // ONCE notifyModelReplaced gone, don't forget to dispose oldModel here (SModelRepository does this in addition to notification dispatch)
    }

    fireModelReplaced();

    if (getRepository() != null) { // for a model not yet visible to anyone, no reason to drop a cache
      // FIXME cache invalidation shall be a repository listener, and not done forcefully on model change
      MPSModuleRepository.getInstance().invalidateCaches();
    }
  }

  @Override
  protected void assertCanRead() {
    final SRepository repo = myRepository;
    if (repo != null) {
      repo.getModelAccess().checkReadAccess();
    }
  }

  @Override
  protected void assertCanChange() {
    final SRepository repo = myRepository;
    if (repo != null) {
      repo.getModelAccess().checkWriteAccess();
    }
//      if (!UndoHelper.getInstance().isInsideUndoableCommand()) {
//        throw new IllegalModelChangeError("registered model can only be modified inside undoable command");
//      }
  }

  /**
   * Generally, shall be protected (as well as {@link #setLoadingState(ModelLoadingState)}, but made public for uses in
   * {@code PartialModelDataSupport} aka {@code jetbrains.mps.smodel.loading.UpdateableModel}.
   */
  @NotNull
  public final ModelLoadingState getLoadingState() {
    return myModelLoadState;
  }

  public final void setLoadingState(@NotNull ModelLoadingState modelLoadState) {
    myModelLoadState = modelLoadState;
  }


  /**
   * CLIENTS SHALL NOT USE THIS METHOD. It's public merely to overcome java package boundaries (those of SModelData implementation and this class).
   * FIXME This is a hack. We shall pass myEventDispatch the moment internal model is initialized.
   * However, it's tricky to find out exact moment with present approach (getSModelInternal() either
   * returns existing or creates new), fireModeStateChanged is feasible option, but misguiding as well.
   * Refactoring required to split access to SModel internal from initialization.
   * To put event dispatch into smodel.SModel doesn't seem to be an option as we need to add listeners without
   * loading whole model.
   */
  @NotNull
  public final ModelEventDispatch getNodeEventDispatch() {
    return myNodeEventDispatch;
  }
}
