/*
 * (c) Copyright Sysdeo SA 2001, 2002.
 * All Rights Reserved.
 */

package com.sysdeo.eclipse.tomcat.editors;

import static com.sysdeo.eclipse.tomcat.TomcatPluginResources.PREF_PAGE_ADDBUTTON_LABEL;
import static com.sysdeo.eclipse.tomcat.TomcatPluginResources.PREF_PAGE_DOWNBUTTON_LABEL;
import static com.sysdeo.eclipse.tomcat.TomcatPluginResources.PREF_PAGE_LIST_SEPARATOR;
import static com.sysdeo.eclipse.tomcat.TomcatPluginResources.PREF_PAGE_REMOVEBUTTON_LABEL;
import static com.sysdeo.eclipse.tomcat.TomcatPluginResources.PREF_PAGE_UPBUTTON_LABEL;
import static com.sysdeo.eclipse.tomcat.TomcatPluginResources.PREF_PAGE_UPDATEBUTTON_LABEL;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.StringTokenizer;

import org.eclipse.jface.dialogs.IDialogConstants;
import org.eclipse.jface.dialogs.InputDialog;
import org.eclipse.jface.preference.FieldEditor;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
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;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.List;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Widget;

/**
 * ListFieldEditor
 *
 */
public class ListFieldEditor extends FieldEditor implements SelectionListener, DisposeListener {

	/**
	 * The list widget; <code>null</code> if none (before creation or after
	 * disposal).
	 */
	private List list;

	/**
	 * The button box containing the Add, Remove, Up, and Down buttons;
	 * <code>null</code> if none (before creation or after disposal).
	 */
	private Composite buttonBox;
	/** addButton */
	private Button addButton;
	/** removeButton */
	private Button removeButton;
	/** updateButton */
	private Button updateButton;
	/** upButton */
	private Button upButton;
	/** downButton */
	private Button downButton;

	/**
	 * Creates a new list field editor
	 */
	public ListFieldEditor() {
		super();
	}

	/**
	 * Creates a list field editor.
	 *
	 * @param name the name of the preference this field editor works on
	 * @param labelText the label text of the field editor
	 * @param parent the parent of the field editor's control
	 */
	public ListFieldEditor(final String name, final String labelText, final Composite parent) {
		init(name, labelText);
		createControl(parent);
	}

	/**
	 * @return the list
	 */
	protected List getList() {
		return this.list;
	}

	/**
	 * @param val the list to set
	 */
	protected void setList(final List val) {
		this.list = val;
	}

	/**
	 * @return the buttonBox
	 */
	protected Composite getButtonBox() {
		return this.buttonBox;
	}

	/**
	 * @param val the buttonBox to set
	 */
	protected void setButtonBox(final Composite val) {
		this.buttonBox = val;
	}

	/**
	 * @return the addButton
	 */
	protected Button getAddButton() {
		return this.addButton;
	}

	/**
	 * @param val the addButton to set
	 */
	protected void setAddButton(final Button val) {
		this.addButton = val;
	}

	/**
	 * @return the removeButton
	 */
	protected Button getRemoveButton() {
		return this.removeButton;
	}

	/**
	 * @param val the removeButton to set
	 */
	protected void setRemoveButton(final Button val) {
		this.removeButton = val;
	}

	/**
	 * @return the updateButton
	 */
	protected Button getUpdateButton() {
		return this.updateButton;
	}

	/**
	 * @param val the updateButton to set
	 */
	protected void setUpdateButton(final Button val) {
		this.updateButton = val;
	}

	/**
	 * @return the upButton
	 */
	protected Button getUpButton() {
		return this.upButton;
	}

	/**
	 * @param val the upButton to set
	 */
	protected void setUpButton(final Button val) {
		this.upButton = val;
	}

	/**
	 * @return the downButton
	 */
	protected Button getDownButton() {
		return this.downButton;
	}

	/**
	 * @param val the downButton to set
	 */
	public void setDownButton(final Button val) {
		this.downButton = val;
	}

	/**
	 * Notifies that the Add button has been pressed.
	 */
	protected void addPressed() {
		setPresentsDefaultValue(false);
		String input = getNewInputObject();

		if (input != null) {
			int index = this.list.getSelectionIndex();
			if (index >= 0) {
				this.list.add(input, index + 1);
			} else {
				this.list.add(input, 0);
			}
			selectionChanged();
		}
	}

	/**
	 * Notifies that the Add button has been pressed.
	 */
	protected void updatePressed() {
		String input = getNewInputObject();

		if (input != null) {
			int index = this.list.getSelectionIndex();
			if (index >= 0) {
				this.list.setItem(index, input);
			}
			selectionChanged();
		}
	}

	/**
	 * Method declared on FieldEditor.
	 */
	@Override
	protected void adjustForNumColumns(final int numColumns) {
		Control control = getLabelControl();
		((GridData) control.getLayoutData()).horizontalSpan = numColumns;
		((GridData) this.list.getLayoutData()).horizontalSpan = numColumns - 1;
	}

	/**
	 * Creates the Add, Remove, Up, and Down button in the given button box.
	 *
	 * @param bBox the box for the buttons
	 */
	protected void createButtons(final Composite bBox) {
		this.addButton = createPushButton(bBox, PREF_PAGE_ADDBUTTON_LABEL);
		this.removeButton = createPushButton(bBox, PREF_PAGE_REMOVEBUTTON_LABEL);
		this.updateButton = createPushButton(bBox, PREF_PAGE_UPDATEBUTTON_LABEL);
		this.upButton = createPushButton(bBox, PREF_PAGE_UPBUTTON_LABEL);
		this.downButton = createPushButton(bBox, PREF_PAGE_DOWNBUTTON_LABEL);
	}

	/**
	 * Helper method to create a push button.
	 *
	 * @param parent the parent control
	 * @param label the resource name used to supply the button's label text
	 * @return Button
	 */
	protected Button createPushButton(final Composite parent, final String label) {
		Button button = new Button(parent, SWT.PUSH);
		button.setText(label);
		GridData data = new GridData(GridData.FILL_HORIZONTAL);
		// data.heightHint = convertVerticalDLUsToPixels(button, IDialogConstants.BUTTON_HEIGHT);
		int widthHint = convertHorizontalDLUsToPixels(button, IDialogConstants.BUTTON_WIDTH);
		data.widthHint = Math.max(widthHint, button.computeSize(SWT.DEFAULT, SWT.DEFAULT, true).x);
		// data.horizontalSpan = hs; use to have multiple buttons on the same
		// row
		button.setLayoutData(data);
		button.addSelectionListener(getSelectionListener());
		return button;
	}

	/**
	 * @see org.eclipse.swt.events.SelectionListener#widgetSelected(org.eclipse.swt.events.SelectionEvent)
	 */
	@Override
	public void widgetSelected(final SelectionEvent event) {
		Widget widget = event.widget;
		if (widget == this.addButton) {
			addPressed();
		} else if (widget == this.removeButton) {
			removePressed();
		} else if (widget == this.updateButton) {
			updatePressed();
		} else if (widget == this.upButton) {
			upPressed();
		} else if (widget == this.downButton) {
			downPressed();
		} else if (widget == this.list) {
			selectionChanged();
		}
	}


	/**
	 * @see org.eclipse.swt.events.SelectionListener#widgetDefaultSelected(org.eclipse.swt.events.SelectionEvent)
	 */
	@Override
	public void widgetDefaultSelected(final SelectionEvent e) {
		return;
	}

	/**
	 * Method declared on FieldEditor.
	 */
	@Override
	protected void doFillIntoGrid(final Composite parent, final int numColumns) {

		Control control = getLabelControl(parent);
		GridData gd = new GridData();
		gd.horizontalSpan = numColumns;
		control.setLayoutData(gd);

		this.list = getListControl(parent);
		gd = new GridData(GridData.FILL_HORIZONTAL);
		gd.heightHint = 80;
		gd.widthHint = 300;
		gd.verticalAlignment = GridData.FILL;
		gd.horizontalSpan = numColumns - 1;
		gd.horizontalAlignment = GridData.FILL;
		gd.grabExcessHorizontalSpace = true;
		this.list.setLayoutData(gd);

		this.buttonBox = getButtonBoxControl(parent);
		gd = new GridData();
		gd.verticalAlignment = GridData.BEGINNING;
		this.buttonBox.setLayoutData(gd);
	}

	/**
	 * Method declared on FieldEditor.
	 */
	@Override
	protected void doLoad() {
		if (this.list != null) {
			String s = getPreferenceStore().getString(getPreferenceName());
			String[] array = parseString(s);
			for (final String str : array) {
				this.list.add(str);
			}
		}
	}

	/**
	 * Method declared on FieldEditor.
	 */
	@Override
	protected void doLoadDefault() {
		if (this.list != null) {
			this.list.removeAll();
			String s = getPreferenceStore().getDefaultString(getPreferenceName());
			String[] array = parseString(s);
			for (final String str : array) {
				this.list.add(str);
			}
		}
	}

	/**
	 * Method declared on FieldEditor.
	 */
	@Override
	protected void doStore() {
		String s = createList(this.list.getItems());
		getPreferenceStore().setValue(getPreferenceName(), s);
	}

	/**
	 * Notifies that the Down button has been pressed.
	 */
	protected void downPressed() {
		swap(false);
	}

	/**
	 * Returns this field editor's button box containing the Add, Remove, Up,
	 * and Down button.
	 *
	 * @param parent the parent control
	 * @return the button box
	 */
	public Composite getButtonBoxControl(final Composite parent) {
		if (this.buttonBox == null) {
			this.buttonBox = new Composite(parent, SWT.NULL);
			GridLayout layout = new GridLayout();
			// change value to have multiple buttons on
			layout.numColumns = 1;
			// the same row
			layout.marginWidth = 0;
			layout.marginHeight = 0;
			layout.verticalSpacing = 0;
			this.buttonBox.setLayout(layout);
			createButtons(this.buttonBox);
			this.buttonBox.addDisposeListener(this);

		} else {
			checkParent(this.buttonBox, parent);
		}

		selectionChanged();
		return this.buttonBox;
	}

	/**
	 * @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent)
	 */
	@Override
	public void widgetDisposed(final DisposeEvent event) {
		if (event.widget == this.list) {
			this.list = null;
		} else {
			this.addButton = null;
			this.removeButton = null;
			this.upButton = null;
			this.downButton = null;
			this.buttonBox = null;
		}
	}

	/**
	 * Returns this field editor's list control.
	 *
	 * @param parent the parent control
	 * @return the list control
	 */
	public List getListControl(final Composite parent) {
		if (this.list == null) {
			this.list = new List(parent, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL);
			this.list.addSelectionListener(getSelectionListener());
			this.list.addDisposeListener(this);
		} else {
			checkParent(this.list, parent);
		}
		return this.list;
	}

	/**
	 * Method declared on FieldEditor.
	 */
	@Override
	public int getNumberOfControls() {
		return 2;
	}

	/**
	 * Returns this field editor's selection listener. The listener is created
	 * if nessessary.
	 *
	 * @return the selection listener
	 */
	protected SelectionListener getSelectionListener() {
		return this;
	}

	/**
	 * Returns this field editor's shell.
	 * <p>
	 * This method is internal to the framework; subclassers should not call
	 * this method.
	 * </p>
	 *
	 * @return the shell
	 */
	protected Shell getShell() {
		if (this.addButton == null) {
			return null;
		}
		return this.addButton.getShell();
	}

	/**
	 * Notifies that the Remove button has been pressed.
	 */
	protected void removePressed() {
		setPresentsDefaultValue(false);
		int[] indices = this.list.getSelectionIndices();
		if (indices.length > 0) {
			this.list.remove(indices);
			selectionChanged();
		}
	}

	/**
	 * Notifies that the list selection has changed.
	 */
	protected void selectionChanged() {

		int index = this.list.getSelectionIndex();
		int size = this.list.getItemCount();

		this.removeButton.setEnabled(index >= 0);
		this.upButton.setEnabled(size > 1 && index > 0);
		this.downButton.setEnabled(size > 1 && index >= 0 && index < size - 1);
	}

	/**
	 * Method declared on FieldEditor.
	 */
	@Override
	public void setFocus() {
		if (this.list != null) {
			this.list.setFocus();
		}
	}

	/**
	 * Moves the currently selected item up or down.
	 *
	 * @param up
	 *            <code>true</code> if the item should move up, and
	 *            <code>false</code> if it should move down
	 */
	protected void swap(final boolean up) {
		setPresentsDefaultValue(false);
		int index = this.list.getSelectionIndex();
		int target = up ? index - 1 : index + 1;

		if (index >= 0) {
			String[] selection = this.list.getSelection();
			if (selection.length == 1) {
				this.list.remove(index);
				this.list.add(selection[0], target);
				this.list.setSelection(target);
			}
		}
		selectionChanged();
	}

	/**
	 * Notifies that the Up button has been pressed.
	 */
	protected void upPressed() {
		swap(true);
	}

	/**
	 * Creates and returns a new item for the list.
	 * <p>
	 * Subclasses must implement this method.
	 * </p>
	 *
	 * @return a new item
	 */
	protected String getNewInputObject() {

		String defaultValue = "";
		if (this.list.getSelection().length != 0) {
			defaultValue = this.list.getSelection()[0];
		}

		InputDialog dialog = new InputDialog(getShell(), "New Tomcat JVM paramater",
							"Enter a JVM parameter", defaultValue, null);
		String param = null;
		int dialogCode = dialog.open();
		if (dialogCode == Window.OK) {
			param = dialog.getValue();
			if (param != null) {
				param = param.trim();
				if (param.isEmpty()) {
					return null;
				}
			}
		}
		return param;
	}

	/**
	 * Combines the given list of items into a single string. This method is the
	 * converse of <code>parseString</code>.
	 * <p>
	 * Subclasses must implement this method.
	 * </p>
	 *
	 * @param items the list of items
	 * @return the combined string
	 * @see #parseString
	 */
	private static String createList(final String[] items) {
		StringBuilder path = new StringBuilder();
		for (int i = 0; i < items.length; i++) {
			try {
				if (0 < i) {
					path.append(PREF_PAGE_LIST_SEPARATOR);
				}
				path.append(URLEncoder.encode(items[i], "UTF-8"));
			} catch (final UnsupportedEncodingException e) {
				e.printStackTrace();
			}
		}
		return path.toString();
	}

	/**
	 * Splits the given string into a list of strings. This method is the
	 * converse of <code>createList</code>.
	 * <p>
	 * Subclasses must implement this method.
	 * </p>
	 *
	 * @param stringList the string
	 * @return an array of <code>String</code>
	 * @see #createList
	 */
	private static String[] parseString(final String stringList) {
		StringTokenizer st = new StringTokenizer(stringList, PREF_PAGE_LIST_SEPARATOR);
		ArrayList<String> v = new ArrayList<>();
		while (st.hasMoreTokens()) {
			try {
				v.add(URLDecoder.decode(st.nextToken(), "UTF-8"));
			} catch (final UnsupportedEncodingException e) {
				e.printStackTrace();
			}
		}
		return v.toArray(new String[v.size()]);
	}
}
