/*
 * Copyright 2017 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 com.intellij.execution.RunManager;
import com.intellij.execution.RunnerAndConfigurationSettings;
import com.intellij.facet.Facet;
import com.intellij.facet.FacetManager;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleType;
import com.intellij.openapi.module.ModuleTypeManager;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.ui.EditorNotifications;
import com.intellij.util.PlatformUtils;
import com.jetbrains.lang.dart.sdk.DartSdk;
import io.flutter.FlutterUtils;
import io.flutter.actions.FlutterBuildActionGroup;
import io.flutter.bazel.Workspace;
import io.flutter.bazel.WorkspaceCache;
import io.flutter.dart.DartPlugin;
import io.flutter.pub.PubRoot;
import io.flutter.pub.PubRootCache;
import io.flutter.pub.PubRoots;
import io.flutter.run.FlutterRunConfigurationType;
import io.flutter.run.SdkFields;
import io.flutter.run.SdkRunConfig;
import io.flutter.sdk.FlutterSdk;
import io.flutter.sdk.FlutterSdkUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;

public class FlutterModuleUtils {
  public static final String DEPRECATED_FLUTTER_MODULE_TYPE_ID = "WEB_MODULE";

  private FlutterModuleUtils() {
  }

  /**
   * This provides the {@link ModuleType} ID for Flutter modules to be assigned by the {@link io.flutter.module.FlutterModuleBuilder} and
   * elsewhere in the Flutter plugin.
   * <p/>
   * For Flutter module detection however, {@link ModuleType}s should not be used to determine Flutterness.
   */
  @SuppressWarnings("SameReturnValue")
  @NotNull
  public static String getModuleTypeIDForFlutter() {
    return "JAVA_MODULE";
  }

  public static ModuleType<?> getFlutterModuleType() {
    return ModuleTypeManager.getInstance().findByID(getModuleTypeIDForFlutter());
  }

  /**
   * Return true if the passed module is of a Flutter type. Before version M16 this plugin had its own Flutter {@link ModuleType}.
   * Post M16 a Flutter module is defined by the following:
   * <p>
   * <code>
   * [Flutter support enabled for a module] ===
   * [Dart support enabled && referenced Dart SDK is the one inside a Flutter SDK]
   * </code>
   */
  public static boolean isFlutterModule(@Nullable final Module module) {
    if (module == null || module.isDisposed()) return false;

    if (PlatformUtils.isIntelliJ() || FlutterUtils.isAndroidStudio()) {
      // [Flutter support enabled for a module] ===
      //   [Dart support enabled && referenced Dart SDK is the one inside a Flutter SDK]
      final DartSdk dartSdk = DartPlugin.getDartSdk(module.getProject());
      final String dartSdkPath = dartSdk != null ? dartSdk.getHomePath() : null;
      return validDartSdkPath(dartSdkPath) && DartPlugin.isDartSdkEnabled(module);
    }
    else {
      // If not IntelliJ, assume a small IDE (no multi-module project support).
      return declaresFlutter(module);
    }
  }

  private static boolean validDartSdkPath(String path) {
    return path != null &&
           (path.endsWith(FlutterSdk.DART_SDK_SUFFIX) ||
            path.endsWith(FlutterSdk.LINUX_DART_SUFFIX) ||
            path.endsWith(FlutterSdk.MAC_DART_SUFFIX));
  }

  public static boolean hasInternalDartSdkPath(Project project) {
    final DartSdk dartSdk = DartPlugin.getDartSdk(project);
    final String dartSdkPath = dartSdk != null ? dartSdk.getHomePath() : "";
    return dartSdkPath.endsWith(FlutterSdk.LINUX_DART_SUFFIX) || dartSdkPath.endsWith(FlutterSdk.MAC_DART_SUFFIX);
  }

  public static boolean hasFlutterModule(@NotNull Project project) {
    if (project.isDisposed()) return false;

    return CollectionUtils.anyMatch(getModules(project), FlutterModuleUtils::isFlutterModule);
  }

  public static boolean isInFlutterModule(@NotNull PsiElement element) {
    return isFlutterModule(ModuleUtilCore.findModuleForPsiElement(element));
  }

  /**
   * Return the Flutter {@link Workspace} if there is at least one module that is determined to be a Flutter module by the workspace, and
   * has the Dart SDK enabled module.
   */
  @Nullable
  public static Workspace getFlutterBazelWorkspace(@Nullable Project project) {
    if (project == null || project.isDisposed()) return null;

    final Workspace workspace = WorkspaceCache.getInstance(project).get();
    if (workspace == null) return null;

    for (Module module : getModules(project)) {
      if (DartPlugin.isDartSdkEnabled(module)) {
        return workspace;
      }
    }

    return null;
  }

  /**
   * Return true if the passed {@link Project} is a Bazel Flutter {@link Project}. If the {@link Workspace} is needed after this call,
   * {@link #getFlutterBazelWorkspace(Project)} should be used.
   */
  public static boolean isFlutterBazelProject(@Nullable Project project) {
    return getFlutterBazelWorkspace(project) != null;
  }

  @Nullable
  public static VirtualFile findXcodeProjectFile(@NotNull Project project) {
    if (project.isDisposed()) return null;

    // Look for XCode metadata file in `ios/`.
    for (PubRoot root : PubRoots.forProject(project)) {
      final VirtualFile dir = root.getiOsDir();
      final VirtualFile file = findPreferedXcodeMetadataFile(dir);
      if (file != null) {
        return file;
      }
    }

    // Look for XCode metadata in `example/ios/`.
    for (PubRoot root : PubRoots.forProject(project)) {
      final VirtualFile exampleDir = root.getExampleDir();
      if (exampleDir != null) {
        VirtualFile iosDir = exampleDir.findChild("ios");
        if (iosDir == null) {
          iosDir = exampleDir.findChild(".ios");
        }
        final VirtualFile file = findPreferedXcodeMetadataFile(iosDir);
        if (file != null) {
          return file;
        }
      }
    }

    return null;
  }

  @Nullable
  private static VirtualFile findPreferedXcodeMetadataFile(@Nullable VirtualFile iosDir) {
    if (iosDir != null) {
      // Prefer .xcworkspace.
      for (VirtualFile child : iosDir.getChildren()) {
        if (FlutterUtils.isXcodeWorkspaceFileName(child.getName())) {
          return child;
        }
      }
      // But fall-back to a project.
      for (VirtualFile child : iosDir.getChildren()) {
        if (FlutterUtils.isXcodeProjectFileName(child.getName())) {
          return child;
        }
      }
    }
    return null;
  }

  @NotNull
  public static Module[] getModules(@NotNull Project project) {
    // A disposed project has no modules.
    if (project.isDisposed()) return Module.EMPTY_ARRAY;

    return ModuleManager.getInstance(project).getModules();
  }

  /**
   * Check if any module in this project {@link #declaresFlutter(Module)}.
   */
  public static boolean declaresFlutter(@NotNull Project project) {
    if (project.isDisposed()) return false;
    return CollectionUtils.anyMatch(getModules(project), FlutterModuleUtils::declaresFlutter);
  }

  /**
   * Ensures a Flutter run configuration is selected in the run pull down.
   */
  public static void ensureRunConfigSelected(@NotNull Project project) {
    if (project.isDisposed()) return;
    final FlutterRunConfigurationType configType = FlutterRunConfigurationType.getInstance();

    final RunManager runManager = RunManager.getInstance(project);
    if (!runManager.getConfigurationsList(configType).isEmpty()) {
      if (runManager.getSelectedConfiguration() == null) {
        final List<RunnerAndConfigurationSettings> flutterConfigs = runManager.getConfigurationSettingsList(configType);
        if (!flutterConfigs.isEmpty()) {
          runManager.setSelectedConfiguration(flutterConfigs.get(0));
        }
      }
    }
  }

  /**
   * Creates a Flutter run configuration if none exists.
   */
  public static void autoCreateRunConfig(@NotNull Project project, @NotNull PubRoot root) {
    assert ApplicationManager.getApplication().isReadAccessAllowed();
    if (project.isDisposed()) return;

    VirtualFile main = root.getLibMain();
    if (main == null || !main.exists()) {
      // Check for example main.dart in plugins
      main = root.getExampleLibMain();
      if (main == null || !main.exists()) {
        return;
      }
    }

    final FlutterRunConfigurationType configType = FlutterRunConfigurationType.getInstance();
    final RunManager runManager = RunManager.getInstance(project);
    if (!runManager.getConfigurationsList(configType).isEmpty()) {
      // Don't create a run config if one already exists.
      return;
    }

    final RunnerAndConfigurationSettings settings = runManager.createConfiguration(project.getName(), configType.getFactory());
    final SdkRunConfig config = (SdkRunConfig)settings.getConfiguration();

    // Set config name.
    config.setName("main.dart");

    // Set fields.
    final SdkFields fields = new SdkFields();
    fields.setFilePath(main.getPath());
    config.setFields(fields);

    runManager.addConfiguration(settings, false);
    runManager.setSelectedConfiguration(settings);
  }

  /**
   * If no files are open, show lib/main.dart for the given PubRoot.
   */
  public static void autoShowMain(@NotNull Project project, @NotNull PubRoot root) {
    if (project.isDisposed()) return;

    final VirtualFile main = root.getFileToOpen();
    if (main == null) return;

    DumbService.getInstance(project).runWhenSmart(() -> {
      final FileEditorManager manager = FileEditorManager.getInstance(project);
      if (manager.getAllEditors().length == 0) {
        manager.openFile(main, true);
      }
    });
  }

  /**
   * Introspect into the module's content roots, looking for a pubspec.yaml that references flutter.
   * <p/>
   * True is returned if any of the PubRoots associated with the {@link Module} have a pubspec that declares flutter.
   */
  public static boolean declaresFlutter(@NotNull Module module) {
    final PubRootCache cache = PubRootCache.getInstance(module.getProject());

    for (PubRoot root : cache.getRoots(module)) {
      if (root.declaresFlutter()) {
        return true;
      }
    }

    return false;
  }

  /**
   * Find flutter modules.
   * <p>
   * Flutter modules are defined as:
   * 1. being tagged with the #FlutterModuleType, or
   * 2. containing a pubspec that #declaresFlutterDependency
   */
  @NotNull
  public static List<Module> findModulesWithFlutterContents(@NotNull Project project) {
    return CollectionUtils.filter(getModules(project), module -> isFlutterModule(module) || declaresFlutter(module));
  }

  public static boolean convertFromDeprecatedModuleType(@NotNull Project project) {
    boolean modulesConverted = false;

    // Only automatically convert from older module types to JAVA_MODULE types if we're running in Android Studio.
    if (FlutterUtils.isAndroidStudio()) {
      for (Module module : getModules(project)) {
        if (isDeprecatedFlutterModuleType(module)) {
          setFlutterModuleType(module);
          modulesConverted = true;
        }
      }
    }

    return modulesConverted;
  }

  public static boolean isDeprecatedFlutterModuleType(@NotNull Module module) {
    if (!DEPRECATED_FLUTTER_MODULE_TYPE_ID.equals(module.getOptionValue("type"))) {
      return false;
    }

    // Validate that the pubspec references flutter.
    return declaresFlutter(module);
  }

  public static boolean isInFlutterAndroidModule(@NotNull Project project, @NotNull VirtualFile file) {
    final Module module = FlutterBuildActionGroup.findFlutterModule(project, file);
    if (module != null) {
      for (Facet<?> facet : FacetManager.getInstance(module).getAllFacets()) {
        if ("Android".equals(facet.getName())) {
          return declaresFlutter(project);
        }
      }
    }
    return false;
  }

  /**
   * Set the passed module to the module type used by Flutter, defined by {@link #getModuleTypeIDForFlutter()}.
   */
  public static void setFlutterModuleType(@NotNull Module module) {
    module.setOption(Module.ELEMENT_TYPE, getModuleTypeIDForFlutter());
  }

  public static void setFlutterModuleAndReload(@NotNull Module module, @NotNull Project project) {
    if (project.isDisposed()) return;

    setFlutterModuleType(module);
    enableDartSDK(module);
    project.save();

    EditorNotifications.getInstance(project).updateAllNotifications();
    ProjectManager.getInstance().reloadProject(project);
  }

  public static void enableDartSDK(@NotNull Module module) {
    if (DartPlugin.isDartSdkEnabled(module)) {
      return;
    }

    // parse the .packages file
    String sdkPath = FlutterSdkUtil.guessFlutterSdkFromPackagesFile(module);
    if (sdkPath != null) {
      FlutterSdkUtil.updateKnownSdkPaths(sdkPath);
    }

    // try and locate flutter on the path
    if (sdkPath == null) {
      sdkPath = FlutterSdkUtil.locateSdkFromPath();
      if (sdkPath != null) {
        FlutterSdkUtil.updateKnownSdkPaths(sdkPath);
      }
    }

    if (sdkPath == null) {
      final String[] flutterSdkPaths = FlutterSdkUtil.getKnownFlutterSdkPaths();
      if (flutterSdkPaths.length > 0) {
        sdkPath = flutterSdkPaths[0];
      }
    }

    if (sdkPath != null) {
      final FlutterSdk flutterSdk = FlutterSdk.forPath(sdkPath);
      if (flutterSdk == null) {
        return;
      }
      final String dartSdkPath = flutterSdk.getDartSdkPath();
      if (dartSdkPath == null) {
        return; // Not cached. TODO call flutterSdk.sync() here?
      }
      ApplicationManager.getApplication().runWriteAction(() -> {
        DartPlugin.ensureDartSdkConfigured(module.getProject(), dartSdkPath);
        DartPlugin.enableDartSdk(module);
      });
    }
  }
}