/*
 * Copyright 2000-2015 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.openapi.externalSystem.service.project.manage;

import com.intellij.compiler.CompilerConfiguration;
import com.intellij.ide.util.projectWizard.ModuleBuilder;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.externalSystem.model.DataNode;
import com.intellij.openapi.externalSystem.model.ProjectKeys;
import com.intellij.openapi.externalSystem.model.ProjectSystemId;
import com.intellij.openapi.externalSystem.model.project.ExternalSystemSourceType;
import com.intellij.openapi.externalSystem.model.project.ModuleData;
import com.intellij.openapi.externalSystem.model.project.OrderAware;
import com.intellij.openapi.externalSystem.model.project.ProjectData;
import com.intellij.openapi.externalSystem.service.project.IdeModifiableModelsProvider;
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil;
import com.intellij.openapi.externalSystem.util.ExternalSystemBundle;
import com.intellij.openapi.externalSystem.util.ExternalSystemConstants;
import com.intellij.openapi.externalSystem.util.ExternalSystemUiUtil;
import com.intellij.openapi.module.ModifiableModuleModel;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.ui.CheckBoxList;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Consumer;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.ContainerUtilRt;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.util.*;
import java.util.List;

/**
 * @author Vladislav.Soroka
 * @since 8/5/2015
 */
public abstract class AbstractModuleDataService<E extends ModuleData> extends AbstractProjectDataService<E, Module> {

  public static final Key<ModuleData> MODULE_DATA_KEY = Key.create("MODULE_DATA_KEY");
  public static final Key<Module> MODULE_KEY = Key.create("LINKED_MODULE");
  public static final Key<Map<OrderEntry, OrderAware>> ORDERED_DATA_MAP_KEY = Key.create("ORDER_ENTRY_DATA_MAP");
  public static final Key<Set<String>> ORPHAN_MODULE_FILES = Key.create("ORPHAN_FILES");

  private static final Logger LOG = Logger.getInstance(AbstractModuleDataService.class);

  @Override
  public void importData(@NotNull final Collection<DataNode<E>> toImport,
                         @Nullable ProjectData projectData,
                         @NotNull final Project project,
                         @NotNull IdeModifiableModelsProvider modelsProvider) {
    if (toImport.isEmpty()) {
      return;
    }

    final Collection<DataNode<E>> toCreate = filterExistingModules(toImport, modelsProvider, project);
    if (!toCreate.isEmpty()) {
      createModules(toCreate, modelsProvider, project);
    }

    for (DataNode<E> node : toImport) {
      Module module = node.getUserData(MODULE_KEY);
      if (module != null) {
        String productionModuleId = node.getData().getProductionModuleId();
        modelsProvider.setTestModuleProperties(module, productionModuleId);
        setModuleOptions(module, node);
        ModifiableRootModel modifiableRootModel = modelsProvider.getModifiableRootModel(module);
        syncPaths(module, modifiableRootModel, node.getData());
        setLanguageLevel(modifiableRootModel, node.getData());
      }
    }

    for (DataNode<E> node : toImport) {
      Module module = node.getUserData(MODULE_KEY);
      if (module != null) {
        final String[] groupPath;
        groupPath = node.getData().getIdeModuleGroup();
        final ModifiableModuleModel modifiableModel = modelsProvider.getModifiableModuleModel();
        modifiableModel.setModuleGroupPath(module, groupPath);
      }
    }
  }

  private void createModules(@NotNull Collection<DataNode<E>> toCreate,
                             @NotNull IdeModifiableModelsProvider modelsProvider,
                             @NotNull Project project) {
    for (final DataNode<E> module : toCreate) {
      ModuleData data = module.getData();
      final Module created = modelsProvider.newModule(data);
      module.putUserData(MODULE_KEY, created);
      Set<String> orphanFiles = project.getUserData(ORPHAN_MODULE_FILES);
      if (orphanFiles != null) {
        orphanFiles.remove(created.getModuleFilePath());
      }

      // Ensure that the dependencies are clear (used to be not clear when manually removing the module and importing it via external system)
      final ModifiableRootModel modifiableRootModel = modelsProvider.getModifiableRootModel(created);
      modifiableRootModel.inheritSdk();

      RootPolicy<Object> visitor = new RootPolicy<Object>() {
        @Override
        public Object visitLibraryOrderEntry(LibraryOrderEntry libraryOrderEntry, Object value) {
          modifiableRootModel.removeOrderEntry(libraryOrderEntry);
          return value;
        }

        @Override
        public Object visitModuleOrderEntry(ModuleOrderEntry moduleOrderEntry, Object value) {
          modifiableRootModel.removeOrderEntry(moduleOrderEntry);
          return value;
        }
      };

      for (OrderEntry orderEntry : modifiableRootModel.getOrderEntries()) {
        orderEntry.accept(visitor, null);
      }
    }
  }

  @NotNull
  private Collection<DataNode<E>> filterExistingModules(@NotNull Collection<DataNode<E>> modules,
                                                        @NotNull IdeModifiableModelsProvider modelsProvider,
                                                        @NotNull Project project) {
    Collection<DataNode<E>> result = ContainerUtilRt.newArrayList();
    for (DataNode<E> node : modules) {
      ModuleData moduleData = node.getData();
      Module module = modelsProvider.findIdeModule(moduleData);
      if (module == null) {
        result.add(node);
      }
      else {
        node.putUserData(MODULE_KEY, module);
      }
    }
    return result;
  }

  private static void syncPaths(@NotNull Module module, @NotNull ModifiableRootModel modifiableModel, @NotNull ModuleData data) {
    CompilerModuleExtension extension = modifiableModel.getModuleExtension(CompilerModuleExtension.class);
    if (extension == null) {
      //modifiableModel.dispose();
      LOG.warn(String.format("Can't sync paths for module '%s'. Reason: no compiler extension is found for it", module.getName()));
      return;
    }
    String compileOutputPath = data.getCompileOutputPath(ExternalSystemSourceType.SOURCE);
    extension.setCompilerOutputPath(compileOutputPath != null ? VfsUtilCore.pathToUrl(compileOutputPath) : null);

    String testCompileOutputPath = data.getCompileOutputPath(ExternalSystemSourceType.TEST);
    extension.setCompilerOutputPathForTests(testCompileOutputPath != null ? VfsUtilCore.pathToUrl(testCompileOutputPath) : null);

    extension.inheritCompilerOutputPath(data.isInheritProjectCompileOutputPath());
  }

  @Override
  public void removeData(@NotNull final Computable<Collection<Module>> toRemoveComputable,
                         @NotNull final Collection<DataNode<E>> toIgnore,
                         @NotNull final ProjectData projectData,
                         @NotNull final Project project,
                         @NotNull final IdeModifiableModelsProvider modelsProvider) {
    final Collection<Module> toRemove = toRemoveComputable.compute();
    final List<Module> modules = new SmartList<>(toRemove);
    for (DataNode<E> moduleDataNode : toIgnore) {
      final Module module = modelsProvider.findIdeModule(moduleDataNode.getData());
      ContainerUtil.addIfNotNull(modules, module);
    }

    if (modules.isEmpty()) {
      return;
    }

    ContainerUtil.removeDuplicates(modules);

    for (Module module : modules) {
      if (module.isDisposed()) continue;
      unlinkModuleFromExternalSystem(module);
    }

    ruleOrphanModules(modules, project, projectData.getOwner(), modules1 -> {
      for (Module module : modules1) {
        if (module.isDisposed()) continue;
        String path = module.getModuleFilePath();
        final ModifiableModuleModel moduleModel = modelsProvider.getModifiableModuleModel();
        moduleModel.disposeModule(module);
        ModuleBuilder.deleteModuleFile(path);
      }
    });
  }

  /**
   * There is a possible case that an external module has been un-linked from ide project. There are two ways to process
   * ide modules which correspond to that external project:
   * <pre>
   * <ol>
   *   <li>Remove them from ide project as well;</li>
   *   <li>Keep them at ide project as well;</li>
   * </ol>
   * </pre>
   * This method handles that situation, i.e. it asks a user what should be done and acts accordingly.
   *
   * @param orphanModules    modules which correspond to the un-linked external project
   * @param project          current ide project
   * @param externalSystemId id of the external system which project has been un-linked from ide project
   */
  private static void ruleOrphanModules(@NotNull final List<Module> orphanModules,
                                        @NotNull final Project project,
                                        @NotNull final ProjectSystemId externalSystemId,
                                        @NotNull final Consumer<List<Module>> result) {
    ExternalSystemApiUtil.executeOnEdt(true, () -> {
      List<Module> toRemove = ContainerUtil.newSmartList();
      if (ApplicationManager.getApplication().isHeadlessEnvironment()) {
        toRemove.addAll(orphanModules);
      }
      else {
        final JPanel content = new JPanel(new GridBagLayout());
        content.add(new JLabel(ExternalSystemBundle.message("orphan.modules.text", externalSystemId.getReadableName())),
                    ExternalSystemUiUtil.getFillLineConstraints(0));

        final CheckBoxList<Module> orphanModulesList = new CheckBoxList<>();
        orphanModulesList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
        orphanModulesList.setItems(orphanModules, module -> module.getName());
        for (Module module : orphanModules) {
          orphanModulesList.setItemSelected(module, true);
        }
        orphanModulesList.setBorder(IdeBorderFactory.createEmptyBorder(8));
        content.add(orphanModulesList, ExternalSystemUiUtil.getFillLineConstraints(0));
        content.setBorder(IdeBorderFactory.createEmptyBorder(0, 0, 8, 0));

        DialogWrapper dialog = new DialogWrapper(project) {
          {
            setTitle(ExternalSystemBundle.message("import.title", externalSystemId.getReadableName()));
            init();
          }

          @Nullable
          @Override
          protected JComponent createCenterPanel() {
            return new JBScrollPane(content);
          }

          @NotNull
          protected Action[] createActions() {
            return new Action[]{getOKAction()};
          }
        };

        dialog.showAndGet();

        for (int i = 0; i < orphanModules.size(); i++) {
          Module module = orphanModules.get(i);
          if (orphanModulesList.isItemSelected(i)) {
            toRemove.add(module);
          }
        }
      }
      result.consume(toRemove);
    });
  }

  public static void unlinkModuleFromExternalSystem(@NotNull Module module) {
    module.clearOption(ExternalSystemConstants.EXTERNAL_SYSTEM_ID_KEY);
    module.clearOption(ExternalSystemConstants.LINKED_PROJECT_ID_KEY);
    module.clearOption(ExternalSystemConstants.LINKED_PROJECT_PATH_KEY);
    module.clearOption(ExternalSystemConstants.ROOT_PROJECT_PATH_KEY);
    module.clearOption(ExternalSystemConstants.EXTERNAL_SYSTEM_MODULE_GROUP_KEY);
    module.clearOption(ExternalSystemConstants.EXTERNAL_SYSTEM_MODULE_VERSION_KEY);
  }

  protected void setModuleOptions(Module module, DataNode<E> moduleDataNode) {
    ModuleData moduleData = moduleDataNode.getData();
    module.putUserData(MODULE_DATA_KEY, moduleData);

    module.setOption(ExternalSystemConstants.EXTERNAL_SYSTEM_ID_KEY, moduleData.getOwner().toString());
    module.setOption(ExternalSystemConstants.LINKED_PROJECT_ID_KEY, moduleData.getId());
    module.setOption(ExternalSystemConstants.LINKED_PROJECT_PATH_KEY, moduleData.getLinkedExternalProjectPath());
    final ProjectData projectData = moduleDataNode.getData(ProjectKeys.PROJECT);
    module.setOption(ExternalSystemConstants.ROOT_PROJECT_PATH_KEY, projectData != null ? projectData.getLinkedExternalProjectPath() : "");

    if (moduleData.getGroup() != null) {
      module.setOption(ExternalSystemConstants.EXTERNAL_SYSTEM_MODULE_GROUP_KEY, moduleData.getGroup());
    }
    if (moduleData.getVersion() != null) {
      module.setOption(ExternalSystemConstants.EXTERNAL_SYSTEM_MODULE_VERSION_KEY, moduleData.getVersion());
    }

    // clear maven option
    module.clearOption("org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule");
  }

  @Override
  public void postProcess(@NotNull Collection<DataNode<E>> toImport,
                          @Nullable ProjectData projectData,
                          @NotNull Project project,
                          @NotNull IdeModifiableModelsProvider modelsProvider) {
    for (DataNode<E> moduleDataNode : toImport) {
      final Module module = moduleDataNode.getUserData(MODULE_KEY);
      if (module == null) continue;
      final Map<OrderEntry, OrderAware> orderAwareMap = moduleDataNode.getUserData(ORDERED_DATA_MAP_KEY);
      if (orderAwareMap != null) {
        rearrangeOrderEntries(orderAwareMap, modelsProvider.getModifiableRootModel(module));
      }
      setBytecodeTargetLevel(project, module, moduleDataNode.getData());
    }

    for (Module module : modelsProvider.getModules()) {
      module.putUserData(MODULE_DATA_KEY, null);
    }
  }

  @Override
  public void onSuccessImport(@NotNull Project project) {
    final Set<String> orphanFiles = project.getUserData(ORPHAN_MODULE_FILES);
    if (orphanFiles != null && !orphanFiles.isEmpty()) {
      ExternalSystemApiUtil.executeOnEdt(false, () -> {
        for (String orphanFile : orphanFiles) {
          ModuleBuilder.deleteModuleFile(orphanFile);
        }
      });
      project.putUserData(ORPHAN_MODULE_FILES, null);
    }
  }

  protected void rearrangeOrderEntries(@NotNull Map<OrderEntry, OrderAware> orderEntryDataMap,
                                       @NotNull ModifiableRootModel modifiableRootModel) {
    final OrderEntry[] orderEntries = modifiableRootModel.getOrderEntries();
    final int length = orderEntries.length;
    final OrderEntry[] newOrder = new OrderEntry[length];
    final PriorityQueue<Pair<OrderEntry, OrderAware>> priorityQueue = new PriorityQueue<>(
      11, (o1, o2) -> {
      int order1 = o1.second.getOrder();
      int order2 = o2.second.getOrder();
      return order1 != order2 ? order1 < order2 ? -1 : 1 : 0;
    });

    int shift = 0;
    for (int i = 0; i < length; i++) {
      OrderEntry orderEntry = orderEntries[i];
      final OrderAware orderAware = orderEntryDataMap.get(orderEntry);
      if (orderAware == null) {
        newOrder[i] = orderEntry;
        shift++;
      }
      else {
        priorityQueue.add(Pair.create(orderEntry, orderAware));
      }
    }

    Pair<OrderEntry, OrderAware> pair;
    while ((pair = priorityQueue.poll()) != null) {
      final OrderEntry orderEntry = pair.first;
      final OrderAware orderAware = pair.second;
      final int order = orderAware.getOrder() != -1 ? orderAware.getOrder() : length - 1;
      final int newPlace = findNewPlace(newOrder, order - shift);
      assert newPlace != -1;
      newOrder[newPlace] = orderEntry;
    }

    if (LOG.isDebugEnabled()) {
      final boolean changed = !ArrayUtil.equals(orderEntries, newOrder, new Comparator<OrderEntry>() {
        @Override
        public int compare(OrderEntry o1, OrderEntry o2) {
          return o1.compareTo(o2);
        }
      });
      LOG.debug(String.format("rearrange status (%s): %s", modifiableRootModel.getModule(), changed ? "modified" : "not modified"));
    }
    modifiableRootModel.rearrangeOrderEntries(newOrder);
  }

  private static int findNewPlace(OrderEntry[] newOrder, int newIndex) {
    int idx = newIndex;
    while (idx < 0 || (idx < newOrder.length && newOrder[idx] != null)) {
      idx++;
    }
    if (idx >= newOrder.length) {
      idx = newIndex - 1;
      while (idx >= 0 && (idx >= newOrder.length || newOrder[idx] != null)) {
        idx--;
      }
    }
    return idx == -1 ? -1 : idx;
  }

  private void setLanguageLevel(@NotNull ModifiableRootModel modifiableRootModel, E data) {
    LanguageLevel level = LanguageLevel.parse(data.getSourceCompatibility());
    if (level != null) {
      try {
        modifiableRootModel.getModuleExtension(LanguageLevelModuleExtension.class).setLanguageLevel(level);
      }
      catch (IllegalArgumentException e) {
        LOG.debug(e);
      }
    }
  }

  private void setBytecodeTargetLevel(@NotNull Project project, @NotNull Module module, @NotNull E data) {
    String targetLevel = data.getTargetCompatibility();
    if (targetLevel != null) {
      CompilerConfiguration configuration = CompilerConfiguration.getInstance(project);
      configuration.setBytecodeTargetLevel(module, targetLevel);
    }
  }
}
