// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.

package com.intellij.ide.util.newProjectWizard;

import com.intellij.facet.impl.ui.libraries.LibraryCompositionSettings;
import com.intellij.facet.impl.ui.libraries.LibraryOptionsPanel;
import com.intellij.facet.ui.FacetBasedFrameworkSupportProvider;
import com.intellij.framework.FrameworkGroup;
import com.intellij.framework.FrameworkVersion;
import com.intellij.framework.addSupport.FrameworkSupportInModuleConfigurable;
import com.intellij.framework.addSupport.FrameworkSupportInModuleProvider;
import com.intellij.framework.addSupport.FrameworkVersionListener;
import com.intellij.ide.JavaUiBundle;
import com.intellij.ide.util.frameworkSupport.FrameworkSupportConfigurable;
import com.intellij.ide.util.frameworkSupport.FrameworkSupportProvider;
import com.intellij.ide.util.frameworkSupport.FrameworkSupportUtil;
import com.intellij.ide.util.newProjectWizard.impl.FrameworkSupportCommunicator;
import com.intellij.ide.util.newProjectWizard.impl.FrameworkSupportModelBase;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.roots.IdeaModifiableModelsProvider;
import com.intellij.openapi.roots.ModifiableRootModel;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.ui.configuration.projectRoot.LibrariesContainer;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.CheckedTreeNode;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.JBInsets;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Unmodifiable;

import javax.swing.*;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultTreeModel;
import java.awt.*;
import java.util.*;
import java.util.List;

public class AddSupportForFrameworksPanel implements Disposable {
  private static final Logger LOG = Logger.getInstance(AddSupportForFrameworksPanel.class);
  private static final @NonNls String EMPTY_CARD = "empty";
  private JPanel myMainPanel;
  private JPanel myFrameworksPanel;
  private JLabel myLabel;

  private List<? extends FrameworkSupportInModuleProvider> myProviders;
  private List<FrameworkSupportNodeBase<?>> myRoots;

  private final LibrariesContainer myLibrariesContainer;
  private final FrameworkSupportModelBase myModel;
  private final JPanel myOptionsPanel;
  private final FrameworksTree myFrameworksTree;
  private final Map<FrameworkSupportNode, FrameworkSupportOptionsComponent> myInitializedOptionsComponents = new HashMap<>();
  private final Map<FrameworkGroup<?>, JPanel> myInitializedGroupPanels = new HashMap<>();
  private FrameworkSupportNodeBase<?> myLastSelectedNode;

  private Collection<FrameworkSupportNodeBase<?>> myAssociatedFrameworks;
  private final @Nullable JPanel myAssociatedFrameworksPanel;

  public AddSupportForFrameworksPanel(final List<FrameworkSupportInModuleProvider> providers,
                                      final FrameworkSupportModelBase model, boolean vertical, @Nullable JPanel associatedFrameworksPanel) {
    myModel = model;
    myAssociatedFrameworksPanel = associatedFrameworksPanel;
    myLibrariesContainer = model.getLibrariesContainer();

    myLabel.setVisible(!vertical);
    Splitter splitter = vertical ? new Splitter(true, 0.6f, 0.2f, 0.8f) : new Splitter(false, 0.3f, 0.3f, 0.7f);
    splitter.setHonorComponentsMinimumSize(false);
    myFrameworksTree = new FrameworksTree(model) {
      @Override
      protected void onNodeStateChanged(CheckedTreeNode node) {
        if (!(node instanceof FrameworkSupportNode frameworkSupportNode)) return;

        if (frameworkSupportNode == getSelectedNode()) {
          updateOptionsPanel();
        }
        final FrameworkSupportInModuleConfigurable configurable = frameworkSupportNode.getConfigurable();
        configurable.onFrameworkSelectionChanged(node.isChecked());
        myModel.onFrameworkSelectionChanged(frameworkSupportNode);
        onFrameworkStateChanged();
      }
    };
    model.addFrameworkVersionListener(new FrameworkVersionListener() {
      @Override
      public void versionChanged(FrameworkVersion version) {
        ((DefaultTreeModel)myFrameworksTree.getModel()).nodeChanged(getSelectedNode());
      }
    }, this);

    myFrameworksTree.addTreeSelectionListener(new TreeSelectionListener() {
      @Override
      public void valueChanged(TreeSelectionEvent e) {
        onSelectionChanged();
      }
    });

    JPanel treePanel = new JPanel(new BorderLayout());
    treePanel.add(ScrollPaneFactory.createScrollPane(myFrameworksTree), BorderLayout.CENTER);
    treePanel.setMinimumSize(JBUI.size(200, 300));

    splitter.setFirstComponent(treePanel);
    myOptionsPanel = new JPanel(new CardLayout());
    JPanel emptyCard = new JPanel();
    emptyCard.setPreferredSize(JBUI.size(400, 100));
    myOptionsPanel.add(EMPTY_CARD, emptyCard);

    splitter.setSecondComponent(myOptionsPanel);
    myFrameworksPanel.add(splitter, BorderLayout.CENTER);

    setProviders(providers);
  }

  public void setProviders(List<? extends FrameworkSupportInModuleProvider> providers) {
    setProviders(providers, Collections.emptySet(), Collections.emptySet());
  }

  public void setProviders(List<? extends FrameworkSupportInModuleProvider> providers, Set<String> associated, Set<String> preselected) {
    myProviders = providers;

    myAssociatedFrameworks = createNodes(myProviders, associated, preselected);
    setAssociatedFrameworks();

    myFrameworksTree.setRoots(myRoots);
    myFrameworksTree.setSelectionRow(0);

    for (FrameworkSupportNodeBase<?> node : myRoots) {
      if (preselected.contains(node.getId())) {
        // we need to trigger `FrameworksTree::onNodeStateChanged`
        // in order to preconfigure `FrameworkSupportNode`s
        myFrameworksTree.setNodeState(node, true);
      }
    }
  }

  public void setAssociatedFrameworks() {

    if (myAssociatedFrameworksPanel == null) return;
    for (FrameworkSupportNodeBase<?> nodeBase : myAssociatedFrameworks) {
      if (nodeBase instanceof FrameworkSupportNode) {
        ((FrameworkSupportNode)nodeBase).getConfigurable().onFrameworkSelectionChanged(true);
        FrameworkSupportOptionsComponent component = initializeOptionsPanel((FrameworkSupportNode)nodeBase, false);
        addAssociatedFrameworkComponent(component.getMainPanel(), myAssociatedFrameworksPanel);
      }
      else {
        JPanel panel = initializeGroupPanel((FrameworkGroup<?>)nodeBase.getUserObject(), false);
        addAssociatedFrameworkComponent(panel, myAssociatedFrameworksPanel);
      }
    }
  }

  private static void addAssociatedFrameworkComponent(JPanel component, JPanel panel) {
    panel.add(component, new GridBagConstraints(0, GridBagConstraints.RELATIVE, 2, 1, 1.0, 0, GridBagConstraints.NORTHWEST,
                                                GridBagConstraints.HORIZONTAL, JBInsets.emptyInsets(), 0, 0));
  }

  protected void onFrameworkStateChanged() {}

  private void onSelectionChanged() {
    if (!myFrameworksTree.isProcessingMouseEventOnCheckbox()) {
      updateOptionsPanel();
    }

    final FrameworkSupportNodeBase<?> selectedNode = getSelectedNode();
    if (!Comparing.equal(selectedNode, myLastSelectedNode)) {
      applyLibraryOptionsForSelected();

      myLastSelectedNode = selectedNode;
    }
  }

  @Override
  public void dispose() {
  }

  private void applyLibraryOptionsForSelected() {
    if (myLastSelectedNode instanceof FrameworkSupportNode) {
      final FrameworkSupportOptionsComponent optionsComponent = myInitializedOptionsComponents.get((FrameworkSupportNode)myLastSelectedNode);
      if (optionsComponent != null) {
        final LibraryOptionsPanel optionsPanel = optionsComponent.getLibraryOptionsPanel();
        if (optionsPanel != null) {
          optionsPanel.apply();
        }
      }
    }
  }

  private void updateOptionsPanel() {
    final FrameworkSupportNodeBase<?> node = getSelectedNode();
    if (node instanceof FrameworkSupportNode frameworkSupportNode) {
      initializeOptionsPanel(frameworkSupportNode, true);
      showCard(frameworkSupportNode.getId());
      UIUtil.setEnabled(myOptionsPanel, frameworkSupportNode.isChecked(), true);
      frameworkSupportNode.getConfigurable().onFrameworkSelectionChanged(node.isChecked());
    }
    else if (node instanceof FrameworkGroupNode) {
      FrameworkGroup<?> group = ((FrameworkGroupNode)node).getUserObject();
      initializeGroupPanel(group, true);
      showCard(group.getId());
      UIUtil.setEnabled(myOptionsPanel, true, true);
    }
    else {
      showCard(EMPTY_CARD);
    }
  }

  private JPanel initializeGroupPanel(FrameworkGroup<?> group, boolean addToOptions) {
    JPanel panel = myInitializedGroupPanels.get(group);
    if (panel == null) {
      FrameworkVersionComponent component = new FrameworkVersionComponent(myModel, group.getId(), group.getGroupVersions(),
                                                                          JavaUiBundle.message("add.framework.support.label.version", group.getPresentableName()));
      panel = component.getMainPanel();
      myInitializedGroupPanels.put(group, panel);
      if (addToOptions) {
        myOptionsPanel.add(group.getId(), wrapInScrollPane(panel));
      }
    }
    return panel;
  }

  public @Nullable FrameworkSupportNodeBase<?> getSelectedNode() {
    final FrameworkSupportNodeBase<?>[] nodes = myFrameworksTree.getSelectedNodes(FrameworkSupportNodeBase.class, null);
    return nodes.length == 1 ? nodes[0] : null;
  }

  private FrameworkSupportOptionsComponent initializeOptionsPanel(final FrameworkSupportNode node, boolean addToOptions) {
    FrameworkSupportOptionsComponent component = myInitializedOptionsComponents.get(node);
    if (component == null) {
      final FrameworkSupportNodeBase<?> parentNode = node.getParentNode();
      if (parentNode instanceof FrameworkSupportNode) {
        initializeOptionsPanel((FrameworkSupportNode)parentNode, addToOptions);
      }
      else if (parentNode instanceof FrameworkGroupNode) {
        initializeGroupPanel(((FrameworkGroupNode)parentNode).getUserObject(), addToOptions);
      }

      component = new FrameworkSupportOptionsComponent(myModel, myLibrariesContainer, this,
                                                       node.getUserObject(), node.getConfigurable());
      if (addToOptions) {
        myOptionsPanel.add(node.getId(), wrapInScrollPane(component.getMainPanel()));
      }
      myInitializedOptionsComponents.put(node, component);
    }
    return component;
  }

  private static JScrollPane wrapInScrollPane(JPanel panel) {
    JPanel wrapper = new JPanel(new BorderLayout());
    wrapper.add(panel);
    wrapper.setBorder(JBUI.Borders.empty(5));
    return ScrollPaneFactory.createScrollPane(wrapper, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
                                              ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
  }

  private void showCard(String cardName) {
    ((CardLayout)myOptionsPanel.getLayout()).show(myOptionsPanel, cardName);
  }

  private List<LibraryCompositionSettings> getLibrariesCompositionSettingsList() {
    List<LibraryCompositionSettings> list = new ArrayList<>();
    List<FrameworkSupportNode> selected = getSelectedNodes();
    for (FrameworkSupportNode node : selected) {
      ContainerUtil.addIfNotNull(list, getLibraryCompositionSettings(node));
    }
    return list;
  }

  private @Nullable LibraryCompositionSettings getLibraryCompositionSettings(FrameworkSupportNode node) {
    final FrameworkSupportOptionsComponent optionsComponent = myInitializedOptionsComponents.get(node);
    return optionsComponent != null ? optionsComponent.getLibraryCompositionSettings() : null;
  }

  private Collection<FrameworkSupportNodeBase<?>> createNodes(List<? extends FrameworkSupportInModuleProvider> providers,
                                                           Set<String> associated,
                                                           final Collection<String> preselected) {
    Map<String, FrameworkSupportNode> nodes = new HashMap<>();
    Map<FrameworkGroup<?>, FrameworkGroupNode> groups = new HashMap<>();
    List<FrameworkSupportNodeBase<?>> roots = new ArrayList<>();
    Map<String, FrameworkSupportNodeBase<?>> associatedNodes = new LinkedHashMap<>();
    for (FrameworkSupportInModuleProvider provider : providers) {
      createNode(provider, nodes, groups, roots, providers, associated, associatedNodes);
    }

    FrameworkSupportNodeBase.sortByName(roots,
                                        (o1, o2) -> Boolean.compare(preselected.contains(o2.getId()), preselected.contains(o1.getId())));
    myRoots = roots;
    return associatedNodes.values();
  }

  private @Nullable FrameworkSupportNode createNode(final FrameworkSupportInModuleProvider provider,
                                                    final Map<String, FrameworkSupportNode> nodes,
                                                    final Map<FrameworkGroup<?>, FrameworkGroupNode> groupNodes,
                                                    List<? super FrameworkSupportNodeBase<?>> roots,
                                                    List<? extends FrameworkSupportInModuleProvider> providers,
                                                    Set<String> associated,
                                                    Map<String, FrameworkSupportNodeBase<?>> associatedNodes) {
    String id = provider.getFrameworkType().getId();
    FrameworkSupportNode node = nodes.get(id);
    if (node != null || associatedNodes.containsKey(id)) {
      return node;
    }
    String underlyingTypeId = provider.getFrameworkType().getUnderlyingFrameworkTypeId();
    FrameworkSupportNodeBase<?> parentNode = null;
    final FrameworkGroup<?> group = provider.getFrameworkType().getParentGroup();
    if (underlyingTypeId != null) {
      FrameworkSupportInModuleProvider parentProvider = FrameworkSupportUtil.findProvider(underlyingTypeId, providers);
      if (parentProvider == null) {
        LOG.info("Cannot find id = " + underlyingTypeId);
        return null;
      }
      parentNode = createNode(parentProvider, nodes, groupNodes, roots, providers, associated, associatedNodes);
    }
    else if (group != null) {
      parentNode = groupNodes.get(group);
      if (parentNode == null) {
        FrameworkGroupNode groupNode = new FrameworkGroupNode(group, null);
        if (associated.contains(groupNode.getId())) {
          associatedNodes.put(groupNode.getId(), groupNode);
        }
        else {
          groupNodes.put(group, groupNode);
          parentNode = groupNode;
          roots.add(groupNode);
        }
      }
    }
    node = new FrameworkSupportNode(provider, parentNode, myModel, this);
    if (associated.contains(id)) {
      associatedNodes.put(id, node);
    }
    else {
      nodes.put(id, node);
      if (parentNode == null) {
        roots.add(node);
      }
    }
    return node;
  }

  public JComponent getMainPanel() {
    return myMainPanel;
  }

  public FrameworksTree getFrameworksTree() {
    return myFrameworksTree;
  }

  public boolean hasSelectedFrameworks() {
    return !getSelectedNodes().isEmpty();
  }

  public List<FrameworkSupportNode> getSelectedNodes() {
    List<FrameworkSupportNode> list = new ArrayList<>();
    if (myRoots != null) {
      addChildFrameworks(myRoots, list);
    }
    list.addAll(ContainerUtil.findAll(myAssociatedFrameworks, FrameworkSupportNode.class));
    return list;
  }

  private static void addChildFrameworks(final List<? extends FrameworkSupportNodeBase<?>> list, final List<? super FrameworkSupportNode> result) {
    for (FrameworkSupportNodeBase<?> node : list) {
      if (node.isChecked() || node instanceof FrameworkGroupNode) {
        if (node instanceof FrameworkSupportNode) {
          result.add((FrameworkSupportNode)node);
        }
        addChildFrameworks(node.getChildren(), result);
      }
    }
  }

  public boolean downloadLibraries(final @NotNull JComponent parentComponent) {
    applyLibraryOptionsForSelected();
    for (LibraryCompositionSettings compositionSettings : getLibrariesCompositionSettingsList()) {
      if (!compositionSettings.downloadFiles(parentComponent)) {
        int answer = Messages.showYesNoDialog(parentComponent,
                                              JavaUiBundle.message("warning.message.some.required.libraries.wasn.t.downloaded"),
                                              JavaUiBundle.message("dialog.title.libraries.are.required"), Messages.getWarningIcon());
        return answer == Messages.YES;
      }
    }

    return true;
  }

  public boolean validate() {
    applyLibraryOptionsForSelected();
    List<String> frameworksWithoutRequiredLibraries = new ArrayList<>();
    for (FrameworkSupportNode node : getSelectedNodes()) {
      if (node.getConfigurable().isOnlyLibraryAdded()) {
        LibraryCompositionSettings librarySettings = getLibraryCompositionSettings(node);
        if (librarySettings != null && !librarySettings.isLibraryConfigured()) {
          frameworksWithoutRequiredLibraries.add(node.getTitle());
        }
      }
    }

    if (!frameworksWithoutRequiredLibraries.isEmpty()) {
      String frameworksText = StringUtil.join(frameworksWithoutRequiredLibraries, ", ");
      Messages.showErrorDialog(myMainPanel, JavaUiBundle.message("error.message.required.library.is.not.configured", frameworksText, frameworksWithoutRequiredLibraries.size()),
                               JavaUiBundle.message("error.title.required.library.is.not.configured"));
      return false;
    }
    return true;
  }

  public void addSupport(final @NotNull Module module, final @NotNull ModifiableRootModel rootModel) {
    List<Library> addedLibraries = new ArrayList<>();
    List<? extends FrameworkSupportNode> selectedFrameworks = sortFrameworks(getSelectedNodes());
    List<FrameworkSupportConfigurable> selectedConfigurables = new ArrayList<>();
    final IdeaModifiableModelsProvider modifiableModelsProvider = new IdeaModifiableModelsProvider();
    for (FrameworkSupportNode node : selectedFrameworks) {
      FrameworkSupportInModuleConfigurable configurable = node.getConfigurable();
      if (configurable instanceof OldFrameworkSupportProviderWrapper.FrameworkSupportConfigurableWrapper) {
        selectedConfigurables.add(((OldFrameworkSupportProviderWrapper.FrameworkSupportConfigurableWrapper)configurable).getConfigurable());
      }
      final LibraryCompositionSettings settings = getLibraryCompositionSettings(node);
      Library library = settings != null ? settings.addLibraries(rootModel, addedLibraries, myLibrariesContainer) : null;
      if (configurable instanceof OldFrameworkSupportProviderWrapper.FrameworkSupportConfigurableWrapper) {
        ((OldFrameworkSupportProviderWrapper.FrameworkSupportConfigurableWrapper)configurable).getConfigurable().addSupport(module, rootModel,
                                                                                                                         library);
      }
      else {
        configurable.addSupport(module, rootModel, modifiableModelsProvider);
      }
    }
    for (FrameworkSupportNode node : selectedFrameworks) {
      FrameworkSupportInModuleProvider provider = node.getUserObject();
      if (provider instanceof OldFrameworkSupportProviderWrapper) {
        final FrameworkSupportProvider oldProvider = ((OldFrameworkSupportProviderWrapper)provider).getProvider();
        if (oldProvider instanceof FacetBasedFrameworkSupportProvider && !addedLibraries.isEmpty()) {
          ((FacetBasedFrameworkSupportProvider<?>)oldProvider).processAddedLibraries(module, addedLibraries);
        }
      }
    }
    for (FrameworkSupportCommunicator communicator : FrameworkSupportCommunicator.EP_NAME.getExtensions()) {
      communicator.onFrameworkSupportAdded(module, rootModel, selectedConfigurables, myModel);
    }
  }

  private @Unmodifiable List<? extends FrameworkSupportNode> sortFrameworks(@Unmodifiable List<? extends FrameworkSupportNode> nodes) {
    final Comparator<FrameworkSupportInModuleProvider> comparator = FrameworkSupportUtil.getFrameworkSupportProvidersComparator(myProviders);
    return ContainerUtil.sorted(nodes, (o1, o2) -> comparator.compare(o1.getUserObject(), o2.getUserObject()));
  }

}
