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

import jetbrains.mps.compiler.EclipseJavaCompiler;
import jetbrains.mps.compiler.JavaCompilerOptions;
import jetbrains.mps.make.CompilationErrorsHandler.ClassesErrorsTracker;
import jetbrains.mps.make.ModuleAnalyzer.ModuleAnalyzerResult;
import jetbrains.mps.project.facets.JavaModuleOperations;
import jetbrains.mps.reloading.IClassPathItem;
import jetbrains.mps.util.FileUtil;
import jetbrains.mps.util.NameUtil;
import jetbrains.mps.vfs.IFile;
import org.eclipse.jdt.core.compiler.CategorizedProblem;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.module.SModule;
import org.eclipse.jdt.internal.compiler.CompilationResult;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import static jetbrains.mps.project.SModuleOperations.getJavaFacet;

/**
 * fixme use bundle for this package
 */
class InternalJavaCompiler {
  private final static String MODULE_WITH_REMOVALS_WAS_NOT_CHANGED = "Module With Removals Is Not In The Changed Modules: %s";
  private final static String NO_CHANGES_AFTER_COMPILATION_ERROR = "Compilation Passed But The Changed Modules Are Empty";
  private final static String CALCULATING_DEPS_MSG = "Calculating Classpath";
  private final static String COMPILING_JAVA_MSG = "Compiling Java";
  private final static String PREPARING_TO_COMPILE_MSG = "Preparing";
  private final static String COPYING_RESOURCES_MSG = "Copying Resources";
  private final static String HANDLING_ERRORS_MSG = "Handling Errors";
  private final static String WRITING_CLASSES_MSG = "Writing Classes";
  private final static String ECLIPSE_COMPILER_MSG = "Running ECJ";

  @NotNull private final ModulesContainer myModulesContainer;
  @Nullable private final JavaCompilerOptions myCompilerOptions;

  InternalJavaCompiler(@NotNull ModulesContainer modulesContainer, @Nullable JavaCompilerOptions compilerOptions) {
    myModulesContainer = modulesContainer;
    myCompilerOptions = compilerOptions;
  }

  @NotNull
  public MPSCompilationResult compile(CompositeTracer tracer) {
    if (!myModulesContainer.hasModuleToCompile()) {
      return MPSCompilationResult.ZERO_COMPILATION_RESULT;
    }
    tracer.start("", 5);
    try {
      ModuleAnalyzerResult analysisResult = new ModuleAnalyzer(myModulesContainer).analyze();
      EclipseJavaCompiler compiler;
      tracer.push(PREPARING_TO_COMPILE_MSG);
      try {
        if (!analysisResult.hasJavaToCompile && !analysisResult.hasResourcesToUpdate) {
          return MPSCompilationResult.nothingToDoCompilationResult();
        }
        analysisResult.filesToDelete.forEach(FileUtil::delete); // removing all stale files
        compiler = collectSources();
      } finally {
        tracer.pop(1);
      }

      MPSCompilationResult result;
      if (!analysisResult.hasJavaToCompile) {
        result = MPSCompilationResult.noJavaCompiledCompilationResult();
      } else {
        result = compileJava(compiler, tracer.subTracer(3));
        reportModulesWithRemovalsAreNotChanged(analysisResult.modulesWithRemovals, result.getChangedModules(), tracer);
      }
      copyResources(tracer.subTracer(1));

      return result;
    } finally {
      tracer.done();
    }
  }

  /**
   * @return eclipse java compiler with sources attached
   */
  private EclipseJavaCompiler collectSources() {
    EclipseJavaCompiler compiler = new EclipseJavaCompiler();
    for (SModule module : myModulesContainer.getModules()) {
      if (!myModulesContainer.areClassesUpToDate(module)) {
        for (JavaFile javaFile : myModulesContainer.getSources(module).getFilesToCompile()) {
          compiler.addSource(javaFile.getClassName(), javaFile.getContents());
          myModulesContainer.putClassForModule(javaFile.getClassName(), module);
        }
      }
    }
    return compiler;
  }

  private void copyResources(CompositeTracer tracer) {
    tracer.start(COPYING_RESOURCES_MSG, 1);
    try {
      for (SModule module : myModulesContainer.getModules()) {
        ModuleSources sources = myModulesContainer.getSources(module);
        IFile classesGen = getJavaFacet(module).getClassesGen();
        if (classesGen == null) {
          continue;
        }
        for (ResourceFile toCopy : sources.getResourcesToCopy()) {
          String fqName = toCopy.getPath();

          fqName = fqName.substring(0, fqName.length() - toCopy.getFile().getName().length());
          String path = fqName + toCopy.getFile().getName();

          if (new File(toCopy.getFile().getAbsolutePath()).exists()) {
            FileUtil.copyFile(new File(toCopy.getFile().getAbsolutePath()), new File(classesGen.getDescendant(path).toPath().toAbsolute().toString()));
          }
        }
      }
    } finally {
      tracer.done(1);
    }
  }

  private void reportModulesWithRemovalsAreNotChanged(Set<SModule> modulesWithRemovals, Set<SModule> changedModules, CompositeTracer tracer) {
    for (SModule module : modulesWithRemovals) {
      if (!changedModules.contains(module)) {
        tracer.warn(String.format(MODULE_WITH_REMOVALS_WAS_NOT_CHANGED, module), module.getModuleReference());
      }
    }
  }

  @NotNull
  private MPSCompilationResult compileJava(EclipseJavaCompiler compiler, CompositeTracer tracer) {
    tracer.start(COMPILING_JAVA_MSG, 10);
    try {
      IClassPathItem classPath = computeDependenciesClassPath(myModulesContainer.getModules(), tracer.subTracer(1));
      final CompilationErrorsHandler errorsHandler = new CompilationErrorsHandler(myModulesContainer, tracer.getSender(), classPath);
      CompilationHandler compilationHandler = new CompilationHandler(classPath, tracer.subTracer(3), errorsHandler);
      final CollectingResultsListener listener = new CollectingResultsListener(errorsHandler);

      compiler.addCompilationResultListener(listener);
      doCompileJava(compiler, classPath, myCompilerOptions, tracer.subTracer(6));
      compiler.removeCompilationResultListener(listener);

      Collection<SModule> changedModules = compilationHandler.process(listener.getResults());

      if (changedModules.isEmpty()){
        tracer.error(NO_CHANGES_AFTER_COMPILATION_ERROR);
      }
      return new MPSCompilationResult(errorsHandler.getErrorsCount(), 0, false, changedModules); // fixme: no warnings in the result
    } finally {
      tracer.done();
    }
  }

  private void doCompileJava(EclipseJavaCompiler compiler, IClassPathItem classPath, @Nullable JavaCompilerOptions compilerOptions, CompositeTracer tracer) {
    try {
      tracer.start(ECLIPSE_COMPILER_MSG, 1);
      if (compilerOptions == null) {
        compiler.compile(classPath);
      } else {
        compiler.compile(classPath, compilerOptions);
      }
    } finally {
      tracer.done(1);
    }
  }

  // FIXME at least twice we count all the dependencies WHY
  private static IClassPathItem computeDependenciesClassPath(Set<SModule> modules, CompositeTracer tracer) {
    tracer.start(CALCULATING_DEPS_MSG, 1);
    try {
      Set<String> classpath = JavaModuleOperations.collectCompileClasspath(modules, true);
      tracer.info("ClassPath: " + classpath);
      return JavaModuleOperations.createClassPathItem(classpath, ModuleMaker.class.getName());
    } finally {
      tracer.done(1);
    }
  }

  /**
   * Memorizes and returns all the results. Also handles fatal errors
   */
  private class CollectingResultsListener extends jetbrains.mps.compiler.CompilationResultAdapter {
    private final CompilationErrorsHandler myErrorsHandler;
    private final List<CompilationResult> myResults = new ArrayList<>();

    CollectingResultsListener(@NotNull CompilationErrorsHandler errorsHandler) {
      myErrorsHandler = errorsHandler;
    }

    @Override
    public void onFatalError(@NotNull String msg) {
      myErrorsHandler.handleFatal(msg);
    }

    @Override
    public void onCompilationResult(CompilationResult r) {
      myResults.add(r);
    }

    public List<CompilationResult> getResults() {
      return Collections.unmodifiableList(myResults);
    }
  }

  /**
   * Process all the compilation results
   */
  private class CompilationHandler {
    private final IClassPathItem myClassPath;
    private final CompositeTracer myTracer;
    private final CompilationErrorsHandler myErrorsHandler;
    private final ClassFileWriter myWriter;

    CompilationHandler(IClassPathItem classPath, CompositeTracer tracer, CompilationErrorsHandler errorsHandler) {
      myClassPath = classPath;
      myTracer = tracer;
      myErrorsHandler = errorsHandler;
      myWriter = new ClassFileWriter(myModulesContainer, tracer, myClassPath);
    }

    /**
     * @return a set of changed modules
     */
    public Set<SModule> process(List<CompilationResult> results) {
      myTracer.start(HANDLING_ERRORS_MSG, 11);
      try {
        ClassesErrorsTracker errorsTracker = new ClassesErrorsTracker();
        for (CompilationResult result : results) {
          CategorizedProblem[] errors = result.getErrors();
          if (errors != null && errors.length > 0) {
            errorsTracker = myErrorsHandler.handle(result);
          }
        }
        myTracer.advance(1);
        myTracer.push(WRITING_CLASSES_MSG);
        Set<SModule> changedModules = myWriter.write(results, errorsTracker);
        myTracer.pop(10);
        return changedModules;
      } finally {
        myTracer.done();
      }
    }
  }
}
