// Copyright 2006-2012 AdvancedTools. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.advancedtools.cpp.actions;

import com.advancedtools.cpp.CppBundle;
import com.advancedtools.cpp.CppSupportLoader;
import com.advancedtools.cpp.CppSupportSettings;
import com.advancedtools.cpp.build.*;
import com.advancedtools.cpp.communicator.Communicator;
import com.advancedtools.cpp.communicator.BuildingCommandHelper;
import com.intellij.execution.filters.Filter;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.NonNls;

import javax.swing.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
 * @author maxim
 */
public class CompileCppAction extends AnAction {
  private static CppSupportSettings.CompilerSelectOptions lastOptions;

  public void actionPerformed(AnActionEvent anActionEvent) {
    invoke(anActionEvent.getData(LangDataKeys.PROJECT), anActionEvent.getData(LangDataKeys.VIRTUAL_FILE), null);
  }

  interface CompileCppOptions {
    String getCompilerOptions();
    String getProjectCompilerOptions();
    boolean doRun();
    String getOutputFileName();
    Project getProject();

    boolean isCppFile();
  }

  public static abstract class CompileHandler {
    private static final String MARKER = "$filename$";

    abstract @Nullable List<String> buildCommand(VirtualFile file, CompileCppOptions options);
    abstract @Nullable Map<String, String> getCommandLineProperties();
    abstract @Nullable Filter getCompileLogFilter(VirtualFile file, CompileCppOptions options);

    protected List<String> defaultAppendOptions(CompileCppOptions options, List<String> command, VirtualFile file) {
      command = BuildUtils.appendAllOptions(command, options.getProjectCompilerOptions());
      command = BuildUtils.appendAllOptions(
        command,
        options.getCompilerOptions().replace(
          MARKER,
          getEscapedPathToFile(file.getPath())
        )
      );
      return command;
    }

    abstract String buildProjectCompileOptions(Project project);

    abstract String getOutputFileName(VirtualFile file, CompileCppOptions compileOptions);
  }

  public static class ClangCompileHandler extends CompileHandler {
    private @NonNls Map<String, String> myItems;

    @Nullable List<String> buildCommand(VirtualFile file, CompileCppOptions options) {
      List<String> commandLine = new ArrayList<String>(4);
      if (!options.doRun()) {
        commandLine = BuildUtils.appendOptions(commandLine, "-S");
      }
      else {
        final String fileName = options.getOutputFileName();
        if (fileName != null) {
          commandLine = BuildUtils.appendOptions(commandLine, "-o");
          commandLine = BuildUtils.appendOptions(commandLine, fileName);
        }
      }
      commandLine = defaultAppendOptions(options, commandLine, file);
      myItems = BuildUtils.buildEnvironmentMap(options.getProject(), file);
      return BuildUtils.buildClangToolCall(CppSupportSettings.getInstance().getClangPath(), commandLine);
    }

    @Nullable
    Map<String, String> getCommandLineProperties() {
      return myItems;
    }

    public static String getMatchingPattern() {
      return "^((?:\\w\\:)?[^\\:]+)(?:\\:([0-9]+)\\:(?:([0-9])+\\:))";
    }

    @Nullable
    Filter getCompileLogFilter(VirtualFile file, CompileCppOptions options) {
      return new BasicFormatFilter(file, options.getProject(), getMatchingPattern());
    }

    String buildProjectCompileOptions(Project project) {
      return null;
    }

    String getOutputFileName(VirtualFile file, CompileCppOptions compileOptions) {
      final String fileName = compileOptions.getOutputFileName();
      return fileName != null ? fileName: SystemInfo.isWindows ? "a.exe":"a.out";
    }
  }

  static class GccCompileHandler extends CompileHandler {
    private @NonNls Map<String, String> myItems;

    @Nullable List<String> buildCommand(VirtualFile file, CompileCppOptions options) {
      List<String> command = new ArrayList<String>(4);
      if (!options.doRun()) command = BuildUtils.appendOptions(command, "-c");
      else {
        final String fileName = options.getOutputFileName();
        if (fileName != null) {
          command = BuildUtils.appendOptions(command, "-o");
          command = BuildUtils.appendOptions(command, fileName);
        }
      }
      command = defaultAppendOptions(options, command, file);
      myItems = BuildUtils.buildEnvironmentMap(options.getProject(), file);
      String gccPath = CppSupportSettings.getInstance().getGccPath().replaceAll("gcc", options.isCppFile() ? "g++":"gcc");

      return BuildUtils.buildGccToolCall(gccPath, command);
    }

    @Nullable
    Map<String, String> getCommandLineProperties() {
      return myItems;
    }

    @Nullable
    Filter getCompileLogFilter(VirtualFile file, CompileCppOptions options) {
      return new MakeBuildHandler.MakeFormatFilter(file, options.getProject());
    }

    String buildProjectCompileOptions(Project project) {
      return null;
    }

    String getOutputFileName(VirtualFile file, CompileCppOptions compileOptions) {
      final String fileName = compileOptions.getOutputFileName();
      return fileName != null ? fileName: SystemInfo.isWindows ? "a.exe":"a.out";
    }
  }

  static class VcCompileHandler extends CompileHandler {
    @Nullable List<String> buildCommand(VirtualFile file, CompileCppOptions options) {
      try {
        List<String> commandLine = Arrays.asList("cl.exe");
        if (!options.doRun()) commandLine = BuildUtils.appendOptions(commandLine, "/c");
        else {
          final String fileName = options.getOutputFileName();
          if (fileName != null) {
            commandLine = BuildUtils.appendOptions(commandLine, "/Fe"+ fileName);
          }
        }

        commandLine = defaultAppendOptions(options, commandLine, file);
        return BuildUtils.buildVCToolInvokation(commandLine);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }

    @Nullable
    Map<String, String> getCommandLineProperties() {
      return null;
    }

    @Nullable
    Filter getCompileLogFilter(VirtualFile file, CompileCppOptions options) {
      return new VisualStudioBuildHandler.VCFormatFilter(file, options.getProject());
    }

    String buildProjectCompileOptions(Project project) {
      return null;
    }

    String getOutputFileName(VirtualFile file, CompileCppOptions compileOptions) {
      final String fileName = compileOptions.getOutputFileName();
      return fileName != null ? fileName:file.getNameWithoutExtension() + ".exe";
    }
  }

  private void invoke(final Project project, final VirtualFile file, CompileCppOptions _options) {
    CompileHandler handler = getCompileHandler(CompileCppDialog.getCurrentCompilerOption(project));

    final CompileCppDialog dialog = new CompileCppDialog(project, handler, _options);
    dialog.show();
    if(dialog.getExitCode() != DialogWrapper.OK_EXIT_CODE) return;

    _options = new CompileCppOptions() {
      public String getCompilerOptions() {
        return dialog.getCompilerOptions();
      }

      public String getProjectCompilerOptions() {
        return dialog.getProjectCompilerOptions();
      }

      public boolean doRun() {
        return dialog.doRun();
      }

      public String getOutputFileName() {
        final String fileName = dialog.getOutputFileName();
        return fileName.length() > 0? fileName:null;
      }

      public Project getProject() {
        return project;
      }

      public boolean isCppFile() {
        return "cpp".endsWith(file.getExtension());
      }
    };

    final CompileCppOptions options = _options;
    handler = getCompileHandler(CompileCppDialog.getCurrentCompilerOption(project));
    List<String> runCommand = handler.buildCommand(
      file,
      options
    );

    if (runCommand == null) throw new RuntimeException("Cannot invoke compilation");

    final Map<String, String> commandLineProperties = handler.getCommandLineProperties();

    final String baseForExecutableFile = file.getParent().getPath() + File.separatorChar;
    final String fileToExecute = baseForExecutableFile + handler.getOutputFileName(file, options);
    new File(fileToExecute).delete();

    final ConsoleBuilder consoleBuilderRef[] = new ConsoleBuilder[1];
    Runnable action = new Runnable() {
      public void run() {
        if (options.doRun() && new File(fileToExecute).exists()) {
          new ConsoleBuilder(
            "Run " + file.getName(),
            new BuildState(Arrays.asList(fileToExecute),new File(file.getParent().getPath()),commandLineProperties),
            project,
            null,
            new Runnable() {
              public void run() {
                invoke(project, file, options);
              }
            },
            new Runnable() {
              public void run() {
                consoleBuilderRef[0].start();
              }
            },
            null
          ).start();
        }
      }
    };

    final ConsoleBuilder consoleBuilder = new ConsoleBuilder(
      "Compile File " + file.getName(),
      new BuildState(runCommand,new File(file.getParent().getPath()), commandLineProperties),
      project,
      handler.getCompileLogFilter(file, options),
      new Runnable() {
        public void run() {
          invoke(project, file, options);
        }
      },
      null,
      action
    );
    consoleBuilderRef[0] = consoleBuilder;
    consoleBuilder.start();
  }

  private static CompileHandler getCompileHandler(CppSupportSettings.CompilerSelectOptions compilerOption) {
    CompileHandler handler;

    if (compilerOption == CppSupportSettings.CompilerSelectOptions.GCC ||
        (!SystemInfo.isWindows && compilerOption == CppSupportSettings.CompilerSelectOptions.AUTO)
      ) {
      handler = new GccCompileHandler();
    } else if (compilerOption == CppSupportSettings.CompilerSelectOptions.CLANG) {
      handler = new ClangCompileHandler();
    } else {
      handler = new VcCompileHandler();
    }
    return handler;
  }

  public void update(AnActionEvent e) {
    super.update(e);

    final Project project = e.getData(LangDataKeys.PROJECT);
    final VirtualFile file = e.getData(LangDataKeys.VIRTUAL_FILE);

    boolean visible = project != null &&
      file != null &&
      !file.isDirectory() &&
      file.getFileType() == CppSupportLoader.CPP_FILETYPE &&
      !Communicator.isHeaderFile(file);
    boolean enabled = visible;

    if (!visible) {
      visible = ActionPlaces.MAIN_MENU.equals(e.getPlace());
    }

    e.getPresentation().setEnabled(enabled);
    e.getPresentation().setVisible(visible);

    if (visible) {
      final String s = "Do c&ompile for " + (file != null ? file.getName():"selected c/c++ fileToCompile");
      e.getPresentation().setText(s);
      e.getPresentation().setDescription(s);
    }
  }

  static class CompileCppDialog extends DialogWrapper {
    private JPanel myPanel;
    private JTextField compileProperties;
    private JComboBox compilerSelector;
    private JCheckBox includeProjectCompileParametersCheckBox;
    private JTextField projectCompileParameters;
    private JCheckBox doRun;
    private JComboBox executableFileName;
    private Project project;

    static boolean lastRunStatus;
    
    protected CompileCppDialog(Project _project, CompileHandler compilerHandler, CompileCppOptions options) {
      super(_project, false);

      project = _project;
      setModal(true);

      doRun.addItemListener(new ItemListener() {
        public void itemStateChanged(ItemEvent e) {
          final boolean b = doRun.isSelected();
          executableFileName.setEnabled(b);
          executableFileName.setEditable(b);
        }
      });

      executableFileName.getEditor().setItem(options != null ? options.getOutputFileName() : "");
      doRun.setSelected(lastRunStatus);

      final CppSupportLoader loader = CppSupportLoader.getInstance(project);
      final String compileParameters = loader.getAdditionalCompileParameters();

      compileProperties.setText(
        (compileParameters != null && compileParameters.length() > 0 ?compileParameters + " ":"") + CompileHandler.MARKER
      );

      setTitle(CppBundle.message("compile.cpp.file.dialog.title"));

      compilerSelector.setModel(new DefaultComboBoxModel(CppSupportSettings.CompilerSelectOptions.values()));
      compilerSelector.setSelectedItem(getCurrentCompilerOption(project));

      setSelectedProjectCompile();

      includeProjectCompileParametersCheckBox.addItemListener(new ItemListener() {
        public void itemStateChanged(ItemEvent e) {
          setSelectedProjectCompile();
        }
      });

      includeProjectCompileParametersCheckBox.setSelected(loader.isIncludeProjectSettings());
      final String compileParametersText = compilerHandler.buildProjectCompileOptions(project);
      projectCompileParameters.setText(compileParametersText != null ? compileParametersText:"");

      init();
    }

    private void setSelectedProjectCompile() {
      boolean b = includeProjectCompileParametersCheckBox.isSelected();
      projectCompileParameters.setEditable(b);
      projectCompileParameters.setEnabled(b);
    }

    static CppSupportSettings.CompilerSelectOptions getCurrentCompilerOption(Project project) {
      return lastOptions != null ? lastOptions: CppSupportLoader.getInstance(project).getCompilerOptions();
    }

    String getCompilerOptions() {
      return compileProperties.getText();
    }

    String getProjectCompilerOptions() {
      return includeProjectCompileParametersCheckBox.isSelected() ? projectCompileParameters.getText():"";
    }

    boolean doRun() {
      return doRun.isSelected();
    }

    CppSupportSettings.CompilerSelectOptions getCompiler() {
      return (CppSupportSettings.CompilerSelectOptions) compilerSelector.getSelectedItem();
    }

    protected void doOKAction() {
      lastRunStatus = doRun.isSelected();
      
      final CppSupportLoader loader = CppSupportLoader.getInstance(project);
      loader.setIncludeProjectSettings(includeProjectCompileParametersCheckBox.isSelected());
      String s = compileProperties.getText();
      int index = s.lastIndexOf(CompileHandler.MARKER);
      if (index != -1) s = s.substring(0, index).trim();
      loader.setAdditionalCompileParameters(s.length() > 0 ? s:null);
      lastOptions = getCompiler();

      super.doOKAction();
    }

    @Nullable
    protected JComponent createCenterPanel() {
      return myPanel;
    }

    public String getOutputFileName() {
      return (String) executableFileName.getEditor().getItem();
    }
  }

  public static String getEscapedPathToFile(String path) {
    return BuildingCommandHelper.quote(path, SystemInfo.isWindows);
  }
}