/**
 * Copyright (C) 2019 Patrik Dufresne Service Logiciel <info@patrikdufresne.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package com.planimod.ui.views.nonavailabilities;

import static com.planimod.ui.Localized._;

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.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;

    /**
     * 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(_("Veuillez sélectionner un employé."));
                }
                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(_("Veuillez entrer la date de début et la date de fin."));
                }
                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(_("Entrez un nom d'employé, un motif ainsi que la plage horaire de son absence.")));

        // 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.style == NEW ? _("Ajouter") : _("Modifier"));
        setTitle(this.style == NEW ? _("Ajouter une absence") : _("Modifier une absence"));
        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(_("Employé :"));
        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(_("au"));
        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(_("Afficher l'absence lors de l'impression des rapports"));
        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);
        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("");
        } else {
            this.summary.setValue(summary);
        }
    }

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

}
