package com.airlock.iam.plugin.configeditor.field;

import com.airlock.iam.elog.api.ELogger;
import com.airlock.iam.elog.api.ELoggerFactory;
import com.airlock.iam.elog.api.LogTypes;
import com.airlock.iam.plugin.api.annotation.property.PluginMapProperty;
import com.airlock.iam.plugin.api.exception.InitializeException;
import com.airlock.iam.plugin.configeditor.command.ChangePropertyConfigurationCommand;
import com.airlock.iam.plugin.configeditor.command.CreateNewConnectedPluginConfigurationCommand;
import com.airlock.iam.plugin.configeditor.data.ConfigurableProperty;
import com.airlock.iam.plugin.configeditor.data.PropertyAdapter;
import com.airlock.iam.plugin.configeditor.ui.dialog.NewPluginDialog;
import com.airlock.iam.plugin.configeditor.util.GuiResources;
import com.airlock.iam.plugin.framework.application.configuration.MutablePluginConfiguration;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.vaadin.event.MouseEvents.ClickEvent;
import com.vaadin.server.Sizeable.Unit;
import com.vaadin.ui.Component;
import com.vaadin.ui.DragAndDropWrapper;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Image;
import com.vaadin.ui.Notification;
import com.vaadin.ui.Notification.Type;
import com.vaadin.v7.data.Property.ValueChangeEvent;
import com.vaadin.v7.ui.AbstractField;
import com.vaadin.v7.ui.ComboBox;
import com.vaadin.v7.ui.Table;
import com.vaadin.v7.ui.TextField;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;

public class MedusaPluginMapField extends AbstractMedusaListField<PluginMapProperty, Object> {
   private static final ELogger LOG = ELoggerFactory.getLogger(MedusaPluginMapField.class);
   private Set<String> keys;
   private Map<TextField, ComboBox> mapTextCombo;
   private final Image newRowIcon = this.makeIcon(GuiResources.ICON_INSERT_ROW, "Add new row.", "with-hand", this);

   @SuppressFBWarnings(value = "MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR", justification = "See ALIAM-15315")
   public MedusaPluginMapField(PropertyAdapter<PluginMapProperty> propertyAdapter) {
      super(propertyAdapter, true);
      this.newRowIcon.addStyleName("newRowIcon");
   }

   @Override
   protected Component initContent() {
      this.getIconLayout().addComponent(this.newRowIcon);
      this.addUndefinePropertyButton();
      this.getDefaultIcon().addStyleName("table-default-icon");
      return super.initContent();
   }

   @Override
   public AbstractField<Object> createInnerField() {
      Table table = new Table();
      table.setWidth("100%");
      table.setPageLength(0);
      table.setColumnReorderingAllowed(true);
      table.setSelectable(false);
      table.addContainerProperty("Key", TextField.class, null);
      table.addContainerProperty("Plugin", DragAndDropWrapper.class, null);
      table.setColumnExpandRatio("Plugin", 1.0F);
      table.addContainerProperty("Actions", HorizontalLayout.class, null);
      table.addStyleName("medusa-noscroll");
      return table;
   }

   @Override
   public Class<?> getType() {
      return Map.class;
   }

   protected void setInternalValue(Object newValue) {
      super.setInternalValue(newValue);
      this.mapTextCombo = new HashMap<>();
      Table table = (Table)this.getInnerField();
      table.addStyleName("multiValueTable");
      table.removeAllItems();
      if (newValue != null) {
         Validate.isTrue(newValue instanceof Map, "Expected a Map<String, PluginConfiguration> object.", new Object[0]);
         boolean readonly = this.getConfigurableProperty().isReadOnly();
         Map<String, MutablePluginConfiguration> sortedMap = Maps.newTreeMap(new MedusaPluginMapField.PluginMapKeyComparator());
         this.keys = sortedMap.keySet();
         sortedMap.putAll((Map<? extends String, ? extends MutablePluginConfiguration>)newValue);
         if (sortedMap.size() > 50) {
            table.setPageLength(50);
         }

         for (Entry<String, MutablePluginConfiguration> mapping : sortedMap.entrySet()) {
            TextField key = new TextField();
            key.setWidth("100%");
            key.setImmediate(true);
            key.setBuffered(false);
            key.setNullSettingAllowed(false);
            key.setValue(mapping.getKey());
            key.addValueChangeListener(this);
            key.setReadOnly(readonly);
            PluginComboBox combo = new PluginComboBox();
            combo.setWidth("100%");
            combo.setInvalidAllowed(false);
            combo.setNullSelectionAllowed(false);
            combo.setBuffered(false);
            combo.setImmediate(true);
            this.initSelectablePlugins(combo);
            MutablePluginConfiguration pc = mapping.getValue();
            combo.setValue(pc);
            combo.addValueChangeListener(this);
            combo.setReadOnly(readonly);
            this.mapTextCombo.put(key, combo);
            DragAndDropWrapper dndWrapperCombo = new TransparentDnDWrapper(combo);
            dndWrapperCombo.setWidth("100%");
            dndWrapperCombo.setDropHandler(new PluginDropHandler(this.getApplication(), combo));
            HorizontalLayout actions = new HorizontalLayout();
            if (!readonly) {
               Image newPluginIcon = this.createNewPluginIcon(mapping);
               actions.addComponent(newPluginIcon);
               Image deleteIcon = this.createDeleteIcon(mapping);
               actions.addComponent(deleteIcon);
            }

            if (pc != null) {
               Image followIcon = this.createFollowIcon(pc);
               actions.addComponent(followIcon);
            } else {
               Image placeholder = new Image();
               placeholder.addStyleName("icon-placeholder");
               placeholder.setWidth(16.0F, Unit.PIXELS);
               actions.addComponent(placeholder);
            }

            table.addItem(new Object[]{key, dndWrapperCombo, actions}, mapping.getKey());
         }

         this.getConfigurableProperty().setValue(newValue);
      } else {
         this.mapTextCombo = null;
      }

      this.updateUiAndValidate();
   }

   private void initSelectablePlugins(PluginComboBox combo) {
      try {
         combo.initSelectablePlugins(
            this.getPropertyAdapter().getPluginConfiguration(),
            this.getPropertyAdapter().getMapElementType(),
            this.getPropertyAdapter().getConfigurationEnvironment()
         );
      } catch (InitializeException e) {
         LOG.error(LogTypes.CONFIGURATION_ERROR, "Initialize Exception: " + e.getMessage());
      }
   }

   private Image createFollowIcon(MutablePluginConfiguration pc) {
      Image followIcon = this.makeIcon(
         GuiResources.ICON_FOLLOW,
         "Navigate to this plugin.",
         "with-hand",
         event -> this.getApplication().selectConfiguration((MutablePluginConfiguration)((Image)event.getSource()).getData())
      );
      followIcon.setData(pc);
      followIcon.addStyleName("navigateToPluginIcon");
      return followIcon;
   }

   private Image createDeleteIcon(Entry<String, MutablePluginConfiguration> mapping) {
      Image deleteIcon = this.makeIcon(
         GuiResources.ICON_DELETE_RECORD, "Delete this row.", "with-hand", event -> this.deleteRow((String)((Image)event.getSource()).getData())
      );
      deleteIcon.setData(mapping.getKey());
      deleteIcon.addStyleName("deleteRowIcon");
      return deleteIcon;
   }

   private Image createNewPluginIcon(Entry<String, MutablePluginConfiguration> mapping) {
      Image newPluginIcon = this.makeIcon(
         GuiResources.ICON_NEW, "Create a new plugin.", "with-hand", event -> this.createNewPlugin((String)((Image)event.getSource()).getData())
      );
      newPluginIcon.setData(mapping.getKey());
      newPluginIcon.addStyleName("newPluginIcon");
      return newPluginIcon;
   }

   @Override
   protected void updateUI(ConfigurableProperty property) {
      this.newRowIcon.setVisible(!property.isReadOnly());
      this.getInnerField().setVisible(this.mapTextCombo != null);
      this.addCommonIconsForProperty(property);
   }

   @Override
   public Object getValue() {
      if (this.mapTextCombo == null) {
         return null;
      }

      Map<String, MutablePluginConfiguration> valueMap = new HashMap<>();

      for (Entry<TextField, ComboBox> mapping : this.mapTextCombo.entrySet()) {
         MutablePluginConfiguration pc = (MutablePluginConfiguration)mapping.getValue().getValue();
         Object key = mapping.getKey().getValue();
         valueMap.put(key != null ? key.toString() : "", pc);
      }

      return valueMap;
   }

   private Map<String, MutablePluginConfiguration> getMapValue() {
      return (Map<String, MutablePluginConfiguration>)this.getValue();
   }

   @Override
   public void valueChange(ValueChangeEvent event) {
      if (event instanceof com.vaadin.v7.ui.Field.ValueChangeEvent fieldChangeEvent) {
         Map<String, MutablePluginConfiguration> newValue = this.getMapValue();
         String newKey = null;
         if (fieldChangeEvent.getSource() instanceof TextField field) {
            newKey = (String)field.getValue();
            if (this.keys.contains(newKey)) {
               this.restoreOldValue(newValue, field);
               this.scrollToLineWithIndex(List.copyOf(this.keys).indexOf(field.getValue()));
               Notification.show("Key \"" + newKey + "\" already exists!", Type.WARNING_MESSAGE);
               return;
            }
         }

         ChangePropertyConfigurationCommand.createChangePluginMapPropertyConfigurationCmdBuilder()
            .property(this.getConfigurableProperty())
            .newValue(newValue)
            .build()
            .execute();
         this.setInternalValue(newValue);
         if (newKey != null) {
            this.scrollToLineWithIndex(List.copyOf(this.keys).indexOf(newKey));
            this.focusPluginField(newKey);
         }
      }
   }

   private void restoreOldValue(Map<String, MutablePluginConfiguration> newValue, TextField field) {
      SetView<String> keyDifference = Sets.difference(ImmutableSet.copyOf(this.keys), newValue.keySet());
      this.keys.removeAll(keyDifference);
      field.setValue(keyDifference.iterator().hasNext() ? (String)keyDifference.iterator().next() : null);
   }

   @Override
   public void click(ClickEvent event) {
      super.click(event);
      if (event.getSource() == this.getInheritIcon()) {
         this.setInternalValue(this.getConfigurableProperty().getValue());
      } else if (event.getSource() == this.getOverrideIcon()) {
         Map<String, MutablePluginConfiguration> map = this.getMapValue();
         this.setInternalValue(map);
         if (map == null || map.isEmpty()) {
            this.insertNewRow();
         }
      } else if (event.getSource() == this.newRowIcon) {
         this.insertNewRow();
      } else if (event.getSource() == this.getUndefinePropertyIcon()) {
         this.setInternalValue(this.getConfigurableProperty().getValue());
      }
   }

   private void deleteRow(String key) {
      Map<String, MutablePluginConfiguration> pcMap = this.getMapValue();
      if (pcMap != null) {
         pcMap.remove(key);
         ChangePropertyConfigurationCommand.createChangePluginMapPropertyConfigurationCmdBuilder()
            .property(this.getConfigurableProperty())
            .newValue(pcMap)
            .build()
            .execute();
      }

      this.setInternalValue(pcMap);
   }

   private void insertNewRow() {
      Map<String, MutablePluginConfiguration> pcMap = this.getMapValue();
      if (pcMap == null) {
         pcMap = new HashMap<>();
      }

      if (!pcMap.containsValue(null)) {
         String key = "";
         pcMap.put(key, null);
         this.setInternalValue(pcMap);
         this.scrollToLastLine();
         this.focusKeyTextfield(key);
      }
   }

   private void focusKeyTextfield(String key) {
      for (TextField field : this.mapTextCombo.keySet()) {
         if (((String)field.getValue()).equals(key)) {
            field.focus();
            break;
         }
      }
   }

   private void focusPluginField(String key) {
      for (Entry<TextField, ComboBox> field : this.mapTextCombo.entrySet()) {
         if (((String)field.getKey().getValue()).equals(key)) {
            field.getValue().focus();
            break;
         }
      }
   }

   private void createNewPlugin(String key) {
      NewPluginDialog dialog;
      try {
         dialog = new NewPluginDialog(this.getPropertyAdapter().getPluginConfiguration(), null, this.getPropertyAdapter().getMapElementType());
      } catch (InitializeException e) {
         throw new IllegalStateException(e);
      }

      String replacedKey = key;
      this.getApplication().addWindow(dialog);
      dialog.addCloseListener(e -> {
         MutablePluginConfiguration pc = ((NewPluginDialog)e.getSource()).getPluginConfiguration();
         if (pc != null) {
            this.replacePlugin(replacedKey, pc);
            if (dialog.isEditAfterCreatingPlugin()) {
               this.getApplication().selectConfiguration(pc);
            }
         }
      });
   }

   private void replacePlugin(String key, MutablePluginConfiguration pc) {
      Map<String, MutablePluginConfiguration> map = this.getMapValue();
      if (map == null) {
         map = new HashMap<>();
      }

      map.put(key, pc);
      new CreateNewConnectedPluginConfigurationCommand(this.getApplication().getConfigurationDictionary(), pc, this.getConfigurableProperty(), map).execute();
      this.setInternalValue(map);
   }

   private static final class PluginMapKeyComparator implements Comparator<String> {
      public int compare(String o1, String o2) {
         if (StringUtils.isBlank(o1) && StringUtils.isBlank(o2)) {
            return 0;
         } else if (StringUtils.isBlank(o1)) {
            return 1;
         } else {
            return StringUtils.isBlank(o2) ? -1 : o1.compareTo(o2);
         }
      }
   }
}
