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

import jetbrains.mps.extapi.persistence.FileBasedModelRoot;
import jetbrains.mps.extapi.persistence.FileSystemBasedDataSource;
import jetbrains.mps.extapi.persistence.datasource.PreinstalledDataSourceTypes;
import jetbrains.mps.extapi.persistence.SourceRoot;
import jetbrains.mps.extapi.persistence.SourceRootKinds;
import jetbrains.mps.extapi.persistence.datasource.DataSourceFactoryFromURL;
import org.jetbrains.mps.openapi.persistence.datasource.DataSourceType;
import jetbrains.mps.extapi.persistence.datasource.DataSourceFactoryFromName;
import jetbrains.mps.extapi.persistence.datasource.DataSourceFactoryRuleService;
import jetbrains.mps.extapi.persistence.datasource.PreinstalledURLDataSourceFactories;
import jetbrains.mps.extapi.persistence.datasource.URLNotSupportedException;
import jetbrains.mps.vfs.IFile;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.annotations.Immutable;
import org.jetbrains.mps.openapi.model.SModelName;
import org.jetbrains.mps.openapi.persistence.DataSource;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;

import static jetbrains.mps.persistence.DataSourceFactoryBridge.CompositeResult.build;

/**
 * Creates data sources (folder and file based) for the {@link DefaultModelRoot}.
 *
 * @author apyshkin
 * @since 3.5
 */
@Immutable
public final class DataSourceFactoryBridge {
  private static final Logger LOG = LogManager.getLogger(DataSourceFactoryBridge.class);

  @Immutable
  private final FileBasedModelRoot myModelRoot;

  public DataSourceFactoryBridge(@NotNull FileBasedModelRoot modelRoot) {
    myModelRoot = modelRoot;
  }

  @NotNull
  private static DataSourceFactoryFromName getDataSourceFactory(@NotNull DataSourceType dataSourceType) throws DataSourceFactoryNotFoundException {
    DataSourceFactoryFromName factory = DataSourceFactoryRuleService.getInstance().getFactory(dataSourceType);
    if (factory == null) {
      throw new DataSourceFactoryNotFoundException(dataSourceType);
    }
    return factory;
  }

  @NotNull
  public CompositeResult<DataSource> createFileDataSource(@NotNull SModelName modelName,
                                                          @Nullable SourceRoot sourceRoot) throws DataSourceFactoryNotFoundException,
                                                                                                  NoSourceRootsInModelRootException,
                                                                                                  SourceRootDoesNotExistException {
    return create(modelName, sourceRoot, PreinstalledDataSourceTypes.MPS);
  }

  @NotNull
  public CompositeResult<DataSource> createPerRootDataSource(@NotNull SModelName modelName,
                                                             @Nullable SourceRoot sourceRoot) throws DataSourceFactoryNotFoundException,
                                                                                                     NoSourceRootsInModelRootException,
                                                                                                     SourceRootDoesNotExistException {
    return create(modelName, sourceRoot, PreinstalledDataSourceTypes.MODEL);
  }

  /**
   * @param modelName new model name
   * @param sourceRoot if null the default (the first one) is chosen
   * @param dataSourceType the data source type you wish to create
   * @return new data source and corresponding model creation parameters (FIXME remove params?)
   * @throws DataSourceFactoryNotFoundException when there is no data source factory for a given type
   * @throws SourceRootDoesNotExistException when the given source root is not found in the model root
   * @throws NoSourceRootsInModelRootException when there are no source roots at all in the model root
   */
  @NotNull
  public CompositeResult<DataSource> create(@NotNull SModelName modelName,
                                     @Nullable SourceRoot sourceRoot,
                                     @NotNull DataSourceType dataSourceType) throws DataSourceFactoryNotFoundException,
                                                                                    SourceRootDoesNotExistException,
                                                                                    NoSourceRootsInModelRootException {
    DataSourceFactoryFromName factory = getDataSourceFactory(dataSourceType);
    return create(modelName, sourceRoot, factory);
  }

  @NotNull
  public CompositeResult<DataSource> create(@NotNull SModelName modelName,
                                            @Nullable SourceRoot sourceRoot,
                                            @NotNull DataSourceFactoryFromName factory) throws SourceRootDoesNotExistException,
                                                                                               NoSourceRootsInModelRootException {
    if (sourceRoot == null) {
      sourceRoot = DefaultModelRoot.Defaults.sourceRoot(myModelRoot);
    }
    checkSourceRootIsAttachedToTheModelRoot(sourceRoot);
    ModelCreationOptions parameters = new ParametersCalculator(myModelRoot).calculate(modelName);
    DataSource dataSource = factory.create(modelName, sourceRoot, myModelRoot);
    return build(dataSource, parameters);
  }

  private void checkSourceRootIsAttachedToTheModelRoot(@NotNull SourceRoot sourceRoot) throws NoSourceRootsInModelRootException,
                                                                                              SourceRootDoesNotExistException {
    List<SourceRoot> existingSourceRoots = myModelRoot.getSourceRoots(SourceRootKinds.SOURCES);
    if (existingSourceRoots.isEmpty()) {
      throw new NoSourceRootsInModelRootException(myModelRoot);
    } else if (!existingSourceRoots.contains(sourceRoot)) {
      throw new SourceRootDoesNotExistException(sourceRoot, myModelRoot);
    }
  }

  /**
   * Creates data source from a file.
   * No need to calculate model name here -- it must be provided in the
   * data source itself.
   */
  @Nullable
  CompositeResult<DataSource> create(@NotNull IFile file) {
    assert !file.isDirectory();
    DataSource dataSource = null;
    try {
      URL url = file.getUrl();
      DataSourceFactoryFromURL factory = DataSourceFactoryRuleService.getInstance().getFactory(url);
      if (factory == null) {
        throw new RuntimeException(new DataSourceFactoryNotFoundException("Could not find factory using the url " + url));
      }
      if (factory.supports(url)) {
        dataSource = factory.create(url, myModelRoot);
      }
    } catch (URLNotSupportedException | MalformedURLException e) {
      LOG.error("Could not get URL from IFile : '" + file + "'", e);
      return null;
    }
    ModelCreationOptions parameters = new ParametersCalculator(myModelRoot).calculate();
    if (dataSource == null) {
      LOG.error("Data source could not be constructed from the file: " + file, new Throwable());
      return null;
    }
    return build(dataSource, parameters);
  }

  /**
   * A composite of the data source and creation parameters for it.
   * Used as a result in the factory methods of the enclosing {@link DataSourceFactoryBridge}.
   */
  @Immutable
  public static final class CompositeResult<T extends DataSource> {
    private final T source;
    private final ModelCreationOptions parameters;

    private CompositeResult(T source0, ModelCreationOptions parameters0) {
      this.source = source0;
      this.parameters = parameters0;
    }

    @NotNull
    public static <T extends DataSource> CompositeResult<T> build(@NotNull T source,
                                                                  @NotNull ModelCreationOptions parameters) {
      return new CompositeResult<>(source, parameters);
    }

    @NotNull
    public T getDataSource() {
      return source;
    }

    @NotNull
    public ModelCreationOptions getOptions() {
      return parameters;
    }
  }
}
