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

import jetbrains.mps.extapi.model.SModelBase;
import jetbrains.mps.smodel.loading.ModelLoadingState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.persistence.DataSource;

/**
 * General [openapi]SModel implementation, with proper synchronization and loading notifications, with factory method {@link #createModel()}
 * for subclasses to override.
 *
 * IMPLEMENTATION NOTES:
 * Bears 'ModelDescriptor' name to keep it consistent, where model data is smodel.SModel, and openapi.SModel implementation is
 * merely a proxy to model data. Once I can do similar implementation using API classes only, shall switch to use 'Model' for
 * these proxies (openapi.SModel) and stick to various XXXModelData for smodel.SModel.
 *
 * Lives in j.m.smodel, not j.m.extapi.model as it depends from smodel.SModel now, and I want API to use API classes only.
 */
public abstract class RegularModelDescriptor extends SModelBase {
  // FIXME SModelBase/SModelDefscriptorStub with gtSModelInternal demand we keep SModel, not SModelData
  private volatile jetbrains.mps.smodel.SModel mySModel;

  /**
   * left protected for subclasses that need extended control over loading process (i.e. partial/sequential model loading)
   */
  protected final Object myLoadLock = new Object();

  protected RegularModelDescriptor(@NotNull SModelReference modelReference, @NotNull DataSource dataSource) {
    super(modelReference, dataSource);
  }

  @Override
  public final jetbrains.mps.smodel.SModel getSModelInternal() {
    SModel copy = mySModel;
    if (copy != null) {
      return copy;
    }
    final ModelLoadingState oldState;
    synchronized (myLoadLock) {
      oldState = getLoadingState();
      if (mySModel == null) {
        ModelLoadResult<jetbrains.mps.smodel.SModel> loadResult = createModel();
        mySModel = loadResult.getModelData();
        mySModel.setModelDescriptor(this);
        setLoadingState(loadResult.getState());
      }
    }
    fireModelStateChanged(oldState, getLoadingState());
    return mySModel;
  }

  @Nullable
  @Override
  protected final jetbrains.mps.smodel.SModel getCurrentModelInternal() {
    return mySModel;
  }

  /**
   * @return new model data and level it was loaded to
   */
  @NotNull
  protected abstract ModelLoadResult<jetbrains.mps.smodel.SModel> createModel();

  @Override
  protected void doUnload() {
    synchronized (myLoadLock) {
      super.doUnload();
      mySModel = null;
    }
  }

  /**
   * Handy utility when the new model data is known/obtained beforehand, not through #createModel() factory
   */
  protected void replace(@NotNull ModelLoadResult<jetbrains.mps.smodel.SModel> newModel) {
    final ModelLoadingState oldState;
    synchronized (myLoadLock) {
      oldState = getLoadingState();
      if (mySModel != null) {
        mySModel.setModelDescriptor(null);
        mySModel.dispose();
        mySModel = null;
      }
      mySModel = newModel.getModelData();
      if (mySModel != null) {
        mySModel.setModelDescriptor(this);
      }
      setLoadingState(newModel.getState());
    }
    fireModelStateChanged(oldState, getLoadingState());
    fireModelReplaced();
  }
}
