/*
 * Copyright 2003-2017 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.facets;

import jetbrains.mps.extapi.module.ModuleFacetBase;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.project.ProjectPathUtil;
import jetbrains.mps.project.Solution;
import jetbrains.mps.project.structure.modules.ModuleDescriptor;
import jetbrains.mps.smodel.Generator;
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 java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;

/**
 * todo: divide into two parts: JavaModuleFacetSrcImpl && JavaModuleFacetPackagedImpl
 */
public class JavaModuleFacetImpl extends ModuleFacetBase implements JavaModuleFacet {
  private static final Logger LOG = LogManager.getLogger(JavaModuleFacetImpl.class);

  public JavaModuleFacetImpl() {
    super(FACET_TYPE);
  }

  @Override
  public String getFacetPresentation() {
    return "Java";
  }

  @Override
  public boolean isCompileInMps() {
    AbstractModule module = getModule();
    if (module instanceof Generator) {
      return true;
    }
    ModuleDescriptor descriptor = module.getModuleDescriptor();
    return descriptor != null && descriptor.getCompileInMPS();
  }

  @NotNull
  @Override
  public AbstractModule getModule() {
    return (AbstractModule) super.getModule();
  }

  @Override
  @Nullable
  public IFile getClassesGen() {
    if (getModule().isPackaged()) {
      return null;
    }
    ModuleDescriptor moduleDescriptor = getModule().getModuleDescriptor();
    if (moduleDescriptor == null) {
      // this facet implementation doesn't know how to handle modules not based on ModuleDescriptor
      return null;
    }
    if (moduleDescriptor.getDeploymentDescriptor() != null) {
      // in fact, this is what isPackaged() shall check (according to its javadoc), but at the moment it cares about
      // module source dir not being in archive, which is not exactly the same, hence extra check here.
      return null;
    }
    // XXX there's same code in MM, shall refactor, likely move to ModuleDescriptor
    String sourceGenPath = ProjectPathUtil.getGeneratorOutputPath(moduleDescriptor);
    if (sourceGenPath == null) {
      // a kind of a module without generated sources, no classes_gen then.
      return null;
    }
    // XXX would adore IFile from ModuleDescriptor, not String.
    return getModule().getFileSystem().getFile(sourceGenPath).getParent().getDescendant("classes_gen");
  }

  @Override
  public Set<String> getLibraryClassPath() {
    Set<String> libraryClassPath = new LinkedHashSet<String>();

    // add additional java stub paths
    ModuleDescriptor moduleDescriptor = getModule().getModuleDescriptor();
    if (moduleDescriptor != null) {
      // XXX for deployed modules, we could use DD.getLibraries here. But as long as MM updates getAdditionalJavaStubPaths of a source module
      //     and source module is always present, enjoy.
      libraryClassPath.addAll(moduleDescriptor.getAdditionalJavaStubPaths());
    }

    // add classes folder for modules compiled outside MPS
    if (getModule() instanceof Solution && !isCompileInMps() && !getModule().isPackaged()) {
      // for packaged modules, we can't tell if classes deployed with it shall go into libraryCP or into #getClassPath(). Now they
      // go into latter, as there's (a) no uses for #getLibraryClassPath; (b) there's no need to compile deployed modules, hence no
      // reason to have its external classes available in libraries.
      // todo: remove this logic?
      String generatorOutputPath = ProjectPathUtil.getGeneratorOutputPath(getModule().getModuleDescriptor());
      IFile classes = null;
      if (generatorOutputPath != null) {
        // same 'sibling to sources_gen/' logic is in ModulesMiner. Location of a module as IFile would be much more handy.
        classes = getModule().getFileSystem().getFile(generatorOutputPath).getParent().getDescendant("classes");
      }
      if (classes != null && classes.exists()) {
        libraryClassPath.add(getClassPath(classes));
      }
    }
    return libraryClassPath;
  }

  @Override
  public final Set<String> getClassPath() {
    Set<String> result = new LinkedHashSet<String>();
    result.addAll(getLibraryClassPath());
    // XXX CP entry for IDEA-compiled modules (classes/) is part of library CP. Is it right?
    //     On the one hand, we might need classes compiled outside of a module to build it, OTOH, it makes classes/
    //     somewhat different from classes_gen/
    IFile classesGen = getClassesGen();
    if (classesGen == null && getModule().isPackaged()) {
      // Despite isPackaged(), there might be modules like stub and test that lack MD or DD, doesn't hurt to check
      ModuleDescriptor moduleDescriptor = getModule().getModuleDescriptor();
      if (moduleDescriptor != null && moduleDescriptor.getDeploymentDescriptor() != null) {
        // 'Right' scenario. Deployed module has DD and we take classpath from there
        result.addAll(moduleDescriptor.getDeploymentDescriptor().getClasspath());
      } else {
        // Compatibility code:
        // Case 1. Deployed generator modules have no DD and are read independently from their source languages.
        //         Include their separate jar (hard-coded knowledge about build layout) into classpath.
        if (getModule() instanceof Generator) {
          IFile descriptorFile = ((Generator) getModule()).getSourceLanguage().getDescriptorFile();
          IFile bundleHome = descriptorFile == null ? null : descriptorFile.getBundleHome();
          if (bundleHome != null) {
            // bundleHome for module itself and {bundleHome without .jar}-generator.jar for generator
            String mainPath = bundleHome.getPath().substring(0, bundleHome.getPath().length() - ".jar".length());
            String jarPath = mainPath + "-generator.jar";
            classesGen = bundleHome.getFileSystem().getFile(jarPath);
          }
        } else if (getModule().getDescriptorFile() != null) {
          // CASE 2. Solution(s) bundled into single jar with classes (both from hand-written and generated sources) at the root.
          // HACK. Fallback for manually bundled modules (vcs.jar or mps-core.jar):
          //   my.jar
          //     compile output of module1
          //   modules
          //      module sources of module1
          // There's no DD there, and assumption is that there are classes at the jar root.
          // Not yet sure what's the right way to deal with them:
          //   - specify DD (META-INF/module.xml) at build time looks most 'honest', however, with multiple modules inside same jar it's not an option,
          //     unless we can make DD per module, not per jar (requires support in MM.tryReadFromModulesDir). Support in Build language needed, too (to
          //     specify 'module descriptor of' under 'folder with sources of'
          //   - Patch MD in MM when loaded from modules/ location (e.g. add DD with proper classpath there). (+) keep knowledge about deployment layout
          //     inside MM.
          //   - Hack here
          classesGen = getModule().getDescriptorFile().getBundleHome();
        }
      }
    }
    if (classesGen != null) {
      result.add(getClassPath(classesGen));
    }
    return result;
  }

  private String getClassPath(@NotNull IFile classes) {
    String path = classes.getPath();
    if (path.contains("!")) {
      String[] split = path.split("!");
      if (split.length > 0) {
        if (!split[1].isEmpty() && !split[1].equals("/")) {
          LOG.warn("Can not transform directory " + path + " to proper classpath while calculating classpath for module " + getModule());
        }
      }
      return split[0];
    }
    return path;
  }

  @Override
  public Set<String> getAdditionalSourcePaths() {
    ModuleDescriptor moduleDescriptor = getModule().getModuleDescriptor();

    if (moduleDescriptor == null) {
      return Collections.emptySet();
    }

    return new HashSet<String>(moduleDescriptor.getSourcePaths());
  }
}
