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

import com.airlock.iam.configeditor.shared.application.ConfigEditorProperties;
import com.airlock.iam.plugin.api.config.ConfigurationVariable;
import com.airlock.iam.plugin.api.exception.ConfigurationException;
import com.airlock.iam.plugin.api.exception.PropertyConfigurationException;
import com.airlock.iam.plugin.api.testlet.Testlet;
import com.airlock.iam.plugin.api.testlet.Testlet.TestletResult;
import com.airlock.iam.plugin.api.testlet.Testlet.TestletType;
import com.airlock.iam.plugin.api.testlet.Testlet.TestletResult.TestResult;
import com.airlock.iam.plugin.configeditor.ConfigEditorApplication;
import com.airlock.iam.plugin.configeditor.data.ConfigurableProperty;
import com.airlock.iam.plugin.configeditor.data.PropertyAdapter;
import com.airlock.iam.plugin.configeditor.ui.dialog.VariableDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.testlet.TestletInputDialog;
import com.airlock.iam.plugin.configeditor.ui.dialog.testlet.TestletResultDialog;
import com.airlock.iam.plugin.configeditor.util.GuiResources;
import com.airlock.iam.plugin.framework.application.configuration.MutablePluginConfiguration;
import com.airlock.iam.plugin.framework.misc.properties.TestletInputHelper;
import com.vaadin.event.MouseEvents.ClickEvent;
import com.vaadin.event.MouseEvents.ClickListener;
import com.vaadin.server.CompositeErrorMessage;
import com.vaadin.server.ErrorMessage;
import com.vaadin.server.ThemeResource;
import com.vaadin.server.UserError;
import com.vaadin.server.Sizeable.Unit;
import com.vaadin.shared.ui.ContentMode;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Component;
import com.vaadin.ui.DragAndDropWrapper;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Image;
import com.vaadin.ui.UI;
import com.vaadin.ui.Window;
import com.vaadin.ui.Window.CloseEvent;
import com.vaadin.v7.data.Property;
import com.vaadin.v7.data.Property.ValueChangeEvent;
import com.vaadin.v7.data.Validator.InvalidValueException;
import com.vaadin.v7.data.util.TransactionalPropertyWrapper;
import com.vaadin.v7.ui.AbstractField;
import com.vaadin.v7.ui.CheckBox;
import com.vaadin.v7.ui.CustomField;
import com.vaadin.v7.ui.Table;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;

public abstract class MedusaField<PROPERTY extends Annotation, VALUE> extends CustomField<VALUE> implements ClickListener {
   private static final String OVERRIDE_ICON_TOOLTIP = "Override this value.";
   private static final String INHERIT_ICON_TOOLTIP = "Inherit this value.";
   private static final String DEFAULT_VALUE_ICON_TOOLTIP = "This property is not defined and uses its default value.";
   private static final String DEFAULT_VALUE_ICON_STYLE = "default-icon";
   protected static final String TABLE_DEFAULT_ICON = "table-default-icon";
   protected static final String FOLLOW_ICON_TOOLTIP = "Navigate to this plugin.";
   protected static final String DELETE_ROW_ICON_TOOLTIP = "Delete this row.";
   protected static final String NEW_PLUGIN_ICON_TOOLTIP = "Create a new plugin.";
   protected static final String NEW_ROW_ICON_TOOLTIP = "Add new row.";
   private static final String UNDEFINE_ICON_TOOLTIP = "Undefine = use default if available";
   private static final String DEFINE_VARIABLE_TOOLTIP = "Define a variable.";
   private static final String EDIT_VARIABLE_TOOLTIP = "Edit variable.";
   protected static final int DEFAULT_ICON_WIDTH = 16;
   protected static final int INNER_FIELD_ICON_EXPAND_RATIO = 40;
   private final AbstractField<VALUE> innerField;
   private DragAndDropWrapper dndWrapper;
   private final HorizontalLayout fieldLayout;
   private final HorizontalLayout iconLayout;
   private final HorizontalLayout mainLayout;
   private final Image infoIcon;
   private final Image overrideIcon;
   private final Image inheritIcon;
   private final Image testletIcon;
   private final Image defaultValueIcon;
   private final Image undefinePropertyIcon;
   private final Image defineVariableIcon;
   private final Image showVariableIcon;
   private final PropertyAdapter<? extends PROPERTY> propertyAdapter;
   private String infoText = "";

   public MedusaField(PropertyAdapter<? extends PROPERTY> propertyAdapter) {
      this(propertyAdapter, false);
   }

   @SuppressFBWarnings(value = "MC_OVERRIDABLE_METHOD_CALL_IN_CONSTRUCTOR", justification = "See ALIAM-15315")
   public MedusaField(PropertyAdapter<? extends PROPERTY> propertyAdapter, boolean supportsDnd) {
      this.propertyAdapter = propertyAdapter;
      this.addStyleName("medusa-thinBorder");
      this.addStyleName("medusa-hideIcons");
      this.innerField = this.createInnerField();
      this.infoIcon = this.makeIcon(GuiResources.ICON_INFO, null, "with-hand", this);
      this.infoIcon.addStyleName("infoIcon");
      this.overrideIcon = this.makeIcon(GuiResources.ICON_OVERRIDE_VALUE, "Override this value.", "with-hand", this);
      this.overrideIcon.addStyleName("overrideValue");
      this.inheritIcon = this.makeIcon(GuiResources.ICON_INHERIT_VALUE, "Inherit this value.", "with-hand", this);
      this.inheritIcon.addStyleName("inheritValue");
      this.testletIcon = this.makeIcon(GuiResources.ICON_TESTLET, "", "with-hand", this);
      this.testletIcon.addStyleName("testletIcon");
      this.defaultValueIcon = this.makeIcon(GuiResources.ICON_DEFAULT_VALUE, "This property is not defined and uses its default value.", "default-icon", null);
      this.defaultValueIcon.addStyleName("defaultValueIcon");
      this.defaultValueIcon.setVisible(false);
      this.undefinePropertyIcon = this.makeIcon(GuiResources.ICON_DELETE, "Undefine = use default if available", "with-hand", this);
      this.undefinePropertyIcon.addStyleName("undefineIcon");
      this.defineVariableIcon = this.makeIcon(GuiResources.ICON_DEFINE_VARIABLE, "Define a variable.", "with-hand", this);
      this.defineVariableIcon.addStyleName("defineVariable");
      this.showVariableIcon = this.makeIcon(GuiResources.ICON_SHOW_VARIABLE, "Edit variable.", "with-hand", this);
      this.showVariableIcon.addStyleName("editVariable");
      List<AbstractComponent> layouts = new ArrayList<>();
      this.fieldLayout = this.makeFieldLayout(supportsDnd, this.innerField, this.defaultValueIcon);
      this.iconLayout = this.makeHorizontalLayout(this.infoIcon, this.testletIcon, this.showVariableIcon, this.defineVariableIcon);
      layouts.add(this.fieldLayout);
      layouts.add(this.iconLayout);
      this.mainLayout = this.makeHorizontalLayout(Alignment.MIDDLE_LEFT, layouts.toArray(new AbstractComponent[0]));
      this.mainLayout.setSizeFull();
      this.mainLayout.setExpandRatio(this.fieldLayout, 4.0F);
      this.mainLayout.setExpandRatio(this.iconLayout, 1.0F);
      if (!(this.innerField instanceof Table)) {
         this.innerField.addValueChangeListener(this);
      }
   }

   private HorizontalLayout makeFieldLayout(boolean supportsDnd, AbstractField<VALUE> field, Image defaultIcon) {
      HorizontalLayout layout = this.makeHorizontalLayoutWithDragAndDrop(supportsDnd, field, defaultIcon);
      layout.setSizeFull();
      layout.setComponentAlignment(defaultIcon, Alignment.TOP_LEFT);
      return layout;
   }

   private HorizontalLayout makeHorizontalLayoutWithDragAndDrop(boolean supportsDnd, AbstractField<VALUE> field, Image defaultIcon) {
      HorizontalLayout layout;
      if (supportsDnd) {
         this.dndWrapper = new TransparentDnDWrapper(field);
         this.dndWrapper.setSizeFull();
         layout = this.makeHorizontalLayout(Alignment.MIDDLE_LEFT, this.dndWrapper, defaultIcon);
         layout.setExpandRatio(this.dndWrapper, 1.0F);
      } else {
         layout = this.makeHorizontalLayout(Alignment.MIDDLE_LEFT, field, defaultIcon);
         layout.setExpandRatio(field, 1.0F);
      }

      return layout;
   }

   private HorizontalLayout makeHorizontalLayout(Alignment alignment, AbstractComponent... components) {
      HorizontalLayout hl = new HorizontalLayout();

      for (AbstractComponent component : components) {
         hl.addComponent(component);
         if (!(component instanceof CheckBox)) {
            hl.setComponentAlignment(component, alignment);
         }
      }

      return hl;
   }

   private HorizontalLayout makeHorizontalLayout(AbstractComponent... components) {
      HorizontalLayout hl = new HorizontalLayout();

      for (AbstractComponent component : components) {
         hl.addComponent(component);
      }

      return hl;
   }

   protected Image makeIcon(ThemeResource iconResource, String description, String style, ClickListener listener) {
      Image icon = new Image(null, iconResource);
      icon.setDescription(description, ContentMode.HTML);
      if (style != null) {
         icon.addStyleName(style);
      }

      if (listener != null) {
         icon.addClickListener(listener);
      }

      icon.setWidth(16.0F, Unit.PIXELS);
      return icon;
   }

   protected PropertyAdapter<? extends PROPERTY> getPropertyAdapter() {
      return this.propertyAdapter;
   }

   public abstract AbstractField<VALUE> createInnerField();

   public Class<? extends VALUE> getType() {
      return this.innerField.getType();
   }

   protected void updateUI(ConfigurableProperty property) {
      this.addCommonIconsForProperty(property);
   }

   protected final void addCommonIconsForProperty(ConfigurableProperty property) {
      this.setPropertyIconsForChild(property);
      this.setOverrideIconDescription(property);
      this.updateDefaultValueIcon(this.getConfigurableProperty().getValue());
      this.getUndefinePropertyIcon().setVisible(this.isUndefineButtonVisible());
   }

   protected final void updateUiAndValidate() {
      ConfigurableProperty property = this.getConfigurableProperty();
      this.updateUI(property);
      this.updateValidation();
      this.updateReadOnlyState(property);
      this.getApplication().updateVariablesPanel();
   }

   private void setOverrideIconDescription(ConfigurableProperty property) {
      if (!property.isValueDefinedLocally()) {
         ConfigurableProperty.ValueStatus status = property.getValueStatus();
         this.overrideIcon.setDescription("Override this value.<br/>(" + status.getDescription() + ")", ContentMode.HTML);
      } else {
         this.overrideIcon.setDescription("Override this value.");
      }
   }

   private void setPropertyIconsForChild(ConfigurableProperty property) {
      this.setIconVisible(this.overrideIcon, property.isInherited(), true);
      this.setIconVisible(this.inheritIcon, !property.isInherited() && property.hasInheritance(), true);
      this.setIconVisible(this.testletIcon, property.hasTestlet(), false, property.getTestletToolTip());
      this.defineVariableIcon.setVisible(canDefineConfigVariable(property));
      this.showVariableIcon.setVisible(hasVariable(property));
   }

   private static boolean canDefineConfigVariable(ConfigurableProperty property) {
      return ConfigEditorProperties.configEditorProperties().isShowVariables()
         && property.isPropertyDefinedLocallyInEnvironmentAndContext()
         && !property.hasVariable();
   }

   private static boolean hasVariable(ConfigurableProperty property) {
      return ConfigEditorProperties.configEditorProperties().isShowVariables()
         && property.isPropertyDefinedLocallyInEnvironmentAndContext()
         && property.hasVariable();
   }

   protected void setIconVisible(Image icon, boolean isVisible, boolean addAsFirst) {
      this.setIconVisible(icon, isVisible, addAsFirst, null);
   }

   private void setIconVisible(Image icon, boolean isVisible, boolean addAsFirst, String tooltip) {
      if (isVisible) {
         this.addComponentToIconLayout(icon, addAsFirst);
         if (icon.equals(this.testletIcon)) {
            this.testletIcon.setDescription(tooltip, ContentMode.HTML);
         }
      } else {
         this.getIconLayout().removeComponent(icon);
      }
   }

   private void addComponentToIconLayout(Image icon, boolean addAsFirst) {
      if (addAsFirst) {
         this.getIconLayout().addComponentAsFirst(icon);
      } else {
         this.getIconLayout().addComponent(icon);
      }
   }

   private void updateValidation() {
      this.getApplication().updateValidationState();
      this.markAsDirtyRecursive();
   }

   protected ConfigurableProperty getConfigurableProperty() {
      Property<?> property = this.getPropertyDataSource();
      if (property instanceof TransactionalPropertyWrapper) {
         return (ConfigurableProperty)((TransactionalPropertyWrapper)property).getWrappedProperty();
      } else {
         return property instanceof ConfigurableProperty ? (ConfigurableProperty)property : null;
      }
   }

   public void setInfo(String info) {
      this.infoIcon.setDescription(info, ContentMode.HTML);
      this.infoText = info;
   }

   public void click(ClickEvent event) {
      ConfigurableProperty configurableProperty = this.getConfigurableProperty();
      if (event.getSource() == this.inheritIcon) {
         configurableProperty.removeProperty();
         this.updateReadOnlyState(configurableProperty);
         this.getApplication().updateEnvironmentValuesState();
         this.updateUiAndValidate();
      } else if (event.getSource() == this.overrideIcon) {
         configurableProperty.overrideValue();
         this.updateReadOnlyState(configurableProperty);
         this.getInnerField().setValue(this.getValue());
         this.getApplication().updateEnvironmentValuesState();
         this.updateUiAndValidate();
      } else if (event.getSource() == this.infoIcon) {
         this.getApplication().displayDocsText(this.infoText);
         this.getApplication().selectDocsPanelInViewsTab();
      } else if (event.getSource() == this.testletIcon) {
         this.showTestletDialog();
      } else if (event.getSource() == this.undefinePropertyIcon) {
         if (!this.getPropertyAdapter().hasDefaultValue() && !this.getConfigurableProperty().hasInheritance()) {
            this.getConfigurableProperty().removeProperty();
         } else {
            this.getConfigurableProperty().setValue(null);
         }

         this.undefinePropertyIcon.setVisible(false);
         this.updateUiAndValidate();
      } else if (event.getSource() == this.showVariableIcon || event.getSource() == this.defineVariableIcon) {
         this.showVariableDialog();
      }
   }

   private void showTestletDialog() {
      ConfigurableProperty dataSource = this.getConfigurableProperty();
      Testlet testlet = dataSource.getTestlet();
      if (!testlet.areRequiredPropertiesConfigured(
         dataSource.getPluginConfiguration(), this.getApplication().getSelectedConfigurationEnvironment(), dataSource.getConfigurationContext()
      )) {
         showPropertiesForTestletNotConfiguredDialog(dataSource, testlet);
      } else {
         this.showTestletDialogBasedOnType(dataSource, testlet);
      }
   }

   private void showTestletDialogBasedOnType(ConfigurableProperty dataSource, Testlet testlet) {
      TestletType type = testlet.getType();
      switch (type) {
         case INPUT:
            this.showTestletInputDialog(testlet, dataSource, TestletInputHelper.findInputFields(testlet));
            break;
         case RESULT:
            this.showTestletResultDialog(testlet, dataSource);
            break;
         default:
            throw new IllegalStateException("Invalid testlet state. The testlet type is unknown: " + type);
      }
   }

   private static void showPropertiesForTestletNotConfiguredDialog(ConfigurableProperty dataSource, Testlet testlet) {
      TestletResult res = new TestletResult(TestResult.FAILED, dataSource.getTestletToolTip(), testlet.getRequiredPropertiesMissingMessage());
      UI.getCurrent().addWindow(new TestletResultDialog(dataSource.getTestletToolTip(), res));
   }

   @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE")
   private void showTestletInputDialog(Testlet testlet, ConfigurableProperty dataSource, List<Field> testletFields) {
      Window dialog = new TestletInputDialog(testletFields, dataSource.getTestletToolTip());
      dialog.addCloseListener(e -> {
         TestletInputDialog inputTestletDialog = (TestletInputDialog)e.getWindow();
         if (inputTestletDialog.isUserInputCompleteAndValidated()) {
            TestletInputHelper.setInputValues(testlet, inputTestletDialog.getValues());
            this.showTestletResultDialog(testlet, dataSource);
         }
      });
      UI.getCurrent().addWindow(dialog);
   }

   private void showTestletResultDialog(Testlet testlet, ConfigurableProperty dataSource) {
      UI.getCurrent()
         .addWindow(
            new TestletResultDialog(
               dataSource.getTestletToolTip(),
               testlet,
               dataSource.getPluginConfiguration(),
               this.getApplication().getSelectedConfigurationEnvironment(),
               dataSource.getConfigurationContext()
            )
         );
   }

   private void showVariableDialog() {
      ConfigurableProperty property = this.getConfigurableProperty();
      VariableDialog dialog = new VariableDialog(property.getVariable(), property.isSensitive());
      dialog.addCloseListener(this::handleVariableDialogClosed);
      UI.getCurrent().addWindow(dialog);
   }

   @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE")
   private void handleVariableDialogClosed(CloseEvent e) {
      VariableDialog dialog = (VariableDialog)e.getWindow();
      if (dialog.hasChanged()) {
         Optional<ConfigurationVariable> updatedVariable = dialog.getVariable();
         ConfigurableProperty property = this.getConfigurableProperty();
         if (updatedVariable.isEmpty()) {
            property.removeVariable();
         } else {
            property.setVariable(updatedVariable.get());
         }

         this.updateUI(this.getConfigurableProperty());
         this.getApplication().updateVariablesPanel();
      }
   }

   private void updateReadOnlyState(ConfigurableProperty configurableProperty) {
      boolean readOnly = configurableProperty.determineReadOnlyState();
      this.getPropertyDataSource().setReadOnly(readOnly);
      this.getInnerField().setReadOnly(readOnly);
   }

   public VALUE getValue() {
      return (VALUE)this.innerField.getValue();
   }

   public void focus() {
      this.innerField.focus();
   }

   public void valueChange(ValueChangeEvent event) {
      super.valueChange(event);
      this.updateUiAndValidate();
   }

   protected void propagateNullValueIfFieldIsUndefined() {
      if (this.isValueFromDefault() && !this.getConfigurableProperty().isReadOnly()) {
         this.innerField.setValue(null);
      }
   }

   private void updateDefaultValueIcon(Object value) {
      this.defaultValueIcon.setVisible(this.isDefaultIconVisible(value));
   }

   protected boolean isDefaultIconVisible(Object value) {
      return this.equalsDefaultValue(value) && this.isNotEmptyString(value) && this.isValueFromDefault();
   }

   protected boolean isValueFromDefault() {
      return this.getConfigurableProperty().isValueFromDefault();
   }

   private boolean isNotEmptyString(Object value) {
      return !value.toString().isEmpty();
   }

   protected boolean equalsDefaultValue(Object value) {
      Optional<?> defaultValue = this.getPropertyAdapter().getDefaultValue();
      return defaultValue.isPresent() && value.equals(defaultValue.get());
   }

   public ErrorMessage getErrorMessage() {
      ConfigurableProperty property = this.getConfigurableProperty();
      Set<ConfigurationException> errorsOfSelectedPlugins = this.getApplication().getErrorsOfSelectedPluginConfiguration();
      if (errorsOfSelectedPlugins == null) {
         return null;
      }

      List<ErrorMessage> errorMessages = new ArrayList<>();

      for (ConfigurationException ce : errorsOfSelectedPlugins) {
         if (ce instanceof PropertyConfigurationException pce
            && property.isSameConfigurationProperty(
               (MutablePluginConfiguration)ce.getPluginConfiguration(), ce.getConfigurationContext(), pce.getPropertyNames()
            )) {
            InvalidValueException exception = new InvalidValueException(pce.getDescription());
            errorMessages.add(new UserError(exception.getLocalizedMessage()));
         }
      }

      return errorMessages.isEmpty() ? null : new CompositeErrorMessage(errorMessages);
   }

   protected AbstractField<VALUE> getInnerField() {
      return this.innerField;
   }

   protected DragAndDropWrapper getDndWrapper() {
      return this.dndWrapper;
   }

   protected HorizontalLayout getFieldLayout() {
      return this.fieldLayout;
   }

   protected HorizontalLayout getIconLayout() {
      return this.iconLayout;
   }

   protected Image getOverrideIcon() {
      return this.overrideIcon;
   }

   protected Image getInheritIcon() {
      return this.inheritIcon;
   }

   protected Image getDefaultIcon() {
      return this.defaultValueIcon;
   }

   protected Image getUndefinePropertyIcon() {
      return this.undefinePropertyIcon;
   }

   protected Component initContent() {
      return this.mainLayout;
   }

   protected boolean isUndefineButtonVisible() {
      return this.getConfigurableProperty().getValueStatus() == ConfigurableProperty.ValueStatus.DEFINED_LOCALLY;
   }

   protected void addUndefinePropertyButton() {
      if (!this.getConfigurableProperty().isMandatory()) {
         this.getFieldLayout().addComponent(this.undefinePropertyIcon);
         this.getFieldLayout().setComponentAlignment(this.undefinePropertyIcon, Alignment.MIDDLE_LEFT);
      }
   }

   protected ConfigEditorApplication getApplication() {
      return (ConfigEditorApplication)UI.getCurrent();
   }
}
