package hibergui;

import static hibergui.HiberGUI.*;
import hibergui.assoc.O2MEdit;
import hibergui.event.ChangeEvent;
import hibergui.event.ChangeListener;
import hibergui.event.KeyAdapter;
import hibergui.event.KeyEvent;
import hibergui.event.MessageListener;
import hibergui.event.ModificationEvent;
import hibergui.event.ModificationListener;
import hibergui.verify.VerificationError;
import hibergui.verify.VerificationException;
import hibergui.verify.VerificationWarning;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.NestedNullException;
import org.apache.commons.beanutils.PropertyUtils;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.swt.SWT;
import static org.eclipse.swt.SWT.*;

import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;

/**
 * Common superclass of HiberGUI forms. Using forms business logic entities of
 * type T can be created, deleted, updated or searched for. The individual
 * properties of the entities are handled by edits.
 * 
 * Forms have three parts. Edits are on the top of the form, they are created by
 * the createFields method. Below the edits there are four buttons: create,
 * delete, clear and search. The third part of forms is the selector table. It
 * is visible only in search mode. It can be used for selecting one or more
 * business entity.
 * 
 * 
 * 
 * @author Miklós Espák
 * 
 * @param <T>
 *            The type of the business object edited by the form.
 */
public abstract class Form<T> extends Component<T> {

	/**
	 * The actual business object selected by the form.
	 */
	private T value;
	private IStructuredSelection selection = StructuredSelection.EMPTY;

	/**
	 * The class representing the type parameter of the form.
	 */
	protected final Class<T> persistentClass;

	private String title, message;
	private int messageType;

	private Button btnCreate, btnDelete, btnSearchMode, btnClear;

	private Table tbl;
	private Map<TableColumn, String> columnProperties;
	private TableViewer tbvSelector;

	protected List<Edit<?>> fieldEditors;

	private boolean dirty, hasChanged;
	private boolean searchMode;

	private final ChangeListener lsnUpdate = new ChangeListener() {
		public <Q> void change(ChangeEvent<? extends Component<Q>, Q> e) {
			Edit<?> edit = (Edit<?>) e.getSource();
			try {
				edit.verify();
				modify();
				edit.markClean();
			} catch (Exception exc) {
			}
		}
	};

	private final ModificationListener lsnDirty = new ModificationListener() {
		public void modification(ModificationEvent<?> event) {
			Edit<?> edit = event.getSource();
			dirty = true;
			if (searchMode && selection.isEmpty()) {
				refreshSelector();
				setMessage(tbl.getItemCount() + I18n.get("form.title.found"),
						IMessageProvider.INFORMATION);
			} else {
				try {
					edit.verify();
				} catch (VerificationException w) {
				}
				verify();
			}
			btnClear.setEnabled(true);
		}
	};

	private ISelectionChangedListener lsnSelection = new ISelectionChangedListener() {
		@SuppressWarnings("unchecked")
		public void selectionChanged(SelectionChangedEvent event) {
			setMessage(null);
			if (selection.isEmpty()) {
				setTitle(I18n.get("form.title.select"));
				// removeModificationListeners(lsnDirty);
				if (dirty)
					dirty = false;
				btnClear.setEnabled(true);
				// for (Edit<?> edit: fieldEditors)
				// edit.setEditable(false);
			} else
				removeChangeListeners(lsnUpdate);
			// selection = (IStructuredSelection) event.getSelection();
			selection = (IStructuredSelection) tbvSelector.getSelection();
			if (selection.size() == 1) {
				fill((T) selection.getFirstElement());
				addChangeListeners(lsnUpdate);
			} else
				clear();
		}
	};

	/**
	 * Constructs a form.
	 * 
	 * @param cptParent
	 *            The composite which the form will be placed on. The
	 *            parent of the control of the form.
	 * @param style
	 *            The style of the form. It is a combination of constants
	 *            defined in <a>hibergui.HiberGUI</a>.
	 */
	public Form(Composite cptParent, int style) {
		super(cptParent, style);

		persistentClass = (Class<T>) ((ParameterizedType) getClass()
				.getGenericSuperclass()).getActualTypeArguments()[0];

		btnCreate.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				create();
			}
		});
		btnDelete.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				delete();
			}
		});
		btnSearchMode.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				if (searchMode && !selection.isEmpty()) {
					set((T) selection.getFirstElement());
					return;
				}
				searchMode = !searchMode;
				if (value != null)
					set(null);
				else
					clear();
				setSearchMode(searchMode);
			}
		});
		btnClear.addSelectionListener(new SelectionAdapter() {
			public void widgetSelected(SelectionEvent e) {
				if (value != null)
					set(null);
				else {
					if (!selection.isEmpty())
						removeChangeListeners(lsnUpdate);
					clear();
				}
			}
		});
		tbvSelector = createSelector(tbl);
		selection = StructuredSelection.EMPTY;

		btnCreate.addKeyListener(lsn_WIDGET_KEY);
		btnDelete.addKeyListener(lsn_WIDGET_KEY);
		btnSearchMode.addKeyListener(lsn_WIDGET_KEY);
		btnClear.addKeyListener(lsn_WIDGET_KEY);
		tbl.addKeyListener(lsn_WIDGET_KEY);

		Listener listener = new Listener() {
			public void handleEvent(Event e) {
			}
		};
		TableColumn[] columns = tbl.getColumns();
		for (int i = 0; i < columns.length; i++) {
			columns[i].setMoveable(true);
			columns[i].addListener(SWT.Move, listener);
		}
		// tbvSelector.setSorter(new ViewerSorter() {
		// @Override
		// public int compare(Viewer arg0, Object arg1, Object arg2) {
		//				
		// return super.compare(arg0, arg1, arg2);
		// }
		// });
		// Add sort indicator and sort data when column selected
		SelectionAdapter lsnReorder = new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent e) {
				// determine new sort column and direction
				TableColumn sortColumn = tbl.getSortColumn();
				TableColumn currentColumn = (TableColumn) e.widget;
				int direction = tbl.getSortDirection();
				if (sortColumn == currentColumn) {
					direction = direction == SWT.UP ? SWT.DOWN : SWT.UP;
				} else {
					tbl.setSortColumn(currentColumn);
					direction = SWT.UP;
				}
				tbl.setSortDirection(direction);
				refreshSelector();
			}
		};
		for (TableColumn col : tbl.getColumns())
			col.addSelectionListener(lsnReorder);
		tbl.setSortColumn(tbl.getColumn(0));
		tbl.setSortDirection(SWT.UP);

		btnDelete.setEnabled(false);
		btnClear.setEnabled(false);
		btnCreate.setEnabled(false);

		addKeyListener(new KeyAdapter() {
			@Override
			public void keyPressed(KeyEvent event) {
				switch (event.wrapped.keyCode) {
				case F1:
					if (value != null)
						set(null);
					else {
						if (!selection.isEmpty())
							removeChangeListeners(lsnUpdate);
						clear();
					}
					break;
				case F3:
					if (searchMode && !selection.isEmpty()) {
						set((T) selection.getFirstElement());
						return;
					}
					if (value != null)
						set(null);
					setSearchMode(!searchMode);
					break;
				case F5:
					refresh();
					break;
				case CR:
				case KEYPAD_CR:
					if (!searchMode) {
						if (value == null)
							create();
					} else if (!tbl.isFocusControl() && value == null)
						set((T) tbvSelector.getElementAt(0));
					break;
				// case ARROW_DOWN:
				// if (searchMode && !tbl.isFocusControl()
				// && tbl.getItemCount() > 0) {
				// tbvSelector.setSelection(new StructuredSelection(tbl
				// .getItem(0)));
				// tbl.setFocus();
				// }
				// break;
				case ESC:
					if (searchMode)
						setSearchMode(false);
					break;
				}
			}
		});

		addModificationListeners(lsnDirty);

		setTitle(I18n.get("form.title.new"));
		getControl().getShell().pack();
		hideSelector();
		getControl().getShell().setMinimumSize(
				getControl().getShell().getSize());

		// btnDelete.setVisible(false); // For sure.
	}

	@Override
	protected void init() {
		fieldEditors = new ArrayList<Edit<?>>();
		columnProperties = new HashMap<TableColumn, String>();
	}

	/**
	 * Disposes the edits of the form.
	 * 
	 * @see hibergui.Component#dispose()
	 */
	@Override
	protected void dispose() {
		for (Edit<?> edit : fieldEditors)
			edit.dispose();
	}

	/* package */void addEdit(Edit<?> edit) {
		fieldEditors.add(edit);
	}

	@Override
	protected Composite createContents(Composite cptParent) {
		Composite cpt = new Composite(cptParent, SWT.NONE);

		FormLayout formLayout = new FormLayout();
		FormData data;

		cpt.setLayout(formLayout);

		Composite cptFields = createFields(cpt);
		Composite cptButtons = createButtons(cpt);
		tbl = createTable(cpt);

		data = new FormData();
		data.top = new FormAttachment(0, 0);
		data.left = new FormAttachment(0, 0);
		data.right = new FormAttachment(100, 0);
		cptFields.setLayoutData(data);

		data = new FormData();
		data.top = new FormAttachment(cptFields, 10);
		data.left = new FormAttachment(0, 0);
		data.right = new FormAttachment(100, 0);
		cptButtons.setLayoutData(data);

		data = new FormData();
		data.top = new FormAttachment(cptButtons, 6);
		data.left = new FormAttachment(0, 0);
		data.right = new FormAttachment(100, 0);
		data.bottom = new FormAttachment(100, 0);
		tbl.setLayoutData(data);

		return cpt;
	}

	/**
	 * Creates the composite containing the edits of the form. Subclasses should
	 * override it. The created composite will appear on the top part or the
	 * form.
	 * 
	 * @param cptParent
	 *            The composite which the composite of the edits should be
	 *            placed on.
	 * @return The composite containing the edits.
	 */
	protected abstract Composite createFields(Composite cptParent);

	/**
	 * Creates the composite containing the control buttons of the form.
	 * Subclasses should override it. The created composite will appear below
	 * the edits.
	 * 
	 * There are four buttons on the created composite:
	 * <ul>
	 * <li>Create: creates a business entity from the values typed into edits.
	 * The created object will be the actual entity of the form.</li>
	 * <li>Delete: deletes the actual entity from the storage. The actual
	 * entity will be null.</li>
	 * <li>Clear: clears the fields of the form.</li>
	 * <li>Search: toggle button to switch search mode on/off.</li>
	 * </ul>
	 * 
	 * @param cptParent
	 *            The composite which the composite of the buttons should be
	 *            placed on.
	 * @return The composite containing the edits.
	 */
	protected Composite createButtons(final Composite cptParent) {
		FormLayout formLayout = new FormLayout();
		FormData fdata;
		Composite cptButtons = new Composite(cptParent, SWT.NONE);
		cptButtons.setLayout(formLayout);

		btnCreate = new Button(cptButtons, SWT.PUSH);
		btnCreate.setText(I18n.get("create"));
		fdata = new FormData();
		fdata.width = 100;
		fdata.top = new FormAttachment(0, 0);
		fdata.left = new FormAttachment(0, 12);
		fdata.bottom = new FormAttachment(100, 0);
		btnCreate.setLayoutData(fdata);

		btnDelete = new Button(cptButtons, SWT.PUSH);
		btnDelete.setText(I18n.get("delete"));
		fdata = new FormData();
		fdata.width = 100;
		fdata.top = new FormAttachment(0, 0);
		fdata.left = new FormAttachment(btnCreate, 12);
		fdata.bottom = new FormAttachment(100, 0);
		btnDelete.setLayoutData(fdata);

		btnClear = new Button(cptButtons, SWT.PUSH);
		btnClear.setText(I18n.get("new_clear"));
		fdata = new FormData();
		fdata.width = 120;
		fdata.top = new FormAttachment(0, 0);
		fdata.right = new FormAttachment(100, -12);
		fdata.bottom = new FormAttachment(100, 0);
		btnClear.setLayoutData(fdata);

		btnSearchMode = new Button(cptButtons, SWT.TOGGLE);
		btnSearchMode.setText(I18n.get("searchMode"));
		fdata = new FormData();
		fdata.width = 120;
		fdata.top = new FormAttachment(0, 0);
		fdata.right = new FormAttachment(btnClear, -12);
		fdata.bottom = new FormAttachment(100, 0);
		btnSearchMode.setLayoutData(fdata);

		Composite cptFill = new Composite(cptButtons, SWT.NONE);
		fdata = new FormData();
		fdata.height = 0;
		fdata.left = new FormAttachment(btnDelete);
		fdata.right = new FormAttachment(btnSearchMode);
		cptFill.setLayoutData(fdata);

		return cptButtons;
	}

	/**
	 * Creates the table using which some business entities found in search mode
	 * can be selected. The columns of the table are created by the
	 * createColumsForTable method. Subclasses should override that method to
	 * customize the selector table.
	 * 
	 * @param cptParent
	 *            The composite which the composite of the edits should be
	 *            placed on.
	 * @return The table containing the edits.
	 */
	protected final Table createTable(Composite cptParent) {
		tbl = new Table(cptParent, SWT.BORDER | SWT.FULL_SELECTION
				| (((style & MULTIPLE_SELECTION) != 0) ? SWT.MULTI : 0));
		tbl.setLinesVisible(true);
		tbl.setHeaderVisible(true);
		createColumnsForTable();
		return tbl;
	}

	/**
	 * Helper method for customizing the columns of the selector table.
	 * Subclasses need not override it.
	 * 
	 * @param mode
	 *            Mode (e.g. alignment)
	 * @param prop
	 *            The property of the entity showed in the column.
	 * @param name
	 *            The name of the column visible on its top.
	 * @param width
	 *            The width of the column in pixels.
	 */
	protected void addColumn(int mode, String prop, String name, int width) {
		TableColumn tcl = new TableColumn(tbl, mode);
		tcl.setText(name);
		tcl.setWidth(width);
		columnProperties.put(tcl, prop);
	}

	/**
	 * Creates the columns of the table. Subclasses should override it and
	 * invoke addColumn for the required properties.
	 */
	protected abstract void createColumnsForTable();

	private TableViewer createSelector(Table tbl) {
		TableViewer tbvSelector = new TableViewer(tbl);
		tbvSelector.setContentProvider(new IStructuredContentProvider() {
			public Object[] getElements(Object inputElement) {
				return ((java.util.List<?>) inputElement).toArray();
			}

			public void dispose() {
			}

			public void inputChanged(Viewer viewer, Object oldInput,
					Object newInput) {
			}
		});
		tbvSelector.setLabelProvider(new ITableLabelProvider() {
			public Image getColumnImage(Object element, int columnIndex) {
				return null;
			}

			@SuppressWarnings("unchecked")
			public String getColumnText(Object element, int columnIndex) {
				return Form.this.getColumnText((T) element, columnIndex);
			}

			public void addListener(ILabelProviderListener listener) {
			}

			public void dispose() {
			}

			public boolean isLabelProperty(Object element, String property) {
				return false;
			}

			public void removeListener(ILabelProviderListener listener) {
			}
		});
		tbvSelector.addSelectionChangedListener(lsnSelection);
		tbvSelector.addDoubleClickListener(new IDoubleClickListener() {
			@SuppressWarnings("unchecked")
			public void doubleClick(DoubleClickEvent event) {
				set((T) selection.getFirstElement());
			}
		});
		return tbvSelector;
	}

	protected final String getSortProperty() {
		return columnProperties.get(tbl.getSortColumn());
	}

	protected final int getSortDirection() {
		return tbl.getSortDirection();
	}

	protected String getColumnText(T t, int i) {
		String property = columnProperties.get(tbl.getColumn(i));
		try {
			Object prop = PropertyUtils.getProperty(t, property);
			if (prop != null)
				return prop.toString();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			e.printStackTrace();
		} catch (NestedNullException e) {
		}
		return "";
	}

	/**
	 * If the the form is in search mode and one entity is selected then returns
	 * that entity. Otherwise it returns the actual entity of the form.
	 * 
	 * @return The entity selected in the table or the actual entity of the
	 *         form.
	 */
	@SuppressWarnings("unchecked")
	public final T get() {
		if (searchMode && selection.size() == 1)
			return (T) selection.getFirstElement();
		return value;
	}

	/**
	 * Sets the actual entity of the form. If the form was in search mode then
	 * it will be switched off. Finally, the method fires a ChangeEvent.
	 * 
	 * @param newValue
	 *            The new actual entity of the form.
	 */
	public void set(T newValue) {
		if (value == newValue) {
			// refresh();
			return;
		}

		T oldValue = value;
		if (newValue == null) {
			value = null;
			removeChangeListeners(lsnUpdate);
			removeModificationListeners(lsnDirty);
			clear();
			addModificationListeners(lsnDirty);
			btnDelete.setEnabled(false);
			tbvSelector.setSelection(StructuredSelection.EMPTY);
			setTitle(I18n.get("form.title.new"));
		} else {
			setTitle(I18n.get("form.title.modify"));
			if (value != null || (searchMode && !selection.isEmpty()))
				removeChangeListeners(lsnUpdate);
			value = newValue;
			if (searchMode)
				setSearchMode(false);
			removeModificationListeners(lsnDirty);
			fill(newValue);
			addModificationListeners(lsnDirty);
			addChangeListeners(lsnUpdate);

			verify();

			btnDelete.setEnabled(true);
			btnClear.setEnabled(true);
			tbvSelector.removeSelectionChangedListener(lsnSelection);
			tbvSelector.setSelection(new StructuredSelection(newValue));
			tbvSelector.addSelectionChangedListener(lsnSelection);
		}
		btnCreate.setEnabled(false);

		dirty = false;
		fireChange(new ChangeEvent<Form<T>, T>(this, oldValue, newValue));
	}

	// private enum FormMode {
	// CREATE, SEARCH, SELECT, UPDATE
	// }

	// private FormMode getMode() {
	// if (value != null)
	// return FormMode.UPDATE;
	// else if (searchMode == false)
	// return FormMode.CREATE;
	// else if (selection.isEmpty())
	// return FormMode.SEARCH;
	// else
	// return FormMode.SELECT;
	// }

	/**
	 * Refreshes the form. If the form has an actual entity, edits will be
	 * filled by its properties. Otherwise, edits will be cleared. In search
	 * mode the selector table will also be refreshed.
	 */
	public void refresh() {
		removeModificationListeners(lsnDirty);
		if (value != null) {
			removeChangeListeners(lsnUpdate);
			fill(value);
			addChangeListeners(lsnUpdate);
		} else {
			clear();
		}
		dirty = false;
		addModificationListeners(lsnDirty);
		if (searchMode)
			refreshSelector();
	}

	/**
	 * Refreshes the selector table.
	 */
	public void refreshSelector() {
		try {
			// if (dirty)
			tbvSelector.setInput(findSuch());
			// else
			// tbvSelector.setInput(findAll());
		} catch (Exception exc) {
			exc.printStackTrace(System.err);
			tbvSelector.setInput(Collections.<T> emptyList());
			MessageDialog.openError(getControl().getShell(), "title", I18n
					.get(searchMode ? "error.findSuch" : "error.findAll"));
		}
	}

	/**
	 * Fills each property of the entity into the corresponding edits of the
	 * form. In contrast with the set method, this will not make its argument to
	 * the actual object of the form.
	 * 
	 * @param t
	 *            The entity which should be filled into the form.
	 */
	public void fill(T t) {
		try {
			for (Edit edit : fieldEditors)
				if (!(edit instanceof O2MEdit))
					edit.set(PropertyUtils.getProperty(t, edit
							.getPropertyName()));
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Fills some property of the entity into the corresponding edit of the
	 * form.
	 * 
	 * @param
	 * <P>
	 * The type of the property.
	 * @param property
	 *            The name of the property.
	 * @param value
	 *            The value of the property.
	 */
	public <P> void fill(String property, P value) {
		for (Edit<?> edit : fieldEditors)
			if (edit.getPropertyName().equals(property))
				((Edit<P>) edit).set(value);
	}

	/**
	 * Extracts the values of the edits of the form and assigns them to the
	 * corresponding properties of the entity.
	 * 
	 * @param t
	 */
	public void extract(T t) {
		try {
			for (Edit edit : fieldEditors)
				if (!(edit instanceof O2MEdit))
					PropertyUtils.setProperty(t, edit.getPropertyName(), edit
							.get());
		} catch (IllegalAccessException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	/**
	 * Creates a new business entity and assigns the value filled into the edits
	 * to the corresponding properties of the entity. Informally, it does
	 * something like this using reflection (unfortunately, generic parameter
	 * types cannot be instantiated):
	 * 
	 * public T extract() { T t = new T(); extract(t); return t; }
	 * 
	 * It may be overridden for performance reasons but leaving it unchanged is
	 * always a safe solution.
	 */
	protected T extract() {
		T t = null;
		try {
			t = persistentClass.newInstance();
			extract(t);
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
		return t;
	}

	/**
	 * Applies the recent changes and refreshes the form.
	 */
	public void apply() {
		try {
			commit();
			refresh();
		} catch (Exception exc) {
			exc.printStackTrace();
			MessageDialog.openError(getControl().getShell(), "title", I18n
					.get("error.apply"));
			revoke();
		}
	}

	/**
	 * Revokes the recent changes and refreshes the form.
	 */
	public void revoke() {
		try {
			rollback();
			refresh();
		} catch (Exception exc) {
			exc.printStackTrace();
			MessageDialog.openError(getControl().getShell(), "title", I18n
					.get("error.revoke"));
		}
	}

	/**
	 * Creates a business entity by extracting the values typed into the form
	 * and then saves the entity to the storage. Finally, it sets the entity as
	 * the actual entity of the form. In case of failure the operation will be
	 * revoked.
	 */
	protected void create() {
		T value = extract();
		try {
			save(value);
			// apply();
			hasChanged = true;
			tbvSelector.add(value);
			btnCreate.setEnabled(false);
			set(value);
		} catch (Exception exc) {
			exc.printStackTrace();
			MessageDialog.openError(getControl().getShell(), "title", I18n
					.get("error.create"));
			revoke();
		}
	}

	/**
	 * Modifies the selected or actual entity of the form. The entity gets the
	 * property values of the business entity extracted from the values typed
	 * into the form. The entity will be updated on the storage. Finally, it
	 * refreshes the selector table. In case of failure the operation will be
	 * revoked.
	 */
	protected void modify() {
		T object = (value == null) ? (T) selection.getFirstElement() : value;
		extract(object);
		try {
			update(object);
			hasChanged = true;
			if (value == null)
				tbvSelector.refresh(object);
		} catch (Exception exc) {
			MessageDialog.openError(getControl().getShell(), "title", I18n
					.get("error.update"));
			revoke();
		}
	}

	/**
	 * Deletes the selected business entities from the storage and removes them
	 * from the selector table as well. Finally, it sets null as the actual
	 * entity of the form. In case of failure the operation will be revoked.
	 */
	protected void delete() {
		try {
			T[] sel = (T[]) selection.toArray();
			if (MessageDialog.openQuestion(getControl().getShell(), "title",
					I18n.get("question.delete"))) {
				for (T t : sel)
					delete(t);
				// apply();
				hasChanged = true;
			}
			tbvSelector.remove(value);
			btnDelete.setEnabled(false);
			set(null);
		} catch (Exception exc) {
			exc.printStackTrace();
			MessageDialog.openError(getControl().getShell(), "title", I18n
					.get("error.delete"));
			revoke();
		}
	}

	/**
	 * Clears the edits of the form and clears the selection from the selector
	 * table when search mode is on.
	 */
	private final void clear() {
		dirty = false;
		if (searchMode && !selection.isEmpty()) {
			tbvSelector.setSelection(StructuredSelection.EMPTY);
			// addModificationListeners(lsnDirty);
			// removeChangeListeners(lsnUpdate);
		}
		for (Edit<?> edit : fieldEditors)
			if (!searchMode && !edit.isFixed() && edit.hasDefault())
				edit.setDefault();
			else
				edit.set(null);
		btnClear.setEnabled(false);
		if (searchMode && !selection.isEmpty()) {
		}
		if (getControl().isVisible())
			setFocus();
	}

	/**
	 * Commits the recent changes on the storage.
	 * 
	 * @throws Exception
	 */
	protected abstract void commit() throws Exception;

	/**
	 * Rolls back the recent changes on the storage.
	 * 
	 * @throws Exception
	 */
	protected abstract void rollback() throws Exception;

	/**
	 * Saves the business entity onto the storage.
	 * 
	 * @param entity
	 *            the business entity to be saved
	 * @throws Exception
	 */
	protected abstract void save(T entity) throws Exception;

	/**
	 * Updates the business entity on the storage.
	 * 
	 * @param entity
	 *            the business entity to be updated
	 * @throws Exception
	 */
	protected abstract void update(T entity) throws Exception;

	/**
	 * Deletes the business entity from the storage.
	 * 
	 * @param entity
	 *            the business entity to be deleted
	 * @throws Exception
	 */
	protected abstract void delete(T entity) throws Exception;

	/**
	 * Finds business entities similar to the entity extracted from the form.
	 * 
	 * @return
	 * @throws Exception
	 */
	protected abstract List<T> findSuch() throws Exception;

	// protected abstract List<T> findAll() throws Exception;

	public boolean setFocus() {
		for (Edit<?> edit : fieldEditors)
			if (edit.setFocus())
				return true;
		return false;
	}

	private void addChangeListeners(ChangeListener listener) {
		for (Edit<?> edit : fieldEditors)
			edit.addChangeListener(listener);
	}

	private void removeChangeListeners(ChangeListener listener) {
		for (Edit<?> edit : fieldEditors)
			edit.removeChangeListener(listener);
	}

	public final boolean isDirty() {
		return dirty;
	}

	private void addModificationListeners(ModificationListener listener) {
		for (Edit<?> edit : fieldEditors)
			edit.addModificationListener(listener);
	}

	private void removeModificationListeners(ModificationListener listener) {
		for (Edit<?> edit : fieldEditors)
			edit.removeModificationListener(listener);
	}

	public final boolean hasChanged() {
		return hasChanged;
	}

	// --------------------------------------------------------------
	// Listening for message change
	// --------------------------------------------------------------

	private List<MessageListener> messageListeners = new ArrayList<MessageListener>();

	public void addMessageListener(MessageListener listener) {
		messageListeners.add(listener);
		listener.titleChanged(title);
		listener.messageChanged(message, messageType);
	}

	public void removeMessageListener(MessageListener listener) {
		messageListeners.remove(listener);
	}

	private void fireTitleChanged(String title) {
		for (MessageListener lsn : messageListeners)
			lsn.titleChanged(title);
	}

	private void setTitle(String title) {
		this.title = title;
		fireTitleChanged(title);
	}

	private void fireMessageChanged(String message, int type) {
		for (MessageListener lsn : messageListeners)
			lsn.messageChanged(message, type);
	}

	protected void setMessage(String message, int type) {
		this.message = message;
		this.messageType = type;
		fireMessageChanged(message, type);
	}

	public void setMessage(String message) {
		setMessage(message, IMessageProvider.NONE);
	}

	/**
	 * Returns an array of business entities selected in the selector table.
	 * 
	 * @return the selected business entities
	 */
	@SuppressWarnings("unchecked")
	public T[] getSelection() {
		// if ((style & NO_SELECTION) != 0) {
		// return tbvSelector.getInput();
		// }
		if (value != null)
			return (T[]) new Object[] { value };
		return (T[]) selection.toArray();
	}

	public void verify() {
		String firstWarning = null;
		for (Edit<?> edit : fieldEditors)
			try {
				edit.verify();
			} catch (VerificationWarning w) {
				if (firstWarning != null)
					firstWarning = w.getMessage();
				continue;
			} catch (VerificationError e) {
				setMessage(e.getMessage(), IMessageProvider.ERROR);
				btnCreate.setEnabled(false);
				return;
			} catch (VerificationException exc) {
				setMessage(exc.getMessage());
				btnCreate.setEnabled(false);
				return;
			}

		if (firstWarning != null)
			setMessage(firstWarning, IMessageProvider.WARNING);
		else
			setMessage(null);
		if (value == null) {
			btnCreate.setEnabled(true);
		}
	}

	private void showSelector() {
		Point size = getControl().getShell().getSize();
		Point tblSize = tbl.computeSize(DEFAULT, DEFAULT);
		getControl().getShell().setSize(size.x, size.y + tblSize.y);
		tbl.setVisible(true);
	}

	private void hideSelector() {
		int tblHeight = tbl.getBounds().height;
		tbl.setVisible(false);
		Point size = getControl().getShell().getSize();
		getControl().getShell().setSize(size.x, size.y - tblHeight);
	}

	/**
	 * Switches search mode on or off.
	 * 
	 * @param b
	 *            When true, search mode will be switched on, otherwise off.
	 */
	public void setSearchMode(boolean b) {
		// if (searchMode == b)
		// return;
		btnSearchMode.setSelection(b);
		searchMode = b;
		if (b) {
			setTitle(I18n.get("form.title.criteria"));
			setMessage(null);
			btnCreate.setEnabled(false);
			for (Edit<?> edit : fieldEditors)
				edit.markValid();
			refreshSelector();
			setMessage(tbl.getItemCount() + I18n.get("form.title.found"),
					IMessageProvider.INFORMATION);
			showSelector();
			btnClear.setText(I18n.get("clear"));
		} else {
			hideSelector();
			// for (Edit<?> edit : fieldEditors)
			// // edit.setEditable(true);
			btnClear.setText(I18n.get("new"));
			if (value == null) {
				setTitle(I18n.get("form.title.new"));
				verify();
			}
			// if (selection.size() == 1)
			// addModificationListeners(lsnDirty);
		}
	}

	/**
	 * Returns whether the search mode is on.
	 * 
	 * @return the state of the search mode
	 */
	public boolean isSearchMode() {
		return searchMode;
	}

	/**
	 * Sets the title of the window containing the form.
	 * 
	 * @param windowTitle
	 *            the new window title
	 */
	protected final void setWindowTitle(String windowTitle) {
		getControl().getShell().setText(windowTitle);
	}
}
