/*
 * Copyright 2000-2013 JetBrains s.r.o.
 * Copyright 2014-2014 AS3Boyan
 * Copyright 2014-2014 Elias Ku
 * Copyright 2017-2018 Eric Bishton
 *
 * 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 com.intellij.plugins.haxe.compilation;

import com.intellij.compiler.options.CompileStepBeforeRun;
import com.intellij.execution.ExecutorRegistry;
import com.intellij.execution.configurations.ModuleBasedConfiguration;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.execution.configurations.RunConfigurationModule;
import com.intellij.execution.executors.DefaultDebugExecutor;
import com.intellij.openapi.compiler.*;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectUtil;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkAdditionalData;
import com.intellij.openapi.roots.CompilerProjectExtension;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderEnumerator;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile;
import com.intellij.plugins.haxe.HaxeBundle;
import com.intellij.plugins.haxe.config.HaxeTarget;
import com.intellij.plugins.haxe.config.sdk.HaxeSdkAdditionalDataBase;
import com.intellij.plugins.haxe.ide.module.HaxeModuleSettings;
import com.intellij.plugins.haxe.ide.module.HaxeModuleType;
import com.intellij.plugins.haxe.module.HaxeModuleSettingsBase;
import com.intellij.plugins.haxe.runner.debugger.HaxeDebugRunner;
import com.intellij.plugins.haxe.tests.runner.HaxeTestsConfiguration;
import com.intellij.plugins.haxe.util.HaxeCommonCompilerUtil;
import com.intellij.plugins.haxe.util.HaxeSdkUtilBase;
import com.intellij.util.PathUtil;
import lombok.CustomLog;
import org.jetbrains.annotations.NotNull;

import java.io.DataInput;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@CustomLog
public class HaxeCompiler implements FileProcessingCompiler {

  /*
  // flag to indicate whether a module needs to be built
  private static HashMap<Module, Boolean> skipBuildMap = new HashMap<Module, Boolean>();
  // holds the change set for each module to track whether anything changed since last build
  private static HashMap<Module, Set<String>> changeSetMap = new HashMap<Module, Set<String>>();
  */

  @NotNull
  public String getDescription() {
    return HaxeBundle.message("haxe.compiler.description");
  }

  @Override
  public boolean validateConfiguration(CompileScope scope) {
    return true;
  }

  @NotNull
  @Override
  public ProcessingItem[] getProcessingItems(CompileContext context) {
    final List<ProcessingItem> itemList = new ArrayList<ProcessingItem>();
    for (final Module module : getModulesToCompile(context.getCompileScope() /*, context.getProject() */)) {
      itemList.add(new MyProcessingItem(module));
    }
    return itemList.toArray(new ProcessingItem[0]);
  }

  private static List<Module> getModulesToCompile(final CompileScope scope /*, final Project project */) {
    final List<Module> result = new ArrayList<Module>();
    for (final Module module : scope.getAffectedModules()) {
      if (ModuleType.get(module) != HaxeModuleType.getInstance()) continue;
      result.add(module);
      /*
      boolean skipBuilding = false; // default: always build
      //-- are any changes since last build
      if (changeSetMap.get(module) != null) { // was built at least once, in past ...
        final Set<String> latestChangeSet = new HashSet<String>();
        Collection<VirtualFile> vFileCollection = FileTypeIndex
          .getFiles(HaxeFileType.HAXE_FILE_TYPE, module.getModuleWithDependenciesScope());
        final VirtualFile[] virtualFiles = vFileCollection.toArray(new VirtualFile[0]);
        for (VirtualFile file : virtualFiles) {
          final FileStatus fileStatus = FileStatusManager.getInstance(project).getStatus(file);
          if (fileStatus.equals(FileStatus.NOT_CHANGED) || fileStatus.equals(FileStatus.UNKNOWN)) continue;
          latestChangeSet.add(file.getPath());
        }
        skipBuilding = latestChangeSet.equals(changeSetMap.get(module));
        changeSetMap.put(module, latestChangeSet);
      }
      else {
        changeSetMap.put(module, new HashSet<String>());
      }
      skipBuildMap.put(module, new Boolean(skipBuilding));
      if (! skipBuilding) {
        result.add(module);
      }
      */
    }
    return result;
  }

  @Override
  public ProcessingItem[] process(CompileContext context, ProcessingItem[] items) {
    final RunConfiguration runConfiguration = CompileStepBeforeRun.getRunConfiguration(context.getCompileScope());
    if (runConfiguration instanceof ModuleBasedConfiguration) {
      return run(context, items, (ModuleBasedConfiguration)runConfiguration);
    }
    return make(context, items);
  }

  private static ProcessingItem[] run(CompileContext context,
                                      ProcessingItem[] items,
                                      ModuleBasedConfiguration configuration) {
    final Module module = configuration.getConfigurationModule().getModule();
    if (module == null) {
      context.addMessage(CompilerMessageCategory.ERROR,
                         HaxeBundle.message("no.module.for.run.configuration", configuration.getName()), null, -1, -1);
      return ProcessingItem.EMPTY_ARRAY;
    }
    HaxeCommonCompilerUtil.CompilationContext compilationContext = createCompilationContext(context, module, configuration);

    if (compileModule(context, module, compilationContext)) {
      final int index = findProcessingItemIndexByModule(items, configuration.getConfigurationModule());
      if (index != -1) {
        return new ProcessingItem[]{items[index]};
      }
    }
    return ProcessingItem.EMPTY_ARRAY;
  }

  private static ProcessingItem[] make(CompileContext context, ProcessingItem[] items) {
    final List<ProcessingItem> result = new ArrayList<ProcessingItem>();
    for (ProcessingItem processingItem : items) {
      if (!(processingItem instanceof MyProcessingItem)) {
        continue;
      }
      final MyProcessingItem myProcessingItem = (MyProcessingItem)processingItem;
      if (compileModule(context, myProcessingItem.myModule, createCompilationContext(context, myProcessingItem.myModule, null))) {
        result.add(processingItem);
      }
    }
    return result.toArray(new ProcessingItem[0]);
  }

  private static boolean compileModule(final CompileContext context,
                                       Module module,
                                       @NotNull final HaxeCommonCompilerUtil.CompilationContext compilationContext) {

    /*
    if ((skipBuildMap.get(module) != null) && (skipBuildMap.get(module).booleanValue())) {
      return false;
    }
    */

    if (!ModuleType.get(module).equals(HaxeModuleType.getInstance())) {
      return true;
    }

    boolean compiled = HaxeCommonCompilerUtil.compile(compilationContext);

    if (!compiled) {
      context.addMessage(CompilerMessageCategory.ERROR, "Compilation failed", null, 0, 0);
    }

    return compiled;
  }

  public static HaxeCommonCompilerUtil.CompilationContext createDummyCompilationContext(final Module module) {
    DummyCompileContext context = new DummyCompileContext(module.getProject()) {
      @Override
      public Project getProject() {
        return module.getProject();
      }
    };
    return createCompilationContext(context, module, null);
  }

  private static HaxeCommonCompilerUtil.CompilationContext createCompilationContext(final CompileContext context,
                                                                                    final Module module,
                                                                                    ModuleBasedConfiguration configuration) {

    final HaxeModuleSettings settings = HaxeModuleSettings.getInstance(module);
    final boolean isDebug = ExecutorRegistry.getInstance()
      .isStarting(context.getProject(), DefaultDebugExecutor.EXECUTOR_ID, HaxeDebugRunner.HAXE_DEBUG_RUNNER_ID);
    final ModuleRootManager moduleRootManager = ModuleRootManager.getInstance(module);
    final Sdk sdk = moduleRootManager.getSdk();
    if (sdk == null) {
      context.addMessage(CompilerMessageCategory.ERROR, HaxeBundle.message("no.sdk.for.module", module.getName()), null, -1, -1);
      return null;
    }

    HaxeTestsConfiguration haxeTestsConfiguration = null;

    if(configuration != null && configuration instanceof HaxeTestsConfiguration) {
      haxeTestsConfiguration = (HaxeTestsConfiguration)configuration;
    }

    final HaxeTestsConfiguration finalHaxeTestsConfiguration = haxeTestsConfiguration;

    return new HaxeCommonCompilerUtil.CompilationContext() {
      private String myErrorRoot;

      @Override
      public HaxeSdkAdditionalDataBase getHaxeSdkData() {
        return HaxeSdkUtilBase.getSdkData(module);
      }

      @NotNull
      @Override
      public HaxeModuleSettingsBase getModuleSettings() {
        return settings;
      }

      @Override
      public String getModuleName() {
        return module.getName();
      }

      @Override
      public String getCompilationClass() {
        return getIsTestBuild() ? finalHaxeTestsConfiguration.getRunnerClass() : getModuleSettings().getMainClass();
      }

      @Override
      public String getOutputFileName() {
        return getFileNameWithCurrentExtension(getModuleSettings().getOutputFileName());
      }

      @Override
      public String getOutputDirectory() {
        return getModuleSettings().getOutputFolder();
      }

      @Override
      public Boolean getIsTestBuild() {
        return finalHaxeTestsConfiguration != null;
      }

      @Override
      public void errorHandler(String message) {
        context.addMessage(CompilerMessageCategory.ERROR, message, null, -1, -1);
      }

      @Override
      public void warningHandler(String message) {
        context.addMessage(CompilerMessageCategory.WARNING, message, null, -1, -1);
      }

      @Override
      public void infoHandler(String message) {
        context.addMessage(CompilerMessageCategory.INFORMATION, message, null, -1, -1);
      }

      public void statisticsHandler(String message) {
        context.addMessage(CompilerMessageCategory.INFORMATION, message, null, -1, -1);
      }

      @Override
      public void log(String message) {
        log.debug(message);
      }

      @Override
      public String getSdkName() {
        return sdk.getName();
      }

      @Override
      public String getSdkHomePath() {
        return sdk.getHomePath();
      }

      @Override
      public String getHaxelibPath() {
        SdkAdditionalData data = sdk.getSdkAdditionalData();
        return data instanceof HaxeSdkAdditionalDataBase ? ((HaxeSdkAdditionalDataBase)data).getHaxelibPath() : null;
      }

      @Override
      public String getNekoBinPath() {
        SdkAdditionalData data = sdk.getSdkAdditionalData();
        return data instanceof  HaxeSdkAdditionalDataBase ? ((HaxeSdkAdditionalDataBase)data).getNekoBinPath(): null;
      }

      @Override
      public boolean isDebug() {
        return isDebug;
      }

      @Override
      public List<String> getSourceRoots() {
        final List<String> result = new ArrayList<String>();
        for (VirtualFile sourceRoot : OrderEnumerator.orderEntries(module).recursively().withoutSdk().exportedOnly().sources().getRoots()) {
          result.add(sourceRoot.getPath());
        }
        for (VirtualFile sourceRoot : OrderEnumerator.orderEntries(module).librariesOnly().getSourceRoots()) {
          result.add(sourceRoot.getPath());
        }
        return result;
      }

      @Override
      public String getModuleDefaultCompileOutputPath() {
        // This is probably the right way to do it, *if* we actually had a CompilerModuleExtension for Haxe.
        //  final CompilerModuleExtension moduleExtension = CompilerModuleExtension.getInstance(module);
        //  final String outputUrl = moduleExtension != null ? moduleExtension.getCompilerOutputUrl() : null;
        //  return VfsUtilCore.urlToPath(outputUrl);

        // Instead, reach down directly and get the project's output path.
        final String projectOutputPath = CompilerProjectExtension.getInstance(module.getProject()).getCompilerOutputUrl();
        return VfsUtilCore.urlToPath(projectOutputPath);
      }

      @Override
      public void setErrorRoot(String root) {
        myErrorRoot = root;
      }

      @Override
      public String getErrorRoot() {
        return (myErrorRoot != null) ? myErrorRoot :  ProjectUtil.guessModuleDir(module).getCanonicalPath();
      }

      @Override
      public void handleOutput(String[] lines) {
        HaxeCompilerUtil.fillContext(context, getErrorRoot(), lines);
      }

      @Override
      public HaxeTarget getHaxeTarget() {
        //actually only neko target is supported for tests
        return getIsTestBuild() ? HaxeTarget.NEKO : getModuleSettings().getHaxeTarget();
      }

      private String getFileNameWithCurrentExtension(String fileName) {
        if (getHaxeTarget() != null) {
          return getHaxeTarget().getTargetFileNameWithExtension(FileUtil.getNameWithoutExtension(fileName));
        }
        return fileName;
      }

      @Override
      public String getModuleDirPath() {
        return ProjectUtil.guessModuleDir(module).getCanonicalPath();
      }
    };
  }

  private static int findProcessingItemIndexByModule(ProcessingItem[] items, RunConfigurationModule moduleConfiguration) {
    final Module module = moduleConfiguration.getModule();
    if (module == null) {
      return -1;
    }
    for (int i = 0; i < items.length; ++i) {
      if (items[i] instanceof MyProcessingItem processingItem) {
        if (module == processingItem.myModule) {
          return i;
        }
      }
    }
    return -1;
  }

  @Override
  public ValidityState createValidityState(DataInput in) throws IOException {
    return new EmptyValidityState();
  }

  private static class MyProcessingItem implements ProcessingItem {
    private final Module myModule;

    private MyProcessingItem(Module module) {
      myModule = module;
    }

    @NotNull
    public VirtualFile getFile() {
      //TODO MLO: does not look like its used, and we will get rid of this when we move to new  compiler execution logic
      // creating fake as  "myModule.getModuleFile" is internal API
      return new FakeVirtualFile(ProjectUtil.guessModuleDir(myModule), "Dummy");
      //return myModule.getModuleFile();
    }

    public ValidityState getValidityState() {
      return new EmptyValidityState();
    }
  }
}
