// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.debug.ui.launcher;

import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.chromium.debug.core.ChromiumDebugPlugin;
import org.chromium.debug.core.model.LaunchParams.ValueConverter;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.debug.ui.AbstractLaunchConfigurationTab;
import org.eclipse.jface.preference.FieldEditor;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;

/**
 * Basic implementation of launch configuration tab. It holds input parameters and created
 * UI elements. The class is abstract and requires a set of fields to be described in
 * a ancestor. This way it can automatically handle main tab lifecycle by loading, storing
 * and initializing them.
 * @param <ELEMENTS> type interface holding UI elements
 * @param <PARAMS> type of interface holding input parameters
 */
abstract class TabBase<ELEMENTS, PARAMS> extends AbstractLaunchConfigurationTab {
  private final PARAMS params;
  private ELEMENTS tabElements = null;

  protected TabBase(PARAMS params) {
    this.params = params;
  }

  protected PARAMS getParams() {
    return params;
  }

  @Override
  public void createControl(Composite parent) {
    Runnable modifyListener = new Runnable() {
      @Override public void run() {
        updateLaunchConfigurationDialog();
      }
    };

    tabElements = createElements(parent, modifyListener);
  }

  protected abstract ELEMENTS createElements(Composite parent, Runnable modifyListener);

  @Override
  public void setDefaults(ILaunchConfigurationWorkingCopy configuration) {
    getTabFields().setDefaults(configuration, getParams());
  }

  @Override
  public void initializeFrom(ILaunchConfiguration configuration) {
    getTabFields().initializeFrom(tabElements, configuration);
  }

  @Override
  public void performApply(ILaunchConfigurationWorkingCopy configuration) {
    getTabFields().saveToConfig(tabElements, configuration);
  }

  @Override
  public boolean isValid(ILaunchConfiguration config) {
    MessageData messageData;
    try {
      messageData = isValidImpl(config);
    } catch (CoreException e) {
      ChromiumDebugPlugin.log(new Exception("Unexpected storage problem", e)); //$NON-NLS-1$
      messageData = new MessageData(true, "Internal error " + e.getMessage()); //$NON-NLS-1$
    }
    if (messageData == null) {
      messageData = MessageData.EMPTY_OK;
    }

    if (messageData.isValid()) {
      setMessage(messageData.getMessage());
      setErrorMessage(null);
    } else {
      setMessage(null);
      setErrorMessage(messageData.getMessage());
    }
    return messageData.isValid();
  }

  /**
   * Tries to check whether config is valid and return message or fails with exception.
   */
  protected abstract MessageData isValidImpl(ILaunchConfiguration config) throws CoreException;

  /**
   * Describes a tab error/warning message.
   */
  protected static class MessageData {
    public static final MessageData EMPTY_OK = new MessageData(true, null);

    private final boolean valid;
    private final String message;

    public MessageData(boolean isValid, String message) {
      this.valid = isValid;
      this.message = message;
    }
    public boolean isValid() {
      return valid;
    }
    public String getMessage() {
      return message;
    }
  }

  protected abstract TabFieldList<? super ELEMENTS, ? super PARAMS> getTabFields();

  /**
   * A dialog window tab field description. It is a static description -- it has no reference to
   * a particular element instance.
   * @param <P> physical type of field as stored in config; used internally
   * @param <L> logical type of field used in runtime operations
   * @param <E> interface to dialog elements
   * @param <C> context that holds immutable input parameter of the tab dialog
   */
  static class TabField<P, L, E, C> {
    private final String configAttributeName;
    private final TypedMethods<P> typedMethods;
    private final FieldAccess<L, E> fieldAccess;
    private final DefaultsProvider<L, C> defaultsProvider;
    private final ValueConverter<P, L> valueConverter;

    TabField(String configAttributeName, TypedMethods<P> typedMethods,
        FieldAccess<L, E> fieldAccess, DefaultsProvider<L, C> defaultsProvider,
        ValueConverter<P, L> valueConverter) {
      this.typedMethods = typedMethods;
      this.defaultsProvider = defaultsProvider;
      this.configAttributeName = configAttributeName;
      this.fieldAccess = fieldAccess;
      this.valueConverter = valueConverter;
    }

    void saveToConfig(E tabElements, ILaunchConfigurationWorkingCopy config) {
      L logicalValue = fieldAccess.getValue(tabElements);
      P persistentValue = valueConverter.encode(logicalValue);
      typedMethods.setConfigAttribute(config, configAttributeName, persistentValue);
    }

    void initializeFrom(E tabElements, ILaunchConfiguration config) {
      L fallbackLogicalValue = defaultsProvider.getFallbackValue();
      P fallbackPersistenValue = valueConverter.encode(fallbackLogicalValue);
      L value;
      try {
        P persistentValue = typedMethods.getConfigAttribute(config, configAttributeName,
            fallbackPersistenValue);
        value = valueConverter.decode(persistentValue);
      } catch (CoreException e) {
        ChromiumDebugPlugin.log(new Exception("Unexpected storage problem", e)); //$NON-NLS-1$
        value = fallbackLogicalValue;
      }
      fieldAccess.setValue(value, tabElements);
    }

    public void setDefault(ILaunchConfigurationWorkingCopy config, C context) {
      L value = defaultsProvider.getInitialConfigValue(context);
      if (value != null) {
        P persistentValue = valueConverter.encode(value);
        typedMethods.setConfigAttribute(config, configAttributeName, persistentValue);
      }
    }
  }

  static abstract class FieldAccess<T, E> {
    abstract void setValue(T value, E tabElements);
    abstract T getValue(E tabElements);
  }

  static abstract class FieldEditorAccess<T, E> extends FieldAccess<T, E> {
    private final TypedMethods<T> fieldType;

    FieldEditorAccess(TypedMethods<T> fieldType) {
      this.fieldType = fieldType;
    }

    @Override
    void setValue(T value, E tabElements) {
      FieldEditor fieldEditor = getFieldEditor(tabElements);
      fieldType.setStoreDefaultValue(fieldEditor.getPreferenceStore(),
          fieldEditor.getPreferenceName(), value);
      fieldEditor.loadDefault();
    }

    @Override
    T getValue(E tabElements) {
      FieldEditor fieldEditor = getFieldEditor(tabElements);
      storeEditor(fieldEditor, getEditorErrorValue());
      return fieldType.getStoreValue(fieldEditor.getPreferenceStore(),
          fieldEditor.getPreferenceName());
    }

    abstract FieldEditor getFieldEditor(E tabElements);
    abstract String getEditorErrorValue();
  }

  static abstract class DefaultsProvider<T, C> {
    abstract T getFallbackValue();
    abstract T getInitialConfigValue(C context);
  }

  /**
   * Provides uniform access to various signatures of config and store methods.
   */
  static abstract class TypedMethods<T> {
    abstract T getConfigAttribute(ILaunchConfiguration config, String attributeName,
        T defaultValue) throws CoreException;
    abstract void setConfigAttribute(ILaunchConfigurationWorkingCopy config, String attributeName,
        T value);

    abstract T getStoreValue(IPreferenceStore store, String preferenceName);
    abstract void setStoreDefaultValue(IPreferenceStore store, String propertyName, T value);

    static final TypedMethods<String> STRING = new TypedMethods<String>() {
      String getConfigAttribute(ILaunchConfiguration config, String attributeName,
          String defaultValue) throws CoreException {
        return config.getAttribute(attributeName, defaultValue);
      }
      public void setConfigAttribute(ILaunchConfigurationWorkingCopy config, String attributeName,
          String value) {
        config.setAttribute(attributeName, value);
      }
      void setStoreDefaultValue(IPreferenceStore store, String propertyName, String value) {
        store.setDefault(propertyName, value);
      }
      String getStoreValue(IPreferenceStore store, String preferenceName) {
        return store.getString(preferenceName);
      }
    };

    static final TypedMethods<Integer> INT = new TypedMethods<Integer>() {
      public void setConfigAttribute(ILaunchConfigurationWorkingCopy config, String attributeName,
          Integer value) {
        config.setAttribute(attributeName, value);
      }
      Integer getConfigAttribute(ILaunchConfiguration config, String attributeName,
          Integer defaultValue) throws CoreException {
        return config.getAttribute(attributeName, defaultValue);
      }
      void setStoreDefaultValue(IPreferenceStore store, String propertyName, Integer value) {
        store.setDefault(propertyName, value);
      }
      Integer getStoreValue(IPreferenceStore store, String preferenceName) {
        return store.getInt(preferenceName);
      }
    };

    static final TypedMethods<Boolean> BOOL = new TypedMethods<Boolean>() {
      public void setConfigAttribute(ILaunchConfigurationWorkingCopy config, String attributeName,
          Boolean value) {
        config.setAttribute(attributeName, value);
      }
      Boolean getConfigAttribute(ILaunchConfiguration config, String attributeName,
          Boolean defaultValue) throws CoreException {
        return config.getAttribute(attributeName, defaultValue);
      }
      void setStoreDefaultValue(IPreferenceStore store, String propertyName, Boolean value) {
        store.setDefault(propertyName, value);
      }
      Boolean getStoreValue(IPreferenceStore store, String preferenceName) {
        return store.getBoolean(preferenceName);
      }
    };
  }

  protected static Composite createDefaultComposite(Composite parent) {
    Composite composite = new Composite(parent, SWT.NULL);

    GridLayout layout = new GridLayout();
    layout.numColumns = 1;
    composite.setLayout(layout);

    GridData data = new GridData();
    data.verticalAlignment = GridData.FILL;
    data.horizontalAlignment = GridData.FILL;
    composite.setLayoutData(data);

    return composite;
  }

  protected static Composite createInnerComposite(Composite parent, int numColumns) {
    Composite composite = new Composite(parent, SWT.NONE);
    composite.setLayout(new GridLayout(numColumns, false));
    GridData gd = new GridData(GridData.FILL_HORIZONTAL);
    composite.setLayoutData(gd);
    return composite;
  }

  protected static GridLayout createHtmlStyleGridLayout(int numberOfColumns) {
    GridLayout layout = new GridLayout(numberOfColumns, false);
    layout.horizontalSpacing = 0;
    layout.verticalSpacing = 0;
    layout.marginWidth = 0;
    layout.marginHeight = 0;
    return layout;
  }

  private static void storeEditor(FieldEditor editor, String errorValue) {
    if (editor.isValid()) {
      editor.store();
    } else {
      editor.getPreferenceStore().setValue(editor.getPreferenceName(), errorValue);
    }
  }

  static class RadioButtonsLogic<K> {
    private final Map<K, Button> buttons;
    RadioButtonsLogic(Map<K, Button> buttons, final Listener listener) {
      this.buttons = buttons;
      SelectionListener selectionListener = new SelectionListener() {
        public void widgetDefaultSelected(SelectionEvent e) {
        }
        public void widgetSelected(SelectionEvent e) {
          if (listener != null && e.widget instanceof Button) {
            Button button = (Button) e.widget;
            if (button.getSelection()) {
              listener.selectionChanged();
            }
          }
        }
      };

      for (Button button : buttons.values()) {
        button.addSelectionListener(selectionListener);
      }
    }
    void select(K key) {
      for (Map.Entry<K, Button> en : buttons.entrySet()) {
        en.getValue().setSelection(en.getKey().equals(key));
      }
    }
    K getSelected() {
      for (Map.Entry<K, Button> en : buttons.entrySet()) {
        if (en.getValue().getSelection()) {
          return en.getKey();
        }
      }
      return null;
    }

    interface Listener {
      void selectionChanged();
    }
  }

  static void addRadioButtonSwitcher(final Collection<Button> buttons) {
    SelectionListener selectionListener = new SelectionListener() {
      public void widgetDefaultSelected(SelectionEvent e) {
      }
      public void widgetSelected(SelectionEvent e) {
        if (e.widget instanceof Button) {
          Button button = (Button) e.widget;
          if (!button.getSelection()) {
            return;
          }
          for (Button other : buttons) {
            if (other == button) {
              continue;
            }
            other.setSelection(false);
          }
        }
      }
    };

    for (Button button : buttons) {
      if ((button.getStyle() & SWT.NO_RADIO_GROUP) == 0) {
        throw new IllegalArgumentException();
      }
      button.addSelectionListener(selectionListener);
    }
  }

  /**
   * Encapsulate tab field list together with a way of accessing them. This includes adapting
   * type of dialog elements structure (<E>) to a type accepted by fields.
   * @param <E> type of elements structure that provides getters to dialog elements
   * @param <P> type of dialog window parameters
   */
  interface TabFieldList<E, P> {
    void setDefaults(ILaunchConfigurationWorkingCopy configuration, P params);
    void initializeFrom(E elements, ILaunchConfiguration configuration);
    void saveToConfig(E elements, ILaunchConfigurationWorkingCopy configuration);
  }

  /**
   * Create a plain implementation of {@link TabFieldList} over a list of tab fields.
   */
  static <E, P> TabFieldList<E, P> createFieldListImpl(
      final List<? extends TabField<?, ?, ? super E, P>> tabFields) {
    return new TabFieldList<E, P>() {
      public void setDefaults(ILaunchConfigurationWorkingCopy configuration, P params) {
        for (TabField<?, ?, ?, P> field : tabFields) {
          field.setDefault(configuration, params);
        }
      }

      @Override
      public void initializeFrom(E elements, ILaunchConfiguration configuration) {
        for (TabField<?, ?, ? super E, ?> field : tabFields) {
          field.initializeFrom(elements, configuration);
        }
      }

      @Override
      public void saveToConfig(E elements, ILaunchConfigurationWorkingCopy configuration) {
        for (TabField<?, ?, ? super E, ?> field : tabFields) {
          field.saveToConfig(elements, configuration);
        }
      }
    };
  }

  interface Adapter<F, T> {
    T get(F from);
  }

  /**
   * Creates {@link TabFieldList} implementation that adapts dialog elements type using the adapter
   * to inner type of dialog elements that provided list of {@link TabFieldList} accept.
   * @param list of tab fields that accepts alternative type of dialog elements structure
   * @param elementsAdapter converts external dialog elements structure type to the inner type
   */
  static <E, INNER, P> TabFieldList<E, P> createFieldListAdapting(
      final TabFieldList<? super INNER, ? super P> list, final Adapter<E, INNER> elementsAdapter) {
    return new TabFieldList<E, P>() {
      @Override public void setDefaults(ILaunchConfigurationWorkingCopy configuration, P params) {
        list.setDefaults(configuration, params);
      }
      @Override public void initializeFrom(E elements, ILaunchConfiguration configuration) {
        list.initializeFrom(elementsAdapter.get(elements), configuration);
      }
      @Override
      public void saveToConfig(E elements, ILaunchConfigurationWorkingCopy configuration) {
        list.saveToConfig(elementsAdapter.get(elements), configuration);
      }
    };
  }

  static <E, P> TabFieldList<E, P> createCompositeFieldList(
      final List<? extends TabFieldList<? super E, ? super P>> listList) {
    return new TabFieldList<E, P>() {
      @Override
      public void setDefaults(ILaunchConfigurationWorkingCopy configuration,
          P params) {
        for (TabFieldList<?, ? super P> list : listList) {
          list.setDefaults(configuration, params);
        }
      }

      @Override
      public void initializeFrom(E elements, ILaunchConfiguration configuration) {
        for (TabFieldList<? super E, ?> list : listList) {
          list.initializeFrom(elements, configuration);
        }
      }

      @Override
      public void saveToConfig(E elements,
          ILaunchConfigurationWorkingCopy configuration) {
        for (TabFieldList<? super E, ?> list : listList) {
          list.saveToConfig(elements, configuration);
        }
      }
    };
  }
}
