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

import jetbrains.mps.kernel.model.MissingDependenciesFixer;
import jetbrains.mps.persistence.DefaultModelRoot;
import jetbrains.mps.persistence.ModelCannotBeCreatedException;
import jetbrains.mps.persistence.PersistenceRegistry;
import jetbrains.mps.project.facets.GenerationTargetFacet;
import jetbrains.mps.project.facets.JavaModuleFacet;
import jetbrains.mps.project.facets.TestsFacet;
import jetbrains.mps.project.persistence.ModuleReadException;
import jetbrains.mps.project.structure.modules.ModuleDescriptor;
import jetbrains.mps.smodel.Generator;
import jetbrains.mps.smodel.Language;
import jetbrains.mps.smodel.MPSModuleOwner;
import jetbrains.mps.smodel.MPSModuleRepository;
import jetbrains.mps.smodel.SModelOperations;
import jetbrains.mps.util.annotation.ToRemove;
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.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.module.SRepository;
import org.jetbrains.mps.openapi.persistence.ModelFactory;
import org.jetbrains.mps.openapi.persistence.ModelFactoryType;
import org.jetbrains.mps.openapi.persistence.ModelRoot;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;

public class SModuleOperations {
  private static final Logger LOG = LogManager.getLogger(SModuleOperations.class);

  /**
   * @deprecated use {@link jetbrains.mps.project.facets.GenerationTargetFacet#getOutputLocation(SModel)} or {@link JavaModuleFacet#getOutputRoot()}.
   *             Even {@link #getOutputRoot(SModel)} is much better as it (a) deals with IFile (b) hints it's root, not model-specific location
   */
  @Deprecated
  @ToRemove(version = 3.5)
  public static String getOutputPathFor(SModel model) {
    IFile outputDir = SModelOperations.getOutputLocation(model);
    return outputDir != null ? outputDir.getPath() : null;
  }

  /**
   * Filesystem location for all output of the model's module.
   * Unlike {@link #getOutputPathFor(org.jetbrains.mps.openapi.model.SModel)} doesn't
   * translate IFile to String.
   *
   * @deprecated use {@link jetbrains.mps.smodel.SModelOperations#getOutputLocation(SModel)} instead. This operation is not {@code SModuleOperation}'s.
   *
   * @return module's output path, or null if unknown
   */
  @ToRemove(version = 3.5)
  @Deprecated
  public static IFile getOutputRoot(@NotNull SModel model) {
    return SModelOperations.getOutputLocation(model);
  }

  /**
   * @deprecated This operation knows about output roots of {@link JavaModuleFacet} and {@link TestsFacet} only. Locations of any other
   *             {@link GenerationTargetFacet} are ignored. There's only 1 use in MPS. Client code shall not assume there's single output root for a
   *             module, there could be multiple outputs, specified per model.
   * @return all locations where generated files (including auxiliary model streams, files with hashes and dependencies) of the module could be found.
   */
  @ToRemove(version = 3.5)
  @Deprecated
  public static Collection<IFile> getOutputRoots(@NotNull SModule module) {
    // XXX there's jetbrains.mps.tool.builder.paths.ModuleOutputPaths which looks quite similar, shall refactor. It's definitely not tooling-specific code.
    ArrayList<IFile> rv = new ArrayList<>(4);
    JavaModuleFacet jmf = module.getFacet(JavaModuleFacet.class);
    if (jmf != null) {
      IFile path = jmf.getOutputRoot();
      if (path != null) {
        rv.add(path);
      }
      path = jmf.getOutputCacheRoot();
      if (path != null) {
        rv.add(path);
      }
    }
    TestsFacet testFacet = module.getFacet(TestsFacet.class);
    if (testFacet != null) {
      IFile path = testFacet.getTestsOutputPath();
      if (path != null) {
        rv.add(path);
      }
      path = testFacet.getOutputCacheRoot();
      if (path != null) {
        rv.add(path);
      }
    }
    // XXX see DefaultStreamManager#getOverridenOutputDir(SModel)
    // we shall iterate over all models of the module, check instanceof GeneratableSModel && isGenerateIntoModelFolder(), and
    // then (md.getSource() as FileDataSource).getParent(), but GeneratedFilesExcludePolicy which I write the method for, used
    // to be satisfied with #getOutputRoot(), which didn't check for overridden output root either.
    return rv;
  }

  @NotNull
  public static JavaModuleFacet getJavaFacet(SModule module) {
    JavaModuleFacet facet = module.getFacet(JavaModuleFacet.class);
    if (facet != null) {
      return facet;
    } else {
      throw new IllegalArgumentException();
    }
  }

  public static boolean isCompileInMps(SModule module) {
    JavaModuleFacet facet = module.getFacet(JavaModuleFacet.class);
    return facet != null && facet.isCompileInMps();
  }

  public static boolean isCompileInIdea(SModule module) {
    JavaModuleFacet facet = module.getFacet(JavaModuleFacet.class);
    return facet != null && !facet.isCompileInMps();
  }

  public static Set<String> getAllSourcePaths(SModule module) {
    // todo: get rid from source paths?
    ArrayList<String> paths = new ArrayList<>(4);
    JavaModuleFacet jmf = module.getFacet(JavaModuleFacet.class);
    if (jmf != null) {
      IFile path = jmf.getOutputRoot();
      if (path != null) {
        paths.add(path.getPath());
      }
      paths.addAll(jmf.getAdditionalSourcePaths());
    }

    TestsFacet testsFacet = module.getFacet(TestsFacet.class);
    if (testsFacet != null) {
      IFile path = testsFacet.getTestsOutputPath();
      if (path != null) {
        paths.add(path.getPath());
      }
    }
    return new TreeSet<>(paths);
  }

  @Nullable
  public static EditableSModel createModelWithAdjustments(@NotNull String name, @NotNull ModelRoot root) {
    try {
      return createModelWithAdjustments(name, root, null);
    } catch (ModelCannotBeCreatedException ignore) {
    }
    return null;
  }

  @NotNull
  public static EditableSModel createModelWithAdjustments(@NotNull String name,
                                                          @NotNull ModelRoot root,
                                                          @Nullable ModelFactoryType modelFactoryType) throws ModelCannotBeCreatedException {
    EditableSModel model;
    if (modelFactoryType != null && root instanceof DefaultModelRoot) {
      DefaultModelRoot defaultModelRoot = (DefaultModelRoot) root;
      model = (EditableSModel) defaultModelRoot.createModel(new SModelName(name), null, null, modelFactoryType);
    } else {
      model = (EditableSModel) root.createModel(name);
    }

    // FIXME something bad: see MPS-18545 SModel api: createModel(), setChanged(), isLoaded(), save()
    // model.getSModel() ?
    model.setChanged(true);
    model.save();

    ModelsAutoImportsManager.doAutoImport(root.getModule(), model);
    new MissingDependenciesFixer(model).fixModuleDependencies();
    return model;
  }

  public static boolean needReloading(AbstractModule module) {
    // todo: ?
    SRepository repo = module.getRepository();
    if (repo != null) {
      repo.getModelAccess().checkReadAccess();
    }

    IFile descriptorFile = module.getDescriptorFile();
    if ((descriptorFile == null) || !descriptorFile.exists()) {
      return false;
    }

    final ModuleDescriptor descriptor = module.getModuleDescriptor();
    if (descriptor == null) return false;

    String timestampString = descriptor.getTimestamp();

    if (timestampString == null) return true;
    long timestamp = Long.decode(timestampString);
    return timestamp != descriptorFile.lastModified();
  }

  /**
   * Reads module from file and eventually reloads it (when CLManager triggers refresh)
   */
  public static void reloadFromDisk(@NotNull AbstractModule module) {
    if (module.getRepository() == null) {
      throw new IllegalArgumentException("Module " + module + " is disposed");
    }

    module.getRepository().getModelAccess().checkWriteAccess();

    try {
      if (module instanceof Generator) {
        // loadDescriptor == null for Generator
        // FIXME shall support reload for generator modules (not necessarily with modile.loadDescriptor() thought)
        return;
      }
      ModuleDescriptor descriptor = module.loadDescriptor();
      module.setModuleDescriptor(descriptor);
    } catch (ModuleReadException e) {
      AbstractModule.handleReadProblem(module, e, false);
    }
  }

  public static Project getProjectForModule(SModule module) {
    if (module == null) {
      return null;
    }
    Project project = null;
    SRepository repository = module.getRepository();
    if (repository instanceof ProjectRepository) {
      project = ((ProjectRepository) repository).getProject();
    } else if (repository instanceof MPSModuleRepository) {
      Language language = null;
      Set<MPSModuleOwner> owners = ((MPSModuleRepository) repository).getOwners(module);
      for (MPSModuleOwner owner : owners) {
        if (owner instanceof Project) {
          project = ((Project) owner);
          break;
        } else if (owner instanceof Language) {
          language = (Language) owner;
        }
      }
      if (project == null && language != null) {
        project = getProjectForModule(language);
      }
    }
    return project;
  }

  // helpers
  private static void checkContentPath(String path, SModule module, ModelRoot modelRoot) {
    if (PersistenceRegistry.JAVA_CLASSES_ROOT.equals(modelRoot.getType())) {
      return;
    }

    String sig = (containsFilesWithSuffix(new File(path), ".java") ? "j" : "") + (containsFilesWithSuffix(new File(path), ".class") ? "c" : "");
    if (sig.length() == 2) {
      sig = "j&c";
    }
    if (!sig.isEmpty()) {
      System.out.printf("!%s at %s type in %s%n", sig, modelRoot.getType(), module.getModuleName());
    }
  }

  private static boolean containsFilesWithSuffix(File path, String suffix) {
    if (path.isFile()) {
      return path.getName().endsWith(suffix);
    } else if (path.isDirectory()) {
      for (File child : path.listFiles()) {
        if (containsFilesWithSuffix(child, suffix)) {
          return true;
        }
      }
    }
    return false;
  }
}
