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

import jetbrains.mps.extapi.model.SModelBase;
import jetbrains.mps.extapi.module.SModuleBase;
import jetbrains.mps.extapi.persistence.DisposableDataSource;
import jetbrains.mps.project.facets.GenerationTargetFacet;
import jetbrains.mps.project.facets.JavaModuleFacet;
import jetbrains.mps.vfs.IFile;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleFacet;
import org.jetbrains.mps.openapi.persistence.DataSource;

import java.util.ArrayDeque;

/**
 * Utility to perform various aspects of expelling a model:
 *  - Model may produce generated files that need to be removed
 *  - Models come from a data source that needs to be removed as well
 *  - Models belong to a module that needs to get notified about model gone
 *
 * All these shall be part of regular API. There's no such API yet, unfortunately.
 * To keep relevant stuff in a single place, accessible not only to UI code (like j.m.workbench.action.model.DeleteModelHelper does),
 * this class was introduced. Its aspects shall move to respective classes (e.g. DataSource shall get #delete() API, SModule needs #detach(SModel), etc)
 * Use either {@link #delete()} for complete disposal of a model, or distinct step
 * ({@link #removeGeneratedArtifacts()}, {@link #detachFromModule()} or {@link #deleteDataSource()}) as appropriate.
 *
 * Lives in [kernel] as it depends from implementation classes now (like SModuleBase).
 *
 * @author Artem Tikhomirov
 * @since 3.4
 */
public class ModelDeleteHelper {
  private final SModel myModel;

  public ModelDeleteHelper(@NotNull SModel model) {
    myModel = model;
  }

  public void delete() {
    removeGeneratedArtifacts();
    detachFromModule();
    deleteDataSource();
  }

  public void removeGeneratedArtifacts() {
    SModule module = myModel.getModule();
    JavaModuleFacet javaFacet = module.getFacet(JavaModuleFacet.class);
    // perhaps, worth an option (pruneEmpty)?
    ArrayDeque<IFile> possiblyEmptyDirsToPrune = new ArrayDeque<>();
    if (javaFacet != null) {
      IFile classesGenDir = javaFacet.getClassesLocation(myModel);
      if (classesGenDir != null) {
        possiblyEmptyDirsToPrune.add(classesGenDir.getParent()); // I don't expect model output dir to be top of the disk, don't care about parent == null
        classesGenDir.delete();
      }
    }

    for (SModuleFacet mf : module.getFacets()) {
      if (!(mf instanceof GenerationTargetFacet)) {
        continue;
      }
      GenerationTargetFacet genFacet = (GenerationTargetFacet) mf;
      IFile modelOutput = genFacet.getOutputLocation(myModel);
      IFile modelCaches = genFacet.getOutputCacheLocation(myModel);
      // XXX if model serves as a namespace, there might be child folders with output for nested models
      // nevertheless, we delete them altogether as removal of a model likely means removal of nested models as well. Or not?
      // e.g. IdeCommandUtil is more careful when executing #removeGenSources console command
      if (modelOutput != null) {
        possiblyEmptyDirsToPrune.add(modelOutput.getParent());
        modelOutput.delete();
      }
      if (modelCaches != null) {
        possiblyEmptyDirsToPrune.add(modelCaches.getParent());
        modelCaches.delete();
      }
    }

    // FIXME what about TestsFacet, why not deleting its artifacts?

    while (!possiblyEmptyDirsToPrune.isEmpty()) {
      IFile d = possiblyEmptyDirsToPrune.removeFirst();
      if (d.exists() && d.getChildren().isEmpty()) {
        IFile parent = d.getParent();
        if (parent != null) {
          possiblyEmptyDirsToPrune.addLast(parent);
        }
        d.delete();
      }
    }
  }

  public void detachFromModule() {
    SModule module = myModel.getModule();
    if (module == null) {
      return;
    }
    ((SModuleBase) module).unregisterModel((SModelBase) myModel);
  }

  public void deleteDataSource() {
    DataSource source = myModel.getSource();
    String modelName = myModel.getModelName();
    if (source instanceof DisposableDataSource) {
      ((DisposableDataSource) source).delete();
    } else {
      String msg = String.format("DataSource (%s) of model %s doesn't support delete operation.", source.getClass().getName(), modelName);
      Logger.getLogger(ModelDeleteHelper.class).info(msg);
    }
  }
}
