/*
 * Copyright 2019 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.utils;

import static com.android.tools.idea.gradle.project.importing.GradleProjectImporter.ANDROID_PROJECT_TYPE;
import static com.intellij.util.ReflectionUtil.findAssignableField;
import static io.flutter.actions.AttachDebuggerAction.ATTACH_IS_ACTIVE;
import static io.flutter.actions.AttachDebuggerAction.findRunConfig;

import com.android.tools.idea.gradle.project.sync.GradleSyncListener;
import com.android.tools.idea.gradle.project.sync.GradleSyncState;
import com.intellij.ProjectTopics;
import com.intellij.debugger.engine.DebugProcess;
import com.intellij.debugger.engine.DebugProcessListener;
import com.intellij.debugger.impl.DebuggerManagerListener;
import com.intellij.debugger.impl.DebuggerSession;
import com.intellij.execution.RunManagerEx;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.ModuleListener;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectTypeService;
import com.intellij.util.ThreeState;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.messages.Topic;
import io.flutter.FlutterUtils;
import io.flutter.actions.AttachDebuggerAction;
import io.flutter.pub.PubRoot;
import io.flutter.run.SdkRunConfig;
import io.flutter.sdk.FlutterSdk;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Objects;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class AddToAppUtils {

  private AddToAppUtils() {
  }

  public static boolean initializeAndDetectFlutter(@NotNull Project project) {
    MessageBusConnection connection = project.getMessageBus().connect(project);
    // GRADLE_SYNC_TOPIC is not public in Android Studio 3.5. It is in 3.6. It isn't defined in 3.4.
    //noinspection unchecked
    Topic<GradleSyncListener> topic = getStaticFieldValue(GradleSyncState.class, Topic.class, "GRADLE_SYNC_TOPIC");
    assert topic != null;
    connection.subscribe(topic, makeSyncListener(project));

    if (!FlutterModuleUtils.hasFlutterModule(project)) {
      connection.subscribe(ProjectTopics.MODULES, new ModuleListener() {
        @Override
        public void moduleAdded(@NotNull Project proj, @NotNull Module mod) {
          if (AndroidUtils.FLUTTER_MODULE_NAME.equals(mod.getName()) ||
              (FlutterUtils.flutterGradleModuleName(project)).equals(mod.getName())) {
            //connection.disconnect(); TODO(messick) Test this deletion!
            AppExecutorUtil.getAppExecutorService().execute(() -> {
              AndroidUtils.enableCoeditIfAddToAppDetected(project);
            });
          }
        }
      });
      return false;
    }
    else {
      if (ANDROID_PROJECT_TYPE.equals(ProjectTypeService.getProjectType(project))) {
        // This is an add-to-app project.
        connection.subscribe(DebuggerManagerListener.TOPIC, makeAddToAppAttachListener(project));
      }
    }
    return true;
  }

  // Derived from the method in ReflectionUtil, with the addition of setAccessible().
  public static <T> T getStaticFieldValue(@NotNull Class objectClass,
                                          @Nullable("null means any type") Class<T> fieldType,
                                          @NotNull @NonNls String fieldName) {
    try {
      final Field field = findAssignableField(objectClass, fieldType, fieldName);
      if (!Modifier.isStatic(field.getModifiers())) {
        throw new IllegalArgumentException("Field " + objectClass + "." + fieldName + " is not static");
      }
      field.setAccessible(true);
      //noinspection unchecked
      return (T)field.get(null);
    }
    catch (NoSuchFieldException | IllegalAccessException e) {
      return null;
    }
  }

  @NotNull
  private static GradleSyncListener makeSyncListener(@NotNull Project project) {
    return new GradleSyncListener() {
      @Override
      public void syncSucceeded(@NotNull Project project) {
        AndroidUtils.checkDartSupport(project);
      }

      @Override
      public void syncFailed(@NotNull Project project, @NotNull String errorMessage) {
        AndroidUtils.checkDartSupport(project);
      }

      @Override
      public void syncSkipped(@NotNull Project project) {
        AndroidUtils.checkDartSupport(project);
      }

      @SuppressWarnings("override")
      public void sourceGenerationFinished(@NotNull Project project) {
      }
    };
  }

  @NotNull
  private static DebuggerManagerListener makeAddToAppAttachListener(@NotNull Project project) {

    return new DebuggerManagerListener() {

      DebugProcessListener dpl = new DebugProcessListener() {
        @Override
        public void processDetached(@NotNull DebugProcess process, boolean closedByUser) {
          ThreeState state = project.getUserData(ATTACH_IS_ACTIVE);
          if (state != null) {
            project.putUserData(ATTACH_IS_ACTIVE, null);
          }
        }

        @Override
        public void processAttached(@NotNull DebugProcess process) {
          if (project.getUserData(ATTACH_IS_ACTIVE) != null) {
            return;
          }
          // Launch flutter attach if a run config can be found.
          if (findRunConfig(project) == null) {
            // Either there is no Flutter run config or there are more than one.
            if (RunManagerEx.getInstanceEx(project).getSelectedConfiguration() instanceof SdkRunConfig) {
              // The selected run config at this point is not Flutter, so we can't start the process automatically.
              return;
            }
          }
          FlutterSdk sdk = FlutterSdk.getFlutterSdk(project);
          if (sdk == null) {
            return;
          }
          // If needed, DataContext could be saved by FlutterReloadManager.beforeActionPerformed() in project user data.
          DataContext context = DataContext.EMPTY_CONTEXT;
          List<Module> modules = FlutterModuleUtils.findModulesWithFlutterContents(project);
          assert modules.size() == 1; // TODO(messick) Need to change this if multiple :flutter sub-projects supported.
          PubRoot pubRoot = PubRoot.forDirectory(Objects.requireNonNull(modules.get(0).getModuleFile()).getParent());
          Application app = ApplicationManager.getApplication();
          project.putUserData(ATTACH_IS_ACTIVE, ThreeState.fromBoolean(true));
          // Note: Using block comments to preserve formatting.
          app.invokeLater( /* After the Android launch completes, */
            () -> app.executeOnPooledThread( /* but not on the EDT, */
              () -> app.runReadAction( /* with read access, */
                () -> new AttachDebuggerAction().startCommand(project, sdk, pubRoot, context)))); /* attach. */
        }
      };

      @Override
      public void sessionCreated(DebuggerSession session) {
        session.getProcess().addDebugProcessListener(dpl);
      }

      @Override
      public void sessionRemoved(DebuggerSession session) {
        session.getProcess().removeDebugProcessListener(dpl);
      }
    };
  }
}