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

import static com.google.wireless.android.sdk.stats.GradleSyncStats.Trigger.TRIGGER_PROJECT_MODIFIED;
import static io.flutter.android.AndroidModuleLibraryType.LIBRARY_KIND;
import static io.flutter.android.AndroidModuleLibraryType.LIBRARY_NAME;

import com.android.tools.idea.gradle.project.sync.GradleSyncInvoker;
import com.android.tools.idea.gradle.project.sync.GradleSyncListener;
import com.intellij.ProjectTopics;
import com.intellij.facet.FacetManager;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.TransactionGuard;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.impl.ProjectImpl;
import com.intellij.openapi.project.impl.ProjectManagerImpl;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.DependencyScope;
import com.intellij.openapi.roots.JavaProjectModelModifier;
import com.intellij.openapi.roots.ModuleRootEvent;
import com.intellij.openapi.roots.ModuleRootListener;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.roots.impl.IdeaProjectModelModifier;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar;
import com.intellij.openapi.roots.libraries.PersistentLibraryKind;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileContentsChangedAdapter;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.modules.CircularModuleDependenciesDetector;
import io.flutter.sdk.AbstractLibraryManager;
import io.flutter.sdk.FlutterSdkUtil;
import io.flutter.utils.FlutterModuleUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.AndroidSdkUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Manages the Android libraries. Add the libraries used by Android modules referenced in a project
 * into the Flutter project, so full editing support is available. Add dependencies to each library
 * to the Android modules. Add a dependency from the Android module to the Flutter module so the
 * libraries can be resolved. Do not add a dependency from the Flutter module to the libraries since
 * Java and Kotlin code are only found in the Android modules. Also set the project SDK to that used
 * by Android.
 * <p>
 * TODO(messick) Test with plugins and modules
 * These are not looking so good. Source files are not marked correctly. Re-check make-host-app-editable.
 *
 * @see AndroidModuleLibraryType
 * @see AndroidModuleLibraryProperties
 */
public class AndroidModuleLibraryManager extends AbstractLibraryManager<AndroidModuleLibraryProperties> {
  private static final Logger LOG = Logger.getInstance(AndroidModuleLibraryManager.class);
  private static final String BUILD_FILE_NAME = "build.gradle";
  private final AtomicBoolean isUpdating = new AtomicBoolean(false);

  public AndroidModuleLibraryManager(@NotNull Project project) {
    super(project);
  }

  public void update() {
    doGradleSync(getProject(), this::scheduleAddAndroidLibraryDeps);
  }

  private Void scheduleAddAndroidLibraryDeps(@NotNull Project androidProject) {
    ApplicationManager.getApplication().invokeLater(
      () -> addAndroidLibraryDependencies(androidProject),
      ModalityState.NON_MODAL);
    return null;
  }

  private void addAndroidLibraryDependencies(@NotNull Project androidProject) {
    for (Module flutterModule : FlutterModuleUtils.getModules(getProject())) {
      if (FlutterModuleUtils.isFlutterModule(flutterModule)) {
        for (Module module : ModuleManager.getInstance(androidProject).getModules()) {
          addAndroidLibraryDependencies(androidProject, module, flutterModule);
        }
      }
    }
    isUpdating.set(false);
  }

  private void addAndroidLibraryDependencies(@NotNull Project androidProject,
                                             @NotNull Module androidModule,
                                             @NotNull Module flutterModule) {
    AndroidSdkUtils.setupAndroidPlatformIfNecessary(androidModule, true);
    Sdk currentSdk = ModuleRootManager.getInstance(androidModule).getSdk();
    if (currentSdk != null) {
      // TODO(messick) Add sdk dependency on currentSdk if not already set
    }
    LibraryTable androidProjectLibraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(androidProject);
    Library[] androidProjectLibraries = androidProjectLibraryTable.getLibraries();
    LibraryTable flutterProjectLibraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(getProject());
    Library[] flutterProjectLibraries = flutterProjectLibraryTable.getLibraries();
    Set<String> knownLibraryNames = new HashSet<>(flutterProjectLibraries.length);
    for (Library lib : flutterProjectLibraries) {
      if (lib.getName() != null) {
        knownLibraryNames.add(lib.getName());
      }
    }
    for (Library library : androidProjectLibraries) {
      if (library.getName() != null && !knownLibraryNames.contains(library.getName())) {

        List<String> roots = Arrays.asList(library.getRootProvider().getUrls(OrderRootType.CLASSES));
        Set<String> filteredRoots = roots.stream().filter(s -> shouldIncludeRoot(s)).collect(Collectors.toSet());
        if (filteredRoots.isEmpty()) continue;

        HashSet<String> sources = new HashSet<>(Arrays.asList(library.getRootProvider().getUrls(OrderRootType.SOURCES)));

        updateLibraryContent(library.getName(), filteredRoots, sources);
        updateAndroidModuleLibraryDependencies(flutterModule);
      }
    }
  }

  @Override
  protected void updateModuleLibraryDependencies(@NotNull Library library) {
    for (final Module module : ModuleManager.getInstance(getProject()).getModules()) {
      // The logic is inverted wrt superclass.
      if (!FlutterModuleUtils.declaresFlutter(module)) {
        addFlutterLibraryDependency(module, library);
      }
      else {
        removeFlutterLibraryDependency(module, library);
      }
    }
  }

  private void updateAndroidModuleLibraryDependencies(Module flutterModule) {
    for (final Module module : ModuleManager.getInstance(getProject()).getModules()) {
      if (module != flutterModule) {
        if (null != FacetManager.getInstance(module).findFacet(AndroidFacet.ID, "Android")) {
          Object circularModules = CircularModuleDependenciesDetector.addingDependencyFormsCircularity(module, flutterModule);
          if (circularModules == null) {
            ModuleRootManager rootManager = ModuleRootManager.getInstance(module);
            if (!rootManager.isDependsOn(flutterModule)) {
              JavaProjectModelModifier[] modifiers = JavaProjectModelModifier.EP_NAME.getExtensions(getProject());
              for (JavaProjectModelModifier modifier : modifiers) {
                if (modifier instanceof IdeaProjectModelModifier) {
                  modifier.addModuleDependency(module, flutterModule, DependencyScope.COMPILE, false);
                }
              }
            }
          }
        }
      }
    }
  }

  @NotNull
  @Override
  protected String getLibraryName() {
    // This is not used since we create many libraries, not one.
    return LIBRARY_NAME;
  }

  @NotNull
  @Override
  protected PersistentLibraryKind<AndroidModuleLibraryProperties> getLibraryKind() {
    return LIBRARY_KIND;
  }

  private void scheduleUpdate() {
    if (isUpdating.get()) {
      return;
    }

    final Runnable runnable = this::updateAndroidLibraries;
    DumbService.getInstance(getProject()).smartInvokeLater(runnable, ModalityState.NON_MODAL);
  }

  private void updateAndroidLibraries() {
    if (!isUpdating.compareAndSet(false, true)) {
      return;
    }
    update();
  }

  private void doGradleSync(Project flutterProject, Function<Project, Void> callback) {
    // TODO(messick): Collect URLs for all Android modules, including those within plugins.
    VirtualFile dir = flutterProject.getBaseDir().findChild("android");
    if (dir == null) dir = flutterProject.getBaseDir().findChild(".android"); // For modules.
    if (dir == null) return;
    EmbeddedAndroidProject androidProject = new EmbeddedAndroidProject(Paths.get(FileUtilRt.toSystemIndependentName(dir.getPath())));
    androidProject.init41(null);
    Disposer.register(flutterProject, androidProject);

    GradleSyncListener listener = new GradleSyncListener() {
      @SuppressWarnings("override")
      public void syncTaskCreated(@NotNull Project project, @NotNull GradleSyncInvoker.Request request) {}

      // TODO(messick) Remove when 3.6 is stable.
      public void syncStarted(@NotNull Project project, boolean skipped, boolean sourceGenerationRequested) {}

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

      @Override
      public void syncSucceeded(@NotNull Project project) {
        if (isUpdating.get()) {
          callback.apply(androidProject);
        }
      }

      @Override
      public void syncFailed(@NotNull Project project, @NotNull String errorMessage) {
        isUpdating.set(false);
      }

      @Override
      public void syncSkipped(@NotNull Project project) {
        isUpdating.set(false);
      }
    };

    GradleSyncInvoker.Request request = new GradleSyncInvoker.Request(TRIGGER_PROJECT_MODIFIED);
    request.runInBackground = true;
    GradleSyncInvoker gradleSyncInvoker = ServiceManager.getService(GradleSyncInvoker.class);
    gradleSyncInvoker.requestProjectSync(androidProject, request, listener);
  }

  private static boolean shouldIncludeRoot(String path) {
    return !path.endsWith("res") && !path.contains("flutter.jar") && !path.contains("flutter-x86.jar");
  }

  @NotNull
  public static AndroidModuleLibraryManager getInstance(@NotNull final Project project) {
    return ServiceManager.getService(project, AndroidModuleLibraryManager.class);
  }

  public static void startWatching(@NotNull Project project) {
    // Start a process to monitor changes to Android dependencies and update the library content.
    if (project.isDefault()) {
      return;
    }
    if (hasAndroidDir(project)) {
      AndroidModuleLibraryManager manager = getInstance(project);
      VirtualFileManager.getInstance().addVirtualFileListener(new VirtualFileContentsChangedAdapter() {
        @Override
        protected void onFileChange(@NotNull VirtualFile file) {
          fileChanged(project, file);
        }

        @Override
        protected void onBeforeFileChange(@NotNull VirtualFile file) {
        }
      }, project);

      project.getMessageBus().connect().subscribe(ProjectTopics.PROJECT_ROOTS, new ModuleRootListener() {
        @Override
        public void rootsChanged(@NotNull ModuleRootEvent event) {
          manager.scheduleUpdate();
        }
      });
      manager.scheduleUpdate();
    }
  }

  private static boolean hasAndroidDir(Project project) {
    if (FlutterSdkUtil.hasFlutterModules(project)) {
      VirtualFile base = project.getBaseDir();
      VirtualFile dir = base.findChild("android");
      if (dir == null) dir = base.findChild(".android");
      return dir != null;
    }
    else {
      return false;
    }
  }

  private static void fileChanged(@NotNull final Project project, @NotNull final VirtualFile file) {
    if (!BUILD_FILE_NAME.equals(file.getName())) {
      return;
    }
    if (LocalFileSystem.getInstance() != file.getFileSystem() && !ApplicationManager.getApplication().isUnitTestMode()) {
      return;
    }
    if (!VfsUtilCore.isAncestor(project.getBaseDir(), file, true)) {
      return;
    }
    getInstance(project).scheduleUpdate();
  }

  private static class EmbeddedAndroidProject extends ProjectImpl {
    private Path path;

    protected EmbeddedAndroidProject(@NotNull Path filePath) {
      super(filePath, TEMPLATE_PROJECT_NAME);
      path = filePath;
    }

    static final String TEMPLATE_PROJECT_NAME = "_android";

    public void init41(@Nullable ProgressIndicator indicator) {
      boolean finished = false;
      try {
        //ProjectManagerImpl.initProject(path, this, true, null, null);
        Method method = ReflectionUtil
          .getDeclaredMethod(ProjectManagerImpl.class, "initProject", Path.class, ProjectImpl.class, boolean.class, Project.class,
                             ProgressIndicator.class);
        assert (method != null);
        try {
          method.invoke(null, path, this, true, null, null);
        }
        catch (IllegalAccessException | InvocationTargetException e) {
          throw new RuntimeException(e);
        }
        finished = true;
      }
      finally {
        if (!finished) {
          TransactionGuard.submitTransaction(this, () -> WriteAction.run(() -> Disposer.dispose(this)));
        }
      }
    }

    public void initPre41(@Nullable ProgressIndicator indicator) {
      boolean finished = false;
      try {
        //registerComponents();
        Method method = ReflectionUtil.getDeclaredMethod(ProjectImpl.class, "registerComponents");
        assert (method != null);
        try {
          method.invoke(this);
        }
        catch (IllegalAccessException | InvocationTargetException e) {
          throw new RuntimeException(e);
        }
        getStateStore().setPath(path, true, null);
        super.init(indicator);
        finished = true;
      }
      finally {
        if (!finished) {
          TransactionGuard.submitTransaction(this, () -> WriteAction.run(() -> Disposer.dispose(this)));
        }
      }
    }

    @Override
    public String toString() {
      return "Project" + (isDisposed() ? " (Disposed)" : " ") + TEMPLATE_PROJECT_NAME;
    }
  }
}