/*
 * Copyright 2003-2012 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 org.jetbrains.mps.openapi.persistence;

import jetbrains.mps.util.annotation.ToRemove;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelName;
import org.jetbrains.mps.openapi.persistence.datasource.DataSourceType;
import org.jetbrains.mps.openapi.persistence.datasource.FileExtensionDataSourceType;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * Represents a data source loading/saving/upgrading strategy.
 *
 * The location resides rather on the {@link DataSource}
 * side than here while the storage parameters belong here.
 *
 * NB: The <code>ModelFactory</code> knows about <code>DataSource</code> but
 * not vice versa.
 *
 * Creates/upgrades/saves/loads models (instances of SModel) from data sources.
 *
 * NB: the default implementation of the methods added in 3.5 will be dropped at the 2017.3
 * minor release. Please do not linger to pass on the new API.
 *
 * @author apyshkin
 */
public interface ModelFactory {
  // --- default options ---
  /**
   * @deprecated not used currently
   */
  @ToRemove(version = 2017.2)
  @Deprecated
  String OPTION_MODULEREF = "moduleReference";

  /**
   * @deprecated not used currently
   */
  @ToRemove(version = 2017.2)
  @Deprecated
  String OPTION_PACKAGE = "package";

  /**
   * @deprecated not in use anymore
   */
  @ToRemove(version = 2017.2)
  @Deprecated
  String OPTION_RELPATH = "relativePath";

  /**
   * Denotes a model name, used as a key in the <code>Map<String, String></code> parameter
   * in the {@link #create(DataSource, Map)} methods.
   *
   * @deprecated this option is unnecessary. Use {@link #create(DataSource, SModelName, ModelLoadingOption...)} instead.
   */
  @ToRemove(version = 2017.3)
  @Deprecated
  String OPTION_MODELNAME = "modelName";

  /**
   * Boolean value, indicates we don't care to build complete model on load, rather read content as-is,
   * and tread loaded model as mere container for nodes, <code>SModelData</code>-like.
   * We use this mechanism from merge driver and various tools that are going to access nodes from
   * the model but are not going to expose this model anywhere else.
   *
   * @deprecated String option is not informatory. Use a {@link ContentOption} instead
   */
  @ToRemove(version = 2017.3)
  @Deprecated
  String OPTION_CONTENT_ONLY = "contentOnly";

  // --- API ---

  /**
   * Instantiates a model on a given data source. Options can be used to pass additional parameters
   * like stream encoding (usually, the default is utf-8), package name, containing module reference
   * or module relative path of the source.
   *
   * @return The loaded model
   * @throws UnsupportedDataSourceException if the data source is not supported
   * @deprecated <code>Map<String,String></code> is hardly perceivable. Please use
   *             rather {@link #load(DataSource, ModelLoadingOption...)} instead
   */
  @ToRemove(version = 2017.3)
  @Deprecated
  @NotNull
  SModel load(@NotNull DataSource dataSource, @NotNull Map<String, String> options) throws IOException;

  /**
   * Creates a new empty model.
   * Implementor must throw <code>UnsupportedDataSourceException</code> if
   * #canCreate returns false.
   *
   * @param dataSource if null then the default data source is created for the supplied model name
   * @param options must content {@link #OPTION_MODELNAME} mapping.
   *
   * @throws UnsupportedDataSourceException if the data source is not supported, in other words 
   *                                        {@link #canCreate(DataSource, Map)} returns false
   * @throws IOException if the model cannot be created for some other reasons
   *
   * @deprecated use more flexible {@link #create(DataSource, SModelName, ModelLoadingOption...)} instead
   */
  @ToRemove(version = 2017.3)
  @Deprecated
  @NotNull
  SModel create(@NotNull DataSource dataSource, @NotNull Map<String, String> options) throws IOException;

  /**
   * Indicates, whether the supplied data source can be used to hold models created by this factory.
   *
   * ---------
   * Currently this method implementations are quite shallow and de facto it does not work at all
   * as the API designer must have suggested. Now the real semantics of this method is whether the data 
   * source is supported by the <code>ModelFactory</code>.
   * I suggest to use the honest {@link #supports} method and catch the {@link ModelCreationException}  post factum rather then
   * do the check beforehand.
   * AP
   * ---------
   *
   * @deprecated rather try calling {@link #create(DataSource, SModelName, ModelLoadingOption...)} method and
   * catching the {@link ModelCreationException} during creation or
   * use {@link #supports(DataSource)} method to acknowledge whether this data source is supported at all.
   */
  @ToRemove(version = 2017.3)
  @Deprecated
  boolean canCreate(@NotNull DataSource dataSource, @NotNull Map<String, String> options);

  /**
   * Determines whether the provided data source is maintained by this model factory instance.
   * Call it before calling load/create/upgrade methods in order to avoid the {@link UnsupportedDataSourceException}.
   *
   * @return true iff the given data source can be managed by this factory
   */
  default boolean supports(@NotNull DataSource dataSource) {
    return true;
  }

  /**
   * Creates a new model with the supplied <code>modelName</code> on the given <code>DataSource</code>.
   * Might consider additional options provided in the <code>options</code> varargs.
   * [General rule: options.contain(SomeOption) => SomeOption is on]
   *
   * @param dataSource -- location to create the model in
   * @param modelName -- a new model name (note that it might be constructed easily from full-qualified <code>String</code>)
   * @param options -- custom options
   *                   @see ModelLoadingOption
   * @return newly created model
   *
   * @throws UnsupportedDataSourceException iff {@link #supports(DataSource)} returns false
   * @throws ModelCreationException iff there was an irrecoverable problem during creation (for instance model file already exists)
   */
  @NotNull
  default SModel create(@NotNull DataSource dataSource,
                        @NotNull SModelName modelName,
                        @NotNull ModelLoadingOption... options) throws UnsupportedDataSourceException,
                                                                       ModelCreationException {
    try {
      Map<String, String> options0 = Collections.singletonMap(OPTION_MODELNAME, modelName.getValue());
      return create(dataSource, options0);
    } catch (IOException e) {
      throw new ModelCreationException("Default #create implementation failed. Please implement.", Collections.emptyList(), e);
    }
  }

  /**
   * Loads an existing model from the given <code>DataSource</code>.
   * Might consider additional options provided in the <code>options</code> varargs.
   * [General rule: options.contain(SomeOption) => SomeOption is on]
   *
   * @param dataSource -- location to load model from
   * @param options -- custom options
   *                   @see ModelLoadingOption
   * @return loaded model
   *
   * @throws UnsupportedDataSourceException iff {@link #supports(DataSource)} returns false
   * @throws ModelLoadException iff there was an irrecoverable load problem (for instance format was broken or unrecognized)
   */
  @NotNull
  default SModel load(@NotNull DataSource dataSource, @NotNull ModelLoadingOption... options) throws UnsupportedDataSourceException, ModelLoadException {
    try {
      return load(dataSource, Collections.emptyMap());
    } catch (IOException e) {
      throw new ModelLoadException("Default #load implementation failed. Please implement.", Collections.emptyList(), e);
    }
  }

  /**
   * Checks if the source content is outdated and needs to be upgraded.
   *
   * @throws UnsupportedDataSourceException if the data source is not supported
   */
  boolean needsUpgrade(@NotNull DataSource dataSource) throws IOException;

  /**
   * Loads the model content, and saves it back in the up-to-date format.
   *
   * @throws UnsupportedDataSourceException if the data source is not supported
   */
  void upgrade(@NotNull DataSource dataSource) throws IOException;

  /**
   * Saves the model in the factory-specific format (including conversion when needed).
   *
   * @throws UnsupportedDataSourceException if the data source is not supported
   */
  void save(@NotNull SModel model, @NotNull DataSource dataSource) throws UnsupportedDataSourceException, ModelSaveException, IOException;

  /**
   * returns true if plain text is not enough to represent stored data.
   *
   * // @deprecated The contract is not clear ("not enough" means what exactly?).
   *                We would rather turn this into some marker interface than have it here
   */
  /*@ToRemove(version = 2017.3)*/
  /*@Deprecated*/
  boolean isBinary();

  /**
   * @return the file extension this factory is registered on
   *         null if for instance model factory is associated rather with a group of files than one file.
   *
   * @deprecated Outrageous breaking of the separation location from the loading strategy principle, which is
   *             planted in the create, load, etc. methods.
   *             The location notion is hidden in {@link DataSource} and there is absolutely no need to expose it.
   *             In order to work out the currently running scenarios we set up special DataSourceFactory <-> ModelFactory
   *             relations which are exposed through the <code>DataSourceFactoryRuleService</code>, <code>ModelFactoryService</code> and
   *             {@link #getPreferredDataSourceTypes}.
   */
  @ToRemove(version = 2017.3)
  @Deprecated
  @Nullable String getFileExtension();

  /**
   * User-readable title of the storage format.
   *
   * @deprecated use {@link #getType()} and {@link ModelFactoryType#getFormatTitle()} instead
   */
  @NotNull
  @Deprecated
  @ToRemove(version = 2017.3)
  /*default*/ String getFormatTitle(); /*{
    return getType().getFormatTitle();
  }*/

  /**
   * Returns an id which is used to get model factory by id in the
   * {@code ModelFactoryService}.
   *
   * @return model factory unique identification entity
   *
   * @implNote THE DEFAULT IMPLEMENTATION IS A FALLBACK STUB, PLEASE IMPLEMENT
   */
  @NotNull
  default ModelFactoryType getType() {
    return NotImplementedModelFactoryType.INSTANCE; // return value is chosen on purpose
  }

  /**
   * Declares a list of preferred data source formats,
   * sorted from the most preferred to the less preferred data source type.
   *
   * @return a list of data source kinds which might be considered when MPS
   *         meets a data source location and needs to choose a model factory
   *         which is likely to support the content of the data source.
   *         For instance if we have data source types associated with file names (e.g. prefix or suffix)
   *         we are able to establish a file name pattern association with a specific model factory.
   *         For example each model file which ends with '.mps_binary' suffix would be associated with the
   *         corresponding data source type which in turn would be associated with 'MyBinaryModelFactory'.
   *
   * @implNote THE DEFAULT IMPLEMENTATION IS A FALLBACK STUB, PLEASE IMPLEMENT
   */
  @NotNull
  default List<DataSourceType> getPreferredDataSourceTypes() {
    String fileExtension = getFileExtension();
    return Collections.singletonList(FileExtensionDataSourceType.of(fileExtension));
  }
}
