/*
 * Copyright 2000-2014 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.ide.plugins;

import consulo.container.plugin.PluginId;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.ui.BooleanTableCellEditor;
import com.intellij.ui.BooleanTableCellRenderer;
import com.intellij.util.containers.hash.HashSet;
import com.intellij.util.ui.ColumnInfo;
import consulo.container.plugin.PluginDescriptor;
import consulo.ide.plugins.InstalledPluginsState;
import org.jetbrains.annotations.NonNls;

import javax.annotation.Nullable;
import javax.swing.*;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.util.List;
import java.util.*;

/**
 * @author stathik
 * @since 3:51:58 PM Dec 26, 2003
 */
public class InstalledPluginsTableModel extends PluginTableModel {
  private final Map<PluginId, Boolean> myEnabled = new HashMap<>();
  private final Map<PluginId, Set<PluginId>> myDependentToRequiredListMap = new HashMap<>();

  private static final String ENABLED_DISABLED = "All plugins";
  private static final String ENABLED = "Enabled plugins";
  private static final String DISABLED = "Disabled plugins";
  public static final String[] ENABLED_VALUES = new String[]{ENABLED_DISABLED, ENABLED, DISABLED};
  private String myEnabledFilter = ENABLED_DISABLED;

  public InstalledPluginsTableModel() {
    super.columns = new ColumnInfo[]{new MyPluginManagerColumnInfo(), new EnabledPluginInfo()};
    view = new ArrayList<>(consulo.container.plugin.PluginManager.getPlugins());
    view.addAll(InstalledPluginsState.getInstance().getAllPlugins());
    reset(view);

    for (Iterator<PluginDescriptor> iterator = view.iterator(); iterator.hasNext(); ) {
      @NonNls final PluginId pluginId = iterator.next().getPluginId();
      if (PluginManagerCore.isSystemPlugin(pluginId)) {
        iterator.remove();
      }
    }

    setSortKey(new RowSorter.SortKey(getNameColumn(), SortOrder.ASCENDING));
  }

  public boolean appendOrUpdateDescriptor(PluginDescriptor descriptor) {
    final PluginId descrId = descriptor.getPluginId();
    final PluginDescriptor installed = PluginManager.getPlugin(descrId);
    InstalledPluginsState pluginsState = InstalledPluginsState.getInstance();

    if (installed != null) {
      pluginsState.updateExistingPlugin(descriptor, installed);
      return true;
    }
    else if (!pluginsState.getAllPlugins().contains(descriptor)) {
      pluginsState.getAllPlugins().add(descriptor);
      view.add(descriptor);
      setEnabled(descriptor, true);
      fireTableDataChanged();
      return true;
    }
    return false;
  }

  public static int getCheckboxColumn() {
    return 1;
  }

  @Override
  public int getNameColumn() {
    return 0;
  }

  private void reset(final List<PluginDescriptor> list) {
    for (PluginDescriptor ideaPluginDescriptor : list) {
      setEnabled(ideaPluginDescriptor);
    }

    updatePluginDependencies();
  }


  private void setEnabled(PluginDescriptor ideaPluginDescriptor) {
    setEnabled(ideaPluginDescriptor, ideaPluginDescriptor.isEnabled());
  }

  private void setEnabled(PluginDescriptor ideaPluginDescriptor, final boolean enabled) {
    final Collection<String> disabledPlugins = PluginManager.getDisabledPlugins();
    final PluginId pluginId = ideaPluginDescriptor.getPluginId();
    if (!enabled && !disabledPlugins.contains(pluginId.toString())) {
      myEnabled.put(pluginId, null);
    }
    else {
      myEnabled.put(pluginId, enabled);
    }
  }

  public Map<PluginId, Set<PluginId>> getDependentToRequiredListMap() {
    return myDependentToRequiredListMap;
  }

  public boolean isLoaded(PluginId pluginId) {
    return myEnabled.get(pluginId) != null;
  }

  public boolean hasProblematicDependencies(PluginId pluginId) {
    final Set<PluginId> ids = myDependentToRequiredListMap.get(pluginId);
    return ids != null && !ids.isEmpty();
  }

  @Nullable
  public Set<PluginId> getRequiredPlugins(PluginId pluginId) {
    return myDependentToRequiredListMap.get(pluginId);
  }

  protected void updatePluginDependencies() {
    myDependentToRequiredListMap.clear();

    InstalledPluginsState pluginsState = InstalledPluginsState.getInstance();

    Set<PluginId> updatedPlugins = pluginsState.getUpdatedPlugins();

    final int rowCount = getRowCount();
    for (int i = 0; i < rowCount; i++) {
      final PluginDescriptor descriptor = getObjectAt(i);
      final PluginId pluginId = descriptor.getPluginId();
      myDependentToRequiredListMap.remove(pluginId);
      if (descriptor.isDeleted()) continue;
      final Boolean enabled = myEnabled.get(pluginId);
      if (enabled == null || enabled) {
        consulo.container.plugin.PluginManager.checkDependants(descriptor, PluginManager::getPlugin, dependantPluginId -> {
          final Boolean enabled1 = myEnabled.get(dependantPluginId);
          if ((enabled1 == null && !updatedPlugins.contains(dependantPluginId)) || (enabled1 != null && !enabled1)) {
            Set<PluginId> required = myDependentToRequiredListMap.get(pluginId);
            if (required == null) {
              required = new HashSet<>();
              myDependentToRequiredListMap.put(pluginId, required);
            }

            required.add(dependantPluginId);
            //return false;
          }

          return true;
        });
        if (enabled == null && !myDependentToRequiredListMap.containsKey(pluginId) && !PluginManager.isIncompatible(descriptor)) {
          myEnabled.put(pluginId, true);
        }
      }
    }
  }

  @Override
  public void updatePluginsList(List<PluginDescriptor> list) {
    //  For each downloadable plugin we need to know whether its counterpart
    //  is already installed, and if yes compare the difference in versions:
    //  availability of newer versions will be indicated separately.
    for (PluginDescriptor descr : list) {
      PluginId descrId = descr.getPluginId();
      PluginDescriptor existing = PluginManager.getPlugin(descrId);
      if (existing != null) {
        if (descr instanceof PluginNode) {
          InstalledPluginsState.getInstance().updateExistingPluginInfo(descr, existing);
        }
        else {
          view.add(descr);
          setEnabled(descr);
        }
      }
    }
    for (PluginDescriptor descriptor : InstalledPluginsState.getInstance().getAllPlugins()) {
      if (!view.contains(descriptor)) {
        view.add(descriptor);
      }
    }
    fireTableDataChanged();
  }

  @Override
  protected ArrayList<PluginDescriptor> toProcess() {
    ArrayList<PluginDescriptor> toProcess = super.toProcess();
    for (PluginDescriptor descriptor : InstalledPluginsState.getInstance().getAllPlugins()) {
      if (!toProcess.contains(descriptor)) {
        toProcess.add(descriptor);
      }
    }
    return toProcess;
  }

  @Override
  public void filter(final List<PluginDescriptor> filtered) {
    view.clear();
    for (PluginDescriptor descriptor : filtered) {
      view.add(descriptor);
    }

    super.filter(filtered);
  }

  public void enableRows(PluginDescriptor[] ideaPluginDescriptors, Boolean value) {
    for (PluginDescriptor ideaPluginDescriptor : ideaPluginDescriptors) {
      final PluginId currentPluginId = ideaPluginDescriptor.getPluginId();
      final Boolean enabled = myEnabled.get(currentPluginId) == null ? Boolean.FALSE : value;
      myEnabled.put(currentPluginId, enabled);
    }
    updatePluginDependencies();
    warnAboutMissedDependencies(value, ideaPluginDescriptors);
    hideNotApplicablePlugins(value, ideaPluginDescriptors);
  }

  private void hideNotApplicablePlugins(Boolean value, final PluginDescriptor... ideaPluginDescriptors) {
    if (!value && ENABLED.equals(myEnabledFilter) || (value && DISABLED.equals(myEnabledFilter))) {
      SwingUtilities.invokeLater(() -> {
        for (PluginDescriptor ideaPluginDescriptor : ideaPluginDescriptors) {
          view.remove(ideaPluginDescriptor);
          filtered.add(ideaPluginDescriptor);
        }
        fireTableDataChanged();
      });
    }
  }


  public static boolean hasNewerVersion(PluginId descr) {
    return InstalledPluginsState.getInstance().hasNewerVersion(descr);
  }

  public static boolean wasUpdated(PluginId descr) {
    return InstalledPluginsState.getInstance().wasUpdated(descr);
  }

  public boolean isEnabled(final PluginId pluginId) {
    final Boolean enabled = myEnabled.get(pluginId);
    return enabled != null && enabled;
  }

  public boolean isDisabled(final PluginId pluginId) {
    final Boolean enabled = myEnabled.get(pluginId);
    return enabled != null && !enabled;
  }

  public Map<PluginId, Boolean> getEnabledMap() {
    return myEnabled;
  }

  public String getEnabledFilter() {
    return myEnabledFilter;
  }

  public void setEnabledFilter(String enabledFilter, String filter) {
    myEnabledFilter = enabledFilter;
    filter(filter);
  }

  @Override
  public boolean isPluginDescriptorAccepted(PluginDescriptor descriptor) {
    if (!myEnabledFilter.equals(ENABLED_DISABLED)) {
      final boolean enabled = isEnabled(descriptor.getPluginId());
      if (enabled && myEnabledFilter.equals(DISABLED)) return false;
      if (!enabled && myEnabledFilter.equals(ENABLED)) return false;
    }
    return true;
  }

  private class EnabledPluginInfo extends ColumnInfo<PluginDescriptor, Boolean> {

    public EnabledPluginInfo() {
      super("");
    }

    @Override
    public Boolean valueOf(PluginDescriptor ideaPluginDescriptor) {
      return myEnabled.get(ideaPluginDescriptor.getPluginId());
    }

    @Override
    public boolean isCellEditable(final PluginDescriptor ideaPluginDescriptor) {
      return true;
    }

    @Override
    public Class getColumnClass() {
      return Boolean.class;
    }

    @Override
    public TableCellEditor getEditor(final PluginDescriptor o) {
      return new BooleanTableCellEditor();
    }

    @Override
    public TableCellRenderer getRenderer(final PluginDescriptor ideaPluginDescriptor) {
      return new BooleanTableCellRenderer() {
        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
          return super.getTableCellRendererComponent(table, value == null ? Boolean.TRUE : value, isSelected, hasFocus, row, column);
        }
      };
    }

    @Override
    public void setValue(final PluginDescriptor ideaPluginDescriptor, Boolean value) {
      final PluginId currentPluginId = ideaPluginDescriptor.getPluginId();
      final Boolean enabled = myEnabled.get(currentPluginId) == null ? Boolean.FALSE : value;
      myEnabled.put(currentPluginId, enabled);
      updatePluginDependencies();
      warnAboutMissedDependencies(enabled, ideaPluginDescriptor);
      hideNotApplicablePlugins(value, ideaPluginDescriptor);
    }

    @Override
    public Comparator<PluginDescriptor> getComparator() {
      return (o1, o2) -> {
        final Boolean enabled1 = myEnabled.get(o1.getPluginId());
        final Boolean enabled2 = myEnabled.get(o2.getPluginId());
        if (enabled1 != null && enabled1) {
          if (enabled2 != null && enabled2) {
            return 0;
          }

          return 1;
        }
        else {
          if (enabled2 == null || !enabled2) {
            return 0;
          }
          return -1;
        }
      };
    }

    @Override
    public int getWidth(JTable table) {
      return new JCheckBox().getPreferredSize().width;
    }
  }

  private void warnAboutMissedDependencies(final Boolean newVal, final PluginDescriptor... ideaPluginDescriptors) {
    final Set<PluginId> deps = new HashSet<>();
    final List<PluginDescriptor> descriptorsToCheckDependencies = new ArrayList<>();
    if (newVal) {
      Collections.addAll(descriptorsToCheckDependencies, ideaPluginDescriptors);
    }
    else {
      descriptorsToCheckDependencies.addAll(getAllPlugins());
      descriptorsToCheckDependencies.removeAll(Arrays.asList(ideaPluginDescriptors));

      for (Iterator<PluginDescriptor> iterator = descriptorsToCheckDependencies.iterator(); iterator.hasNext(); ) {
        PluginDescriptor descriptor = iterator.next();
        final Boolean enabled = myEnabled.get(descriptor.getPluginId());
        if (enabled == null || !enabled) {
          iterator.remove();
        }
      }
    }

    for (final PluginDescriptor ideaPluginDescriptor : descriptorsToCheckDependencies) {
      consulo.container.plugin.PluginManager.checkDependants(ideaPluginDescriptor, PluginManager::getPlugin, pluginId -> {
        Boolean enabled = myEnabled.get(pluginId);
        if (enabled == null) {
          return false;
        }
        if (newVal && !enabled) {
          deps.add(pluginId);
        }

        if (!newVal) {
          if (ideaPluginDescriptor.isDeleted()) {
            return true;
          }
          final PluginId pluginDescriptorId = ideaPluginDescriptor.getPluginId();
          for (PluginDescriptor descriptor : ideaPluginDescriptors) {
            if (pluginId.equals(descriptor.getPluginId())) {
              deps.add(pluginDescriptorId);
              break;
            }
          }
        }
        return true;
      });
    }
    if (!deps.isEmpty()) {
      final String listOfSelectedPlugins = StringUtil.join(ideaPluginDescriptors, PluginDescriptor::getName, ", ");
      final Set<PluginDescriptor> pluginDependencies = new HashSet<>();
      final String listOfDependencies = StringUtil.join(deps, pluginId -> {
        final PluginDescriptor pluginDescriptor = PluginManager.getPlugin(pluginId);
        assert pluginDescriptor != null;
        pluginDependencies.add(pluginDescriptor);
        return pluginDescriptor.getName();
      }, "<br>");
      final String message = !newVal
                             ? "<html>The following plugins <br>" +
                               listOfDependencies +
                               "<br>are enabled and depend" +
                               (deps.size() == 1 ? "s" : "") +
                               " on selected plugins. " +
                               "<br>Would you like to disable them too?</html>"
                             : "<html>The following plugins on which " +
                               listOfSelectedPlugins +
                               " depend" +
                               (ideaPluginDescriptors.length == 1 ? "s" : "") +
                               " are disabled:<br>" +
                               listOfDependencies +
                               "<br>Would you like to enable them?</html>";
      if (Messages.showOkCancelDialog(message, newVal ? "Enable Dependant Plugins" : "Disable Plugins with Dependency on this", Messages.getQuestionIcon()) ==
          Messages.OK) {
        for (PluginId pluginId : deps) {
          myEnabled.put(pluginId, newVal);
        }

        updatePluginDependencies();
        hideNotApplicablePlugins(newVal, pluginDependencies.toArray(new PluginDescriptor[pluginDependencies.size()]));
      }
    }
  }

  private class MyPluginManagerColumnInfo extends PluginManagerColumnInfo {
    public MyPluginManagerColumnInfo() {
      super(PluginManagerColumnInfo.COLUMN_NAME, InstalledPluginsTableModel.this);
    }

    @Override
    public TableCellRenderer getRenderer(final PluginDescriptor pluginDescriptor) {
      return new PluginsTableRenderer(pluginDescriptor, false);
    }

    @Override
    protected boolean isSortByName() {
      return true;
    }

    @Override
    public Comparator<PluginDescriptor> getComparator() {
      final Comparator<PluginDescriptor> comparator = super.getColumnComparator();
      return (o1, o2) -> {
        if (isSortByStatus()) {
          final boolean incompatible1 = PluginManager.isIncompatible(o1);
          final boolean incompatible2 = PluginManager.isIncompatible(o2);
          if (incompatible1) {
            if (incompatible2) return comparator.compare(o1, o2);
            return -1;
          }
          if (incompatible2) return 1;

          final boolean hasNewerVersion1 = hasNewerVersion(o1.getPluginId());
          final boolean hasNewerVersion2 = hasNewerVersion(o2.getPluginId());
          if (hasNewerVersion1) {
            if (hasNewerVersion2) return comparator.compare(o1, o2);
            return -1;
          }
          if (hasNewerVersion2) return 1;


          final boolean wasUpdated1 = wasUpdated(o1.getPluginId());
          final boolean wasUpdated2 = wasUpdated(o2.getPluginId());
          if (wasUpdated1) {
            if (wasUpdated2) return comparator.compare(o1, o2);
            return -1;
          }
          if (wasUpdated2) return 1;


          if (o1 instanceof PluginNode) {
            if (o2 instanceof PluginNode) return comparator.compare(o1, o2);
            return -1;
          }
          if (o2 instanceof PluginNode) return 1;


          final boolean deleted1 = o1.isDeleted();
          final boolean deleted2 = o2.isDeleted();
          if (deleted1) {
            if (deleted2) return comparator.compare(o1, o2);
            return -1;
          }
          if (deleted2) return 1;

          final boolean enabled1 = isEnabled(o1.getPluginId());
          final boolean enabled2 = isEnabled(o2.getPluginId());
          if (enabled1 && !enabled2) return -1;
          if (enabled2 && !enabled1) return 1;
        }
        return comparator.compare(o1, o2);
      };
    }

    @Override
    public int getWidth(JTable table) {
      return super.getWidth(table);
    }
  }
}