/*
 * Copyright (c) 2011, Patrik Dufresne. All rights reserved.
 * Patrik Dufresne PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package com.planimod.ui.views.nonavailabilities;

import java.util.Arrays;
import java.util.Date;

import org.eclipse.core.databinding.AggregateValidationStatus;
import org.eclipse.core.databinding.BindingProperties;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.UpdateValueStrategy;
import org.eclipse.core.databinding.conversion.Converter;
import org.eclipse.core.databinding.observable.map.IObservableMap;
import org.eclipse.core.databinding.observable.set.IObservableSet;
import org.eclipse.core.databinding.observable.set.WritableSet;
import org.eclipse.core.databinding.observable.value.DateAndTimeObservableValue;
import org.eclipse.core.databinding.observable.value.WritableValue;
import org.eclipse.core.databinding.validation.IValidator;
import org.eclipse.core.databinding.validation.MultiValidator;
import org.eclipse.core.databinding.validation.ValidationStatus;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.databinding.dialog.TitleAreaDialogSupport;
import org.eclipse.jface.databinding.swt.SWTObservables;
import org.eclipse.jface.databinding.viewers.ObservableMapLabelProvider;
import org.eclipse.jface.databinding.viewers.ObservableSetContentProvider;
import org.eclipse.jface.databinding.viewers.ViewersObservables;
import org.eclipse.jface.dialogs.TitleAreaDialog;
import org.eclipse.jface.window.IShellProvider;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.DateTime;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;

import com.patrikdufresne.jface.databinding.datetime.DateTimeSupport;
import com.patrikdufresne.jface.databinding.viewers.TextProposalViewerUpdater;
import com.patrikdufresne.jface.dialogs.DefaultValidationMessageProvider;
import com.patrikdufresne.jface.viewers.TextProposalViewer;
import com.patrikdufresne.util.DateFormatRegistry;
import com.patrikdufresne.util.Localized;
import com.planimod.core.Employee;
import com.planimod.core.PlanimodManagers;
import com.planimod.ui.databinding.manager.PlanimodObservables;
import com.planimod.ui.theme.Resources;
import com.planimod.ui.viewers.EmployeeViewerUtils;

/**
 * Dialog to create or edit a non-availability.
 * 
 * @author Patrik Dufresne
 * 
 */
public class NonAvailabilityDialog extends TitleAreaDialog {
	/**
	 * The dialog style constant for editing non-availability.
	 */
	public static final int EDIT = 1;
	/**
	 * The dialog style constant for creating non-availability.
	 */
	public static final int NEW = 0;

	/**
	 * Check the style flag.
	 * 
	 * @param style
	 * @return
	 */
	private static int checkStyle(int style) {
		return style == NEW ? NEW : EDIT;
	}

	/**
	 * Selected employee.
	 */
	private WritableValue employee;
	/**
	 * Widget to select the employee.
	 */
	private TextProposalViewer employeeViewer;
	/**
	 * Observable ending date.
	 */
	private WritableValue end;
	/**
	 * Selected end date.
	 */
	/**
	 * Widget to select the ending date.
	 */
	private DateTime endDate;
	/**
	 * Widget to select the ending time.
	 */
	private Combo endTime;

	/**
	 * Used for localization.
	 */
	protected Localized localized = Localized.load(NonAvailabilityDialog.class);

	/**
	 * The managers to use.
	 */
	private PlanimodManagers managers;

	/**
	 * Observable starting date.
	 */
	private WritableValue start;
	/**
	 * Widget to select start date.
	 */
	private DateTime startDate;
	/**
	 * Widget to select start time.
	 */
	private Combo startTime;
	/**
	 * Define the dialog style.
	 */
	private int style;

	/**
	 * Observable summary
	 */
	private WritableValue summary;

	/**
	 * Summary widget
	 */
	private Text summaryText;

	/**
	 * Button to set non-availability visibility.
	 */
	private Button visibileButton;

	/**
	 * Observable visibility.
	 */
	private WritableValue visible;

	public NonAvailabilityDialog(IShellProvider provider,
			PlanimodManagers managers, int style) {
		this(provider.getShell(), managers, style);
	}

	/**
	 * Create a new dialog.
	 * 
	 * @param parentShell
	 *            the parent shell, or <code>null</code> to create a top-level
	 *            shell
	 */
	public NonAvailabilityDialog(Shell parentShell, PlanimodManagers managers,
			int style) {
		super(parentShell);
		this.style = checkStyle(style);
		this.managers = managers;

		initValues();
	}

	/**
	 * This function is called by {@link #bindValues()} to bind the values
	 * related to employee selection.
	 * 
	 * @param dbc
	 */
	private void bindEmployeeValues(DataBindingContext dbc) {

		// Check if the initial employee is archived
		boolean isArchived = this.employee.getValue() != null
				&& ((Employee) this.employee.getValue()).getArchivedDate() != null;

		// Fill the viewer content using employees
		IObservableSet employees;
		if (isArchived) {
			employees = new WritableSet(
					Arrays.asList(this.employee.getValue()), Employee.class);
		} else {
			employees = PlanimodObservables.listEmployees(this.managers);
		}

		IObservableMap employeeLabels = BindingProperties.convertedValue(
				EmployeeViewerUtils.fullnameConverter()).observeDetail(
				employees);
		this.employeeViewer
				.setContentProvider(new ObservableSetContentProvider(
						new TextProposalViewerUpdater(this.employeeViewer)));
		this.employeeViewer.setLabelProvider(new ObservableMapLabelProvider(
				employeeLabels));
		this.employeeViewer.setInput(employees);

		// Create a custom update strategy to check the selection.
		UpdateValueStrategy targetToModel = new UpdateValueStrategy();
		targetToModel.setAfterGetValidator(new IValidator() {
			@Override
			public IStatus validate(Object value) {
				if (value == null) {
					return ValidationStatus.error(NonAvailabilityDialog.this.localized
							.get("NonAvailabilityDialog.message.selectEmployee")); //$NON-NLS-1$
				}
				return ValidationStatus.ok();
			}
		});

		// Bind the viewer selection to the current employee.
		dbc.bindValue(
				ViewersObservables.observeSingleSelection(this.employeeViewer),
				this.employee, targetToModel, null);
	}

	/**
	 * This function is called by {@link #bindValues()} to bind the start and
	 * end values.
	 * 
	 * @param dbc
	 */
	private void bindTimeRangeValues(DataBindingContext dbc) {

		final WritableValue middleEnd = new WritableValue(null, Date.class);
		final WritableValue middleStart = new WritableValue(null, Date.class);

		// Bind start date time
		WritableValue startTimeValue = new WritableValue(null, Date.class);
		DateTimeSupport startTimeSupport = DateTimeSupport.create(
				this.startTime,
				dbc,
				startTimeValue,
				DateFormatRegistry.getFormat(DateFormatRegistry.TIME
						| DateFormatRegistry.MEDIUM), DateTimeSupport.STEP_60);
		dbc.bindValue(
				new DateAndTimeObservableValue(SWTObservables
						.observeSelection(this.startDate), startTimeValue),
				middleStart);
		if (this.endTime.getLayoutData() instanceof GridData) {
			((GridData) this.startTime.getLayoutData()).widthHint = startTimeSupport
					.getWidthHint();
		}

		// Bind end date time
		WritableValue endTimeValue = new WritableValue(null, Date.class);
		DateTimeSupport endTimeSupport = DateTimeSupport.create(
				this.endTime,
				dbc,
				endTimeValue,
				DateFormatRegistry.getFormat(DateFormatRegistry.TIME
						| DateFormatRegistry.MEDIUM), DateTimeSupport.STEP_60);
		if (this.endTime.getLayoutData() instanceof GridData) {
			((GridData) this.endTime.getLayoutData()).widthHint = endTimeSupport
					.getWidthHint();
		}

		dbc.bindValue(
				new DateAndTimeObservableValue(SWTObservables
						.observeSelection(this.endDate), endTimeValue),
				middleEnd);

		// Create the multi-validator for start and end time to check the start
		// value occured before the end value.
		MultiValidator validator = new MultiValidator() {
			@Override
			protected IStatus validate() {
				// Calculate the validation status
				Date start = (Date) middleStart.getValue();
				Date end = (Date) middleEnd.getValue();
				if (start == null || end == null || start.compareTo(end) > 0) {
					return ValidationStatus
							.error(NonAvailabilityDialog.this.localized
									.get("NonAvailabilityDialog.message.selectTimeRange")); //$NON-NLS-1$
				}
				return ValidationStatus.ok();
			}
		};
		dbc.addValidationStatusProvider(validator);
		dbc.bindValue(middleStart, this.start);
		dbc.bindValue(middleEnd, this.end);
	}

	/**
	 * Sets the binding between the values and the widgets.
	 */
	protected void bindValues() {
		DataBindingContext dbc = new DataBindingContext();

		// Bind summary
		dbc.bindValue(SWTObservables.observeText(this.summaryText, SWT.Modify),
				this.summary);

		// Bind employee viewer
		bindEmployeeValues(dbc);

		// Bind start and end values
		bindTimeRangeValues(dbc);

		// Bind visibility button
		dbc.bindValue(SWTObservables.observeSelection(this.visibileButton),
				this.visible);

		// Bind the title area to the binding context
		TitleAreaDialogSupport.create(this, dbc).setValidationMessageProvider(
				new DefaultValidationMessageProvider(this.localized
						.get("NonAvailabilityDialog.titleArea.message"))); //$NON-NLS-1$

		// Bind the binding status to the OK button
		UpdateValueStrategy modelToTarget = new UpdateValueStrategy();
		modelToTarget.setConverter(new Converter(IStatus.class, Boolean.TYPE) {
			@Override
			public Object convert(Object fromObject) {
				return Boolean.valueOf(((IStatus) fromObject).getSeverity() == IStatus.OK);
			}
		});
		UpdateValueStrategy targetToModel = new UpdateValueStrategy(
				UpdateValueStrategy.POLICY_NEVER);
		dbc.bindValue(SWTObservables.observeEnabled(this.getButton(OK)),
				new AggregateValidationStatus(dbc.getBindings(),
						AggregateValidationStatus.MAX_SEVERITY), targetToModel,
				modelToTarget);

	}

	/**
	 * This implementation sets the binding.
	 */
	@Override
	protected Control createContents(Composite parent) {
		Control control = super.createContents(parent);
		bindValues();
		return control;
	}

	/**
	 * This implementation create required widgets to edit a non-availability.
	 * 
	 * @param parent
	 * @return
	 */
	@Override
	protected Control createDialogArea(Composite parent) {
		Composite comp = (Composite) super.createDialogArea(parent);

		Composite composite = new Composite(comp, SWT.NONE);
		composite.setLayout(new GridLayout(1, false));
		composite
				.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));

		// Setup the title area
		getShell()
				.setText(
						this.localized
								.get(this.style == NEW ? "NonAvailabilityDialog.title.new" //$NON-NLS-1$
										: "NonAvailabilityDialog.title.edit")); //$NON-NLS-1$
		setTitle(this.localized
				.get(this.style == NEW ? "NonAvailabilityDialog.titleArea.title.new" //$NON-NLS-1$
						: "NonAvailabilityDialog.titleArea.title.edit")); //$NON-NLS-1$
		setTitleImage(Resources.getImage(Resources.DLG_EDIT_TITLE_IMAGE));

		// Employee
		Composite compEmployee = new Composite(composite, SWT.NONE);
		GridLayout layout = new GridLayout(2, false);
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		layout.verticalSpacing = 0;
		compEmployee.setLayout(layout);
		compEmployee
				.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));
		Label label = new Label(compEmployee, SWT.NONE);
		label.setText(this.localized
				.get("NonAvailabilityDialog.label.employee")); //$NON-NLS-1$
		this.employeeViewer = new TextProposalViewer(compEmployee, SWT.BORDER);
		this.employeeViewer.getControl().setLayoutData(
				new GridData(SWT.FILL, SWT.FILL, true, false));

		// Summary
		this.summaryText = new Text(composite, SWT.BORDER);
		this.summaryText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true,
				false));

		// Start date To End date
		Composite compDates = new Composite(composite, SWT.NONE);
		layout = new GridLayout(5, false);
		layout.marginHeight = 0;
		layout.marginWidth = 0;
		layout.verticalSpacing = 0;
		compDates.setLayout(layout);

		this.startDate = new DateTime(compDates, SWT.DATE | SWT.DROP_DOWN
				| SWT.BORDER);
		this.startDate.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false,
				false));
		this.startTime = new Combo(compDates, SWT.BORDER | SWT.DROP_DOWN);
		this.startTime.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false,
				false));

		label = new Label(compDates, SWT.NONE);
		label.setText(this.localized
				.get("NonAvailabilityDialog.label.timeRange.to")); //$NON-NLS-1$
		this.endDate = new DateTime(compDates, SWT.DATE | SWT.DROP_DOWN
				| SWT.BORDER);
		this.endDate.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false,
				false));
		this.endTime = new Combo(compDates, SWT.BORDER | SWT.DROP_DOWN);
		this.endTime.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false,
				false));

		// Visibility
		this.visibileButton = new Button(composite, SWT.CHECK);
		this.visibileButton.setText(this.localized
				.get("NonAvailabilityDialog.label.visible")); //$NON-NLS-1$
		this.visibileButton.setLayoutData(new GridData(SWT.FILL, SWT.FILL,
				true, false));

		// Build the separator line
		Label separator = new Label(comp, SWT.HORIZONTAL | SWT.SEPARATOR);
		separator.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

		return comp;
	}

	/**
	 * Return the employee.
	 * 
	 * @return
	 */
	public Employee getEmployee() {
		return (Employee) this.employee.getValue();
	}

	/**
	 * Returns the end date.
	 * 
	 * @return
	 */
	public Date getEndDate() {
		return (Date) this.end.getValue();
	}

	/**
	 * Returns the start date.
	 * 
	 * @return
	 */
	public Date getStartDate() {
		return (Date) this.start.getValue();
	}

	/**
	 * Returns the summary.
	 * 
	 * @return
	 */
	public String getSummary() {
		return (String) this.summary.getValue();
	}

	/**
	 * Returns the visibility
	 * 
	 * @return
	 */
	public boolean getVisible() {
		return ((Boolean) this.visible.getValue()).booleanValue();
	}

	protected void initValues() {
		this.summary = new WritableValue("", String.class); //$NON-NLS-1$
		this.start = new WritableValue(new Date(), Date.class);
		this.end = new WritableValue(new Date(), Date.class);
		this.employee = new WritableValue(null, Employee.class);
		this.visible = new WritableValue(Boolean.TRUE, Boolean.class);
	}

	/**
	 * Set the employee.
	 * 
	 * @param employee
	 */
	public void setEmployee(Employee employee) {
		this.employee.setValue(employee);
	}

	/**
	 * Sets the end date.
	 * 
	 * @param end
	 */
	public void setEndDate(Date end) {
		if (end == null) {
			this.end.setValue(new Date());
		} else {
			this.end.setValue(end);
		}
	}

	/**
	 * Sets the start date.
	 * 
	 * @param start
	 */
	public void setStartDate(Date start) {
		if (start == null) {
			this.start.setValue(new Date());
		} else {
			this.start.setValue(start);
		}
	}

	/**
	 * Sets the default summary.
	 * 
	 * @param summary
	 */
	public void setSummary(String summary) {
		if (summary == null) {
			this.summary.setValue(""); //$NON-NLS-1$
		} else {
			this.summary.setValue(summary);
		}
	}

	/**
	 * Sets the visibility value
	 * 
	 * @param visible
	 */
	public void setVisible(boolean visible) {
		this.visible.setValue(visible ? Boolean.TRUE : Boolean.FALSE);
	}

}
