/*
 * 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.project;

import static com.intellij.openapi.util.io.FileUtilRt.toSystemIndependentName;
import static io.flutter.FlutterUtils.disableGradleProjectMigrationNotification;

import com.android.repository.io.FileOpUtils;
import com.intellij.conversion.ConversionListener;
import com.intellij.conversion.ConversionService;
import com.intellij.execution.OutputListener;
import com.intellij.ide.RecentProjectsManager;
import com.intellij.ide.highlighter.ModuleFileType;
import com.intellij.ide.projectView.ProjectView;
import com.intellij.ide.projectView.impl.ProjectViewPane;
import com.intellij.ide.util.projectWizard.ModuleWizardStep;
import com.intellij.ide.util.projectWizard.ProjectWizardUtil;
import com.intellij.ide.util.projectWizard.SettingsStep;
import com.intellij.ide.util.projectWizard.WizardContext;
import com.intellij.idea.ActionsBundle;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectTypeService;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.openapi.wm.IdeFrame;
import com.intellij.openapi.wm.ToolWindowId;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.platform.PlatformProjectOpenProcessor;
import io.flutter.FlutterMessages;
import io.flutter.FlutterUtils;
import io.flutter.ProjectOpenActivity;
import io.flutter.module.FlutterModuleBuilder;
import io.flutter.pub.PubRoot;
import io.flutter.sdk.FlutterCreateAdditionalSettings;
import io.flutter.sdk.FlutterSdk;
import io.flutter.utils.AndroidUtils;
import io.flutter.utils.FlutterModuleUtils;
import java.io.File;
import java.nio.file.Paths;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Create a Flutter project.
 * <p>
 * There are four types of Flutter projects: applications, plugins, modules, and packages. Each
 * can be represented two different ways in IntelliJ. A Flutter project can be:
 * <ul>
 * <li>a module in an IntelliJ project. @see #createModule()</li>
 * <li>a top-level IntelliJ project. @see @createProject()</li>
 * </ul>
 */
public class FlutterProjectCreator {
  private static final Logger LOG = Logger.getInstance(FlutterProjectCreator.class);
  private static final String SEPARATOR = "/";
  @NotNull private final FlutterProjectModel myModel;

  public FlutterProjectCreator(@NotNull FlutterProjectModel model) {
    myModel = model;
  }

  public void createModule() {
    Project project = myModel.project().getValue();
    VirtualFile baseDir = project.getBaseDir();
    if (baseDir == null) {
      return; // Project was deleted.
    }
    String moduleName, contentRoot;
    if (AndroidUtils.isAndroidProject(project)) {
      VirtualFile location = getLocationFromModel(null, false);
      if (location == null) {
        return;
      }
      moduleName = myModel.projectName().get();
      contentRoot = location.getPath();
    }
    else {
      String baseDirPath = baseDir.getPath();
      moduleName = ProjectWizardUtil.findNonExistingFileName(baseDirPath, myModel.projectName().get(), "");
      contentRoot = baseDirPath + "/" + moduleName;
    }
    File location = new File(contentRoot);
    if (!location.exists() && !location.mkdirs()) {
      String message = ActionsBundle.message("action.NewDirectoryProject.cannot.create.dir", location.getAbsolutePath());
      Messages.showErrorDialog(project, message, ActionsBundle.message("action.NewDirectoryProject.title"));
      return;
    }

    // ModuleBuilder mixes UI and model code too much to easily reuse. We have to override a bunch of stuff to ensure
    // that methods which expect a live UI do not get called.
    FlutterModuleBuilder builder = new FlutterModuleBuilder() {
      @NotNull
      @Override
      public FlutterCreateAdditionalSettings getAdditionalSettings() {
        return makeAdditionalSettings();
      }

      @Override
      protected FlutterSdk getFlutterSdk() {
        return FlutterSdk.forPath(myModel.flutterSdk().get());
      }

      @Override
      public boolean validate(Project current, Project dest) {
        return true;
      }

      @Override
      public ModuleWizardStep getCustomOptionsStep(final WizardContext context, final Disposable parentDisposable) {
        return null;
      }

      @Override
      public void setFlutterSdkPath(String s) {
      }

      @Override
      public ModuleWizardStep modifySettingsStep(@NotNull SettingsStep settingsStep) {
        return null;
      }
    };
    builder.setName(myModel.projectName().get());
    builder.setModuleFilePath(toSystemIndependentName(contentRoot) + "/" + moduleName + ModuleFileType.DOT_DEFAULT_EXTENSION);
    // TODO(messick): Refactor commitModule(). We're getting "Already disposed: Module" errors when a Flutter module is added
    // to an Android app (as a module using the new module wizard).
    builder.commitModule(project, null);
  }

  public void createProject() {
    IdeFrame frame = IdeFocusManager.getGlobalInstance().getLastFocusedFrame();
    final Project projectToClose = frame != null ? frame.getProject() : null;

    VirtualFile baseDir = getLocationFromModel(projectToClose, true);
    if (baseDir == null) {
      return;
    }
    //noinspection ConstantConditions (Keep this refresh even if the converter is removed.)
    VfsUtil.markDirtyAndRefresh(false, true, true, baseDir);

    // Create the project files using 'flutter create'.
    FlutterSdk sdk = FlutterSdk.forPath(myModel.flutterSdk().get());
    if (sdk == null) {
      FlutterMessages.showError("Error creating project", myModel.flutterSdk().get() + " is not a valid Flutter SDK");
      return;
    }
    final OutputListener listener = new OutputListener();
    // TODO(messick,pq): Refactor createFiles() to accept a logging interface instead of module, and display it in the wizard.
    ProgressManager progress = ProgressManager.getInstance();
    AtomicReference<PubRoot> result = new AtomicReference<>(null);
    progress.runProcessWithProgressSynchronously(() -> {
      progress.getProgressIndicator().setIndeterminate(true);
      sdk.createFiles(baseDir, null, listener, makeAdditionalSettings());
      VfsUtil.markDirtyAndRefresh(false, true, true, baseDir);
      result.set(PubRoot.forDirectory(baseDir));
    }, "Creating Flutter Project", false, null);
    PubRoot root = result.get();
    if (root == null) {
      String stderr = listener.getOutput().getStderr();
      FlutterMessages.showError("Error creating project", stderr.isEmpty() ? "Flutter create command was unsuccessful" : stderr);
      return;
    }

    Project project = null;
    if (myModel.shouldOpenNewWindow()) {
      // Open the project window.
      EnumSet<PlatformProjectOpenProcessor.Option> options = EnumSet.noneOf(PlatformProjectOpenProcessor.Option.class);
      project = PlatformProjectOpenProcessor.doOpenProject(baseDir, projectToClose, -1, null, options);
    }

    if (project != null) {
      // Android Studio changes the default project type, so we need to set it.
      ProjectTypeService.setProjectType(project, ProjectOpenActivity.FLUTTER_PROJECT_TYPE);
      disableGradleProjectMigrationNotification(project);
      disableUserConfig(project);
      Project proj = project;
      StartupManager.getInstance(project).registerPostStartupActivity(
        () -> ApplicationManager.getApplication().invokeLater(
          () -> {
            // We want to show the Project view, not the Android view since it doesn't make the Dart code visible.
            DumbService.getInstance(proj).runWhenSmart(
              () -> {
                ToolWindowManager.getInstance(proj).getToolWindow(ToolWindowId.PROJECT_VIEW).activate(null);
                ProjectView.getInstance(proj).changeView(ProjectViewPane.ID);
              });
          }, ModalityState.defaultModalityState()));
    }
  }

  private VirtualFile getLocationFromModel(@Nullable Project projectToClose, boolean saveLocation) {
    final File location = new File(FileUtil.toSystemDependentName(myModel.projectLocation().get()));
    if (!location.exists() && !location.mkdirs()) {
      String message = ActionsBundle.message("action.NewDirectoryProject.cannot.create.dir", location.getAbsolutePath());
      Messages.showErrorDialog(projectToClose, message, ActionsBundle.message("action.NewDirectoryProject.title"));
      return null;
    }
    final File baseFile = new File(location, myModel.projectName().get());
    //noinspection ResultOfMethodCallIgnored
    baseFile.mkdirs();
    final VirtualFile baseDir = ApplicationManager.getApplication().runWriteAction(
      (Computable<VirtualFile>)() -> LocalFileSystem.getInstance().refreshAndFindFileByIoFile(baseFile));
    if (baseDir == null) {
      FlutterUtils.warn(LOG, "Couldn't find '" + location + "' in VFS");
      return null;
    }
    if (saveLocation) {
      RecentProjectsManager.getInstance().setLastProjectCreationLocation(location.getPath());
    }
    return baseDir;
  }

  private FlutterCreateAdditionalSettings makeAdditionalSettings() {
    return new FlutterCreateAdditionalSettings.Builder()
      .setDescription(myModel.description().get().isEmpty() ? null : myModel.description().get())
      .setType(myModel.projectType().getValue())
      .setAndroidX(myModel.isGeneratingAndroidX())
      .setOrg(myModel.packageName().get().isEmpty() ? null : reversedOrgFromPackage(myModel.packageName().get()))
      .setKotlin(isNotModule() && myModel.useKotlin().get() ? true : null)
      .setSwift(isNotModule() && myModel.useSwift().get() ? true : null)
      .setOffline(myModel.isOfflineSelected().get())
      .build();
  }

  private boolean isNotModule() {
    return !myModel.isModule();
  }

  public static void disableUserConfig(Project project) {
    if (FlutterModuleUtils.declaresFlutter(project)) {
      for (Module module : ModuleManager.getInstance(project).getModules()) {
        final AndroidFacet facet = AndroidFacet.getInstance(module);
        if (facet == null) {
          continue;
        }
        facet.getProperties().ALLOW_USER_CONFIGURATION = false;
      }
    }
  }

  public static boolean finalValidityCheckPassed(@NotNull String projectLocation) {
    // See AS NewProjectModel.ProjectTemplateRenderer.doDryRun() for why this is necessary.
    boolean couldEnsureLocationExists = WriteCommandAction.runWriteCommandAction(null, (Computable<Boolean>)() -> {
      try {
        if (VfsUtil.createDirectoryIfMissing(projectLocation) != null && FileOpUtils.create().canWrite(new File(projectLocation))) {
          return true;
        }
      }
      catch (Exception e) {
        LOG.warn(String.format("Exception thrown when creating target project location: %1$s", projectLocation), e);
      }
      return false;
    });
    if (!couldEnsureLocationExists) {
      String msg =
        "Could not ensure the target project location exists and is accessible:\n\n%1$s\n\nPlease try to specify another path.";
      Messages.showErrorDialog(String.format(msg, projectLocation), "Error Creating Project");
      return false;
    }
    return true;
  }

  @Nullable
  public static String reversedOrgFromPackage(@NotNull String packageName) {
    if (packageName.isEmpty()) {
      return null;
    }
    int idx = packageName.lastIndexOf('.');
    if (idx <= 0) {
      return packageName;
    }
    return packageName.substring(0, idx);
  }

  public static class MyConversionListener implements ConversionListener {
    private boolean myConversionNeeded;
    private boolean myConverted;

    @Override
    public void conversionNeeded() {
      myConversionNeeded = true;
    }

    @Override
    public void successfullyConverted(@NotNull File backupDir) {
      myConverted = true;
    }

    @Override
    public void error(@NotNull String message) {
    }

    //@Override
    @SuppressWarnings("override")
    public void cannotWriteToFiles(@NotNull List<? extends File> readonlyFiles) {
    }

    public boolean isConversionNeeded() {
      return myConversionNeeded;
    }

    public boolean isConverted() {
      return myConverted;
    }
  }
}