/*
 * Copyright 2018 The Chromium Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */
package io.flutter.actions;

import com.google.common.base.Joiner;
import com.intellij.execution.ExecutionListener;
import com.intellij.execution.ExecutionManager;
import com.intellij.execution.Executor;
import com.intellij.execution.ProgramRunnerUtil;
import com.intellij.execution.RunManagerEx;
import com.intellij.execution.RunnerAndConfigurationSettings;
import com.intellij.execution.configurations.RunConfiguration;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.runners.ExecutionEnvironment;
import com.intellij.execution.runners.ExecutionEnvironmentBuilder;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.util.ThreeState;
import com.intellij.util.messages.MessageBusConnection;
import io.flutter.FlutterConstants;
import io.flutter.FlutterInitializer;
import io.flutter.pub.PubRoot;
import io.flutter.run.FlutterLaunchMode;
import io.flutter.run.SdkAttachConfig;
import io.flutter.run.SdkFields;
import io.flutter.run.SdkRunConfig;
import io.flutter.sdk.FlutterSdk;
import io.flutter.sdk.FlutterSdkUtil;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.JTextPane;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class AttachDebuggerAction extends FlutterSdkAction {

  // Is 'flutter attach' running for a project?
  // The three states are true, false, and non-existent (null).
  //   true = run automatically via the debug listener in FlutterStudioStartupActivity
  //   false = run from button press here
  //   null = no attach process is running
  // The button is still required because there may be multiple Flutter run configs.
  public static final Key<ThreeState> ATTACH_IS_ACTIVE = new Key<>("attach-is-active");

  @Override
  public void startCommand(@NotNull Project project, @NotNull FlutterSdk sdk, @Nullable PubRoot root, @NotNull DataContext context) {
    // NOTE: When making changes here, consider making similar changes to RunFlutterAction.
    FlutterInitializer.sendAnalyticsAction(this);

    RunConfiguration configuration = findRunConfig(project);
    if (configuration == null) {
      RunnerAndConfigurationSettings settings = RunManagerEx.getInstanceEx(project).getSelectedConfiguration();
      if (settings == null) {
        showSelectConfigDialog();
        return;
      }
      configuration = settings.getConfiguration();
      if (!(configuration instanceof SdkRunConfig)) {
        if (project.isDefault() || !FlutterSdkUtil.hasFlutterModules(project)) {
          return;
        }
        showSelectConfigDialog();
        return;
      }
    }

    SdkAttachConfig sdkRunConfig = new SdkAttachConfig((SdkRunConfig)configuration);
    SdkFields fields = sdkRunConfig.getFields();
    String additionalArgs = fields.getAdditionalArgs();

    String flavorArg = null;
    if (fields.getBuildFlavor() != null) {
      flavorArg = "--flavor=" + fields.getBuildFlavor();
    }

    List<String> args = new ArrayList<>();
    if (additionalArgs != null) {
      args.add(additionalArgs);
    }
    if (flavorArg != null) {
      args.add(flavorArg);
    }
    if (!args.isEmpty()) {
      fields.setAdditionalArgs(Joiner.on(" ").join(args));
    }

    Executor executor = RunFlutterAction.getExecutor(ToolWindowId.DEBUG);
    if (executor == null) {
      return;
    }

    ExecutionEnvironmentBuilder builder = ExecutionEnvironmentBuilder.create(executor, sdkRunConfig);

    ExecutionEnvironment env = builder.activeTarget().dataContext(context).build();
    FlutterLaunchMode.addToEnvironment(env, FlutterLaunchMode.DEBUG);

    if (project.getUserData(ATTACH_IS_ACTIVE) == null) {
      project.putUserData(ATTACH_IS_ACTIVE, ThreeState.fromBoolean(false));
      onAttachTermination(project, (p) -> p.putUserData(ATTACH_IS_ACTIVE, null));
    }
    ProgramRunnerUtil.executeConfiguration(env, false, true);
  }

  @Override
  public void update(AnActionEvent e) {
    Project project = e.getProject();
    if (project == null || project.isDefault()) {
      super.update(e);
      return;
    }
    if (!FlutterSdkUtil.hasFlutterModules(project)) {
      // Hide this button in Android projects.
      e.getPresentation().setVisible(false);
      return;
    }
    RunConfiguration configuration = findRunConfig(project);
    boolean enabled;
    if (configuration == null) {
      RunnerAndConfigurationSettings settings = RunManagerEx.getInstanceEx(project).getSelectedConfiguration();
      if (settings == null) {
        enabled = false;
      }
      else {
        configuration = settings.getConfiguration();
        enabled = configuration instanceof SdkRunConfig;
      }
    }
    else {
      enabled = true;
    }
    if (enabled && (project.getUserData(ATTACH_IS_ACTIVE) != null)) {
      enabled = false;
    }
    e.getPresentation().setVisible(true);
    e.getPresentation().setEnabled(enabled);
  }

  @Nullable
  public static RunConfiguration findRunConfig(Project project) {
    // Look for a Flutter run config. If exactly one is found then return it otherwise return null.
    RunManagerEx mgr = RunManagerEx.getInstanceEx(project);
    List<RunConfiguration> configs = mgr.getAllConfigurationsList();
    int count = 0;
    RunConfiguration sdkConfig = null;
    for (RunConfiguration config : configs) {
      if (config instanceof SdkRunConfig) {
        count += 1;
        sdkConfig = config;
      }
    }
    return count == 1 ? sdkConfig : null;
  }

  private static void onAttachTermination(@NotNull Project project, @NotNull Consumer<Project> runner) {
    MessageBusConnection connection = project.getMessageBus().connect();

    // Need an ExecutionListener to clean up project-scoped state when the Stop button is clicked.
    connection.subscribe(ExecutionManager.EXECUTION_TOPIC, new ExecutionListener() {
      Object handler;

      @Override
      public void processStarted(@NotNull String executorId, @NotNull ExecutionEnvironment env, @NotNull ProcessHandler handler) {
        if (env.getRunProfile() instanceof SdkAttachConfig) {
          this.handler = handler;
        }
      }

      @Override
      public void processTerminated(@NotNull String executorId,
                                    @NotNull ExecutionEnvironment env,
                                    @NotNull ProcessHandler handler,
                                    int exitCode) {
        if (this.handler == handler) {
          runner.accept(project);
          connection.disconnect();
        }
      }
    });
  }

  private static void showSelectConfigDialog() {
    ApplicationManager.getApplication().invokeLater(() -> new SelectConfigDialog().show(), ModalityState.NON_MODAL);
  }

  private static class SelectConfigDialog extends DialogWrapper {
    private JPanel myPanel;
    private JTextPane myTextPane;

    SelectConfigDialog() {
      super(null, false, false);
      setTitle("Run Configuration");
      myPanel = new JPanel();
      myTextPane = new JTextPane();
      Messages.installHyperlinkSupport(myTextPane);
      String selectConfig = "<html><body>" +
                            "<p>The run configuration for the Flutter module must be selected." +
                            "<p>Please change the run configuration to the one created when the<br>" +
                            "module was created. See <a href=\"" +
                            FlutterConstants.URL_RUN_AND_DEBUG +
                            "\">the Flutter documentation</a> for more information.</body></html>";
      myTextPane.setText(selectConfig);
      myPanel.add(myTextPane);
      init();
      //noinspection ConstantConditions
      getButton(getCancelAction()).setVisible(false);
    }

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