/*
 * 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.model.GeneratableSModel;
import jetbrains.mps.extapi.model.SModelBase;
import jetbrains.mps.extapi.persistence.CopyNotSupportedException;
import jetbrains.mps.extapi.persistence.FileBasedModelRoot;
import jetbrains.mps.extapi.persistence.SourceRoot;
import jetbrains.mps.extapi.persistence.SourceRootKinds;
import jetbrains.mps.extapi.persistence.datasource.URLNotSupportedException;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.smodel.CopyUtil;
import jetbrains.mps.util.FileUtil;
import jetbrains.mps.vfs.IFile;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.EditableSModel;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelName;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.persistence.DataSource;
import org.jetbrains.mps.openapi.persistence.ModelFactory;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.List;

import static jetbrains.mps.extapi.persistence.datasource.PreinstalledURLDataSourceFactories.FILE_OR_FOLDER;

/**
 * Helps {@link DefaultModelRoot#copyTo(DefaultModelRoot)}
 *
 * Created by apyshkin on 12/19/16.
 */
final class CopyDefaultModelRootHelper {
  private final static Logger LOG = LogManager.getLogger(CopyDefaultModelRootHelper.class);

  private final DefaultModelRoot mySourceModelRoot;
  private final DefaultModelRoot myTargetModelRoot;
  private final AbstractModule mySourceModule;
  private final AbstractModule myTargetModule;

  CopyDefaultModelRootHelper(DefaultModelRoot sourceModelRoot, DefaultModelRoot targetModelRoot) {
    mySourceModelRoot = sourceModelRoot;
    myTargetModelRoot = targetModelRoot;
    mySourceModule = ((AbstractModule) mySourceModelRoot.getModule());
    myTargetModule = ((AbstractModule) myTargetModelRoot.getModule());
  }

  private boolean isInsideModuleDir() {
    final SModule module = mySourceModelRoot.getModule();
    if (module instanceof AbstractModule) {
      IFile contentDirectory = mySourceModelRoot.getContentDirectory();
      IFile moduleSourceDir = ((AbstractModule) module).getModuleSourceDir();
      if (moduleSourceDir == null) {
        return false;
      }
      assert contentDirectory != null;
      return FileUtil.isAncestor(moduleSourceDir.getPath(), contentDirectory.getPath());
    }
    return false;
  }

  /**
   * We are doing the same thing we do when collecting models but instead of creating models
   * we recalculate the paths (and other options) and create corresponding model copies under
   * the new (target) model root
   *
   * @throws CopyNotSupportedException if the content directory of the model root is not under module source directory
   *                                   since <code>DefaultModelRoot</code> allows to change models under it
   *                                   we forbid copying such model roots with the content directories located outside of the module
   *                                   source directory
   */
  public void copy() throws CopyNotSupportedException {
    if (mySourceModelRoot.getContentDirectory() == null) {
      return;
    }
    if (!isInsideModuleDir()) {
      throw new CopyNotSupportedException("The model root's content path must be inside module directory " + mySourceModelRoot + " : " + mySourceModelRoot.getModule());
    }

    List<SourceRoot> sourceFiles = mySourceModelRoot.getSourceRoots(SourceRootKinds.SOURCES);
    List<SourceRoot> targetFiles = myTargetModelRoot.getSourceRoots(SourceRootKinds.SOURCES);
    assert sourceFiles.size() == targetFiles.size(); // #copyContentRootAndFiles guarantees
    for (int cnt = 0; cnt < sourceFiles.size(); ++cnt) {
      SourceRoot sourceRoot = sourceFiles.get(cnt);
      SourceRoot targetSourceRoot = targetFiles.get(cnt);
      targetSourceRoot.getAbsolutePath().mkdirs();
      ModelSourceRootWalker modelSourceRootWalker = new ModelSourceRootWalker(mySourceModelRoot, (factory, dataSource, options, file) -> {
        try {
          IFile targetModelFile = calculateTargetModelFile(mySourceModule, myTargetModule, sourceRoot, targetSourceRoot, file);
          SModelBase modelData = (SModelBase) new ModelFactoryFacade(factory).load(dataSource, options);
          createModelCopy(factory, targetModelFile, modelData);
        } catch (URLNotSupportedException | URISyntaxException | IOException | ModelCannotBeCreatedException e) {
          LOG.error("", new CopyNotSupportedException("Could not copy because of unexpected error" , e));
        }
      });
      modelSourceRootWalker.traverse(sourceRoot);
    }
  }

  @NotNull
  private SModel createModelCopy(@NotNull ModelFactory factory,
                                 @NotNull IFile targetModelFile,
                                 @NotNull SModelBase modelDataToCopy) throws IOException,
                                                                             URISyntaxException,
                                                                             URLNotSupportedException,
                                                                             ModelCannotBeCreatedException {
    DataSource targetDataSource = FILE_OR_FOLDER.create(targetModelFile.getUrl(), myTargetModelRoot);
    ParametersCalculator prmCalculator = new ParametersCalculator(myTargetModelRoot);
    SModelName newModelName = new SModelName(convertNameConsideringModule(modelDataToCopy.getName().getValue(),
                                                                          mySourceModule,
                                                                          myTargetModule));
    ModelCreationOptions options = prmCalculator.calculate(newModelName);
    SModel targetModel = myTargetModelRoot.createModel0(factory, targetDataSource, options);
    // TODO Since model factory can provide any model implementation
    // TODO model root doesn't know how to exactly copy the content of given model.
    // TODO So model content copying should be carried by model itself.
    // TODO This functionality should be extracted in separate interface (like CopyableSModel).
    CopyUtil.copyModelContentAndPreserveIds(modelDataToCopy, targetModel);
    CopyUtil.copyModelProperties(modelDataToCopy.getSModel(), ((SModelBase) targetModel).getSModel());
    if (targetModel instanceof GeneratableSModel && modelDataToCopy instanceof GeneratableSModel) {
      ((GeneratableSModel) targetModel).setDoNotGenerate(((GeneratableSModel) modelDataToCopy).isDoNotGenerate());
    }
    saveModel(targetModel);
    return targetModel;
  }

  @NotNull
  private IFile calculateTargetModelFile(AbstractModule sourceModule,
                                         AbstractModule targetModule,
                                         SourceRoot sourceRoot,
                                         SourceRoot targetSourceRoot,
                                         IFile sourceModelFile) {
    String relPath = FileBasedModelRoot.relativize(sourceModelFile.getPath(), sourceRoot.getAbsolutePath());
    relPath = convertNameConsideringModule(relPath, sourceModule, targetModule);
    return targetSourceRoot.getAbsolutePath().getDescendant(relPath);
  }

  /**
   * A special hack
   * fixme move to workbench
   */
  @NotNull
  private String convertNameConsideringModule(String name, AbstractModule sourceModule, AbstractModule targetModule) {
    if (name.startsWith(sourceModule.getModuleName())) {
      name = targetModule.getModuleName() + name.substring(sourceModule.getModuleName().length());
    }
    return name;
  }

  // FIXME see MPS-18545
  private static void saveModel(@NotNull SModel targetModel) {
    if (targetModel instanceof EditableSModel) {
      ((EditableSModel) targetModel).setChanged(true);
      ((EditableSModel) targetModel).save();
    }
  }
}
