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

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.LibraryOrderEntry;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.ModuleRootManager;
import com.intellij.openapi.roots.OrderEntry;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.roots.impl.libraries.LibraryEx;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryProperties;
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.text.StringUtil;
import io.flutter.utils.FlutterModuleUtils;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * The shared code for managing a library. To use it, define a subclass that implements the required methods,
 * and calls #updateLibraryContent() with the library URLs.
 * It will need a LibraryType and LibraryProperties, which are registered in plugin.xml.
 *
 * @see FlutterPluginsLibraryManager
 */
public abstract class AbstractLibraryManager<K extends LibraryProperties> {

  @NotNull
  private final Project project;

  public AbstractLibraryManager(@NotNull Project project) {
    this.project = project;
  }

  protected void updateLibraryContent(@NotNull Set<String> contentUrls) {
    if (!FlutterModuleUtils.declaresFlutter(project)) {
      // If we have a Flutter library, remove it.
      final LibraryTable libraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(project);
      final Library existingLibrary = getLibraryByName(getLibraryName());
      if (existingLibrary != null) {
        WriteAction.compute(() -> {
          final LibraryTable.ModifiableModel libraryTableModel = libraryTable.getModifiableModel();
          libraryTableModel.removeLibrary(existingLibrary);
          libraryTableModel.commit();
          return null;
        });
      }
      return;
    }
    updateLibraryContent(getLibraryName(), contentUrls, null);
  }

  protected void updateLibraryContent(@NotNull String name,
                                      @NotNull Set<String> contentUrls,
                                      @SuppressWarnings("unused") @Nullable Set<String> sourceUrls) {
    // TODO(messick) Add support for source URLs.
    final LibraryTable libraryTable = LibraryTablesRegistrar.getInstance().getLibraryTable(project);
    final Library existingLibrary = getLibraryByName(name);

    final Library library = existingLibrary != null
                            ? existingLibrary
                            : WriteAction.compute(() -> {
                              final LibraryTable.ModifiableModel libraryTableModel = libraryTable.getModifiableModel();
                              final Library lib = libraryTableModel.createLibrary(
                                name,
                                getLibraryKind());
                              libraryTableModel.commit();
                              return lib;
                            });

    final Set<String> existingUrls = new HashSet<>(Arrays.asList(library.getUrls(OrderRootType.CLASSES)));
    if (contentUrls.containsAll(existingUrls) && existingUrls.containsAll(contentUrls)) {
      // No changes needed.
      return;
    }

    ApplicationManager.getApplication().runWriteAction(() -> {
      final LibraryEx.ModifiableModelEx model = (LibraryEx.ModifiableModelEx)library.getModifiableModel();

      final Set<String> existingCopy = new HashSet<>(existingUrls);
      existingUrls.removeAll(contentUrls);
      contentUrls.removeAll(existingCopy);

      for (String url : existingUrls) {
        model.removeRoot(url, OrderRootType.CLASSES);
      }

      for (String url : contentUrls) {
        model.addRoot(url, OrderRootType.CLASSES);
      }

      DumbService.getInstance(project).runWhenSmart(() -> ApplicationManager.getApplication().runWriteAction(() -> model.commit()));
    });

    updateModuleLibraryDependencies(library);
  }

  protected void updateModuleLibraryDependencies(@NotNull Library library) {
    for (final Module module : ModuleManager.getInstance(project).getModules()) {
      if (FlutterModuleUtils.declaresFlutter(module)) {
        addFlutterLibraryDependency(module, library);
      }
      else {
        removeFlutterLibraryDependency(module, library);
      }
    }
  }

  @NotNull
  protected Project getProject() {
    return project;
  }

  @NotNull
  protected abstract String getLibraryName();

  @NotNull
  protected abstract PersistentLibraryKind<K> getLibraryKind();

  protected static void addFlutterLibraryDependency(@NotNull Module module, @NotNull Library library) {
    final ModifiableRootModel modifiableModel = ModuleRootManager.getInstance(module).getModifiableModel();

    try {
      for (final OrderEntry orderEntry : modifiableModel.getOrderEntries()) {
        if (orderEntry instanceof LibraryOrderEntry &&
            LibraryTablesRegistrar.PROJECT_LEVEL.equals(((LibraryOrderEntry)orderEntry).getLibraryLevel()) &&
            StringUtil.equals(library.getName(), ((LibraryOrderEntry)orderEntry).getLibraryName())) {
          return; // dependency already exists
        }
      }

      modifiableModel.addLibraryEntry(library);

      ApplicationManager.getApplication().invokeAndWait(() -> WriteAction.run(modifiableModel::commit));
    }
    finally {
      if (!modifiableModel.isDisposed()) {
        modifiableModel.dispose();
      }
    }
  }

  protected static void removeFlutterLibraryDependency(@NotNull Module module, @NotNull Library library) {
    final ModifiableRootModel modifiableModel = ModuleRootManager.getInstance(module).getModifiableModel();

    try {
      boolean wasFound = false;

      for (final OrderEntry orderEntry : modifiableModel.getOrderEntries()) {
        if (orderEntry instanceof LibraryOrderEntry &&
            LibraryTablesRegistrar.PROJECT_LEVEL.equals(((LibraryOrderEntry)orderEntry).getLibraryLevel()) &&
            StringUtil.equals(library.getName(), ((LibraryOrderEntry)orderEntry).getLibraryName())) {
          wasFound = true;
          modifiableModel.removeOrderEntry(orderEntry);
        }
      }

      if (wasFound) {
        ApplicationManager.getApplication().invokeAndWait(() -> WriteAction.run(modifiableModel::commit));
      }
    }
    finally {
      if (!modifiableModel.isDisposed()) {
        modifiableModel.dispose();
      }
    }
  }

  @Nullable
  private Library getLibraryByName(String name) {
    return LibraryTablesRegistrar.getInstance().getLibraryTable(project).getLibraryByName(name);
  }
}