/**
 * 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.production;

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

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.databinding.BindingProperties;
import org.eclipse.core.databinding.DataBindingContext;
import org.eclipse.core.databinding.ObservablesManager;
import org.eclipse.core.databinding.beans.BeanProperties;
import org.eclipse.core.databinding.observable.list.IObservableList;
import org.eclipse.core.databinding.observable.masterdetail.MasterDetailObservables;
import org.eclipse.core.databinding.observable.set.IObservableSet;
import org.eclipse.core.databinding.observable.set.WritableSet;
import org.eclipse.core.databinding.observable.value.WritableValue;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.databinding.swt.SWTObservables;
import org.eclipse.jface.databinding.swt.WidgetProperties;
import org.eclipse.jface.databinding.util.JFaceProperties;
import org.eclipse.jface.databinding.viewers.ObservableMapLabelProvider;
import org.eclipse.jface.databinding.viewers.ObservableSetContentProvider;
import org.eclipse.jface.databinding.viewers.ViewerProperties;
import org.eclipse.jface.dialogs.TitleAreaDialog;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;

import com.patrikdufresne.jface.databinding.viewers.PropertyViewerComparator;
import com.patrikdufresne.jface.databinding.viewers.TextProposalViewerUpdater;
import com.patrikdufresne.jface.viewers.TextProposalViewer;
import com.patrikdufresne.managers.ManagedObject;
import com.patrikdufresne.util.DateFormatRegistry;
import com.planimod.core.AbstractCalendarEvent;
import com.planimod.core.PlanimodManagers;
import com.planimod.core.Product;
import com.planimod.core.ProductionEvent;
import com.planimod.core.Shift;
import com.planimod.core.Team;
import com.planimod.core.comparators.ProductionEventComparators;
import com.planimod.ui.databinding.Converters;
import com.planimod.ui.databinding.DateFormatRangeValueProperty;
import com.planimod.ui.databinding.ListObservableValue;
import com.planimod.ui.databinding.manager.PlanimodObservables;
import com.planimod.ui.theme.Resources;

/**
 * Dialog to create or edit a production event.
 * 
 * @author Patrik Dufresne
 * 
 */
public class EditProductionEventDialog extends TitleAreaDialog {
    /**
     * This action remove the selected products.
     * 
     * @author Patrik Dufresne
     */
    public class RemoveProductAction extends Action {
        /**
         * Property name of this action objects (value <code>"objects"</code>). The objects property define the entities
         * to be deleted.
         */
        public static final String OBJECTS = "objects";
        /**
         * The list of object to be removed.
         */
        private List<ManagedObject> objects;

        /**
         * Create a new remove action.
         */
        public RemoveProductAction() {
            super(_("Supprimer"), Resources.getImageDescriptor(Resources.ICON_LIST_REMOVE_16));
            setToolTipText(_("Supprime les produits sélectionnés"));
            setEnabled(canRun());
        }

        /**
         * Check if this action can be run.
         * 
         * @return
         */
        protected boolean canRun() {
            return this.objects != null && this.objects.size() > 0;
        }

        public List<ManagedObject> getObjects() {
            return this.objects;
        }

        @Override
        public void run() {

            if (!canRun()) {
                return;
            }

            events.removeAll(objects);

            // Clear the selection
            productionEventsViewer.setSelection(new StructuredSelection());
        }

        public void setObjects(List<ManagedObject> objects) {
            this.objects = objects;
            setEnabled(canRun());
        }

    }

    private static final String DOT = ".";

    /**
     * The label used to display the event time range.
     */
    private Label dateLabel;

    private DataBindingContext dbc;

    /**
     * Observable list of event.
     */
    WritableSet events;

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

    private ObservablesManager om;

    TableViewer productionEventsViewer;
    /**
     * Widget to select the product.
     */
    private TextProposalViewer productViewer;
    // Create an action to remove the selection
    private Action removeAction;

    private WritableValue shift;

    /**
     * The label used to display the team's name.
     */
    private Label teamLabel;

    /**
     * Create a new dialog.
     * 
     * @param parentShell
     *            the parent shell, or <code>null</code> to create a top-level shell
     */
    public EditProductionEventDialog(Shell parentShell, PlanimodManagers managers) {
        super(parentShell);
        if (managers == null) {
            throw new NullPointerException();
        }
        this.managers = managers;
        this.shift = new WritableValue(null, Shift.class);
        this.events = new WritableSet();
    }

    /**
     * Sets the binding between the values and the widgets.
     */
    protected void bindValues() {

        /*
         * Bind the team label.
         */
        this.dbc.bindValue(WidgetProperties.text().observe(this.teamLabel), BeanProperties.value(Shift.class, Shift.TEAM + DOT + Team.NAME).value(
                BindingProperties.convertedValue(Converters.removeFrontNumber())).observeDetail(this.shift));

        /*
         * Bind date to date label
         */
        this.dbc.bindValue(SWTObservables.observeText(this.dateLabel), new DateFormatRangeValueProperty(DateFormatRegistry.TIME | DateFormatRegistry.LONG)
                .observeDetail(this.shift));

        /*
         * Bind product viewer
         */
        IObservableSet products = PlanimodObservables.listProduct(this.managers);
        // Setup the viewer to display the product list
        ObservableSetContentProvider oscp = new ObservableSetContentProvider(new TextProposalViewerUpdater(this.productViewer));
        this.productViewer.setContentProvider(oscp);
        this.productViewer.setLabelProvider(new ObservableMapLabelProvider(new ProductLabelObservableMap(this.managers, oscp.getKnownElements(), true)));
        this.productViewer.setInput(products);

        /*
         * Bind events viewer
         */
        ObservableSetContentProvider oscp2 = new ObservableSetContentProvider();
        this.productionEventsViewer.setContentProvider(oscp2);
        this.productionEventsViewer.setLabelProvider(new ObservableMapLabelProvider(new ProductionEventLabelObservableMap(this.managers, oscp2
                .getKnownElements(), true)));
        this.productionEventsViewer.setComparator(new PropertyViewerComparator(ProductionEventComparators.byRefIdFamilyName()));
        this.productionEventsViewer.setInput(this.events);

        /*
         * Bind remove action
         */
        IObservableList selections = ViewerProperties.multipleSelection().observe(this.productionEventsViewer);
        this.dbc.bindValue(JFaceProperties
                .value(RemoveProductAction.class, RemoveProductAction.OBJECTS, RemoveProductAction.OBJECTS)
                .observe(this.removeAction), new ListObservableValue(selections, AbstractCalendarEvent.class));

    }

    /**
     * This implementation free the allocated resources.
     */
    @Override
    public boolean close() {
        if (!super.close()) {
            return false;
        }
        if (this.om != null) {
            this.om.dispose();
            this.om = null;
        }
        if (this.dbc != null) {
            this.dbc.dispose();
            this.dbc = null;
        }
        return true;
    }

    /**
     * This implementation sets the binding.
     */
    @Override
    protected Control createContents(Composite parent) {
        Control control = super.createContents(parent);
        /*
         * Bind widgets
         */
        this.dbc = new DataBindingContext();
        this.om = new ObservablesManager();
        this.om.runAndCollect(new Runnable() {
            @Override
            public void run() {
                bindValues();
            }
        });
        this.om.addObservablesFromContext(this.dbc, true, true);
        return control;
    }

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

        // Setup the title area
        getShell().setText(_("Modifier"));
        setTitle(_("Modifier un production"));
        setMessage(_("Ajoutez ou supprimez les produits pour ce quart de travail."));
        setTitleImage(Resources.getImage(Resources.DLG_EDIT_TITLE_IMAGE));

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

        /*
         * Setup the date label
         */
        this.teamLabel = new Label(composite, SWT.NONE);
        this.teamLabel.setLayoutData(new GridData(SWT.CENTER, SWT.FILL, true, false));
        this.teamLabel.setFont(Resources.getWeekTitleFont());

        this.dateLabel = new Label(composite, SWT.NONE);
        this.dateLabel.setLayoutData(new GridData(SWT.CENTER, SWT.FILL, true, false));
        this.dateLabel.setFont(Resources.getWeekTitleFont());

        Label label = new Label(composite, SWT.NONE);
        label.setText(_("Entrez le nom des produits :"));

        // Create the widget to add production event
        Control control = createProductViewer(composite);
        control.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false));

        /*
         * Create the events viewer
         */
        this.productionEventsViewer = new TableViewer(composite, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER | SWT.FULL_SELECTION);
        GridData data = new GridData(SWT.FILL, SWT.FILL, true, true);
        data.heightHint = 230;
        this.productionEventsViewer.getControl().setLayoutData(data);
        // Add listener to delete the selected elements when used press Delete
        // or Backspace.
        this.productionEventsViewer.getTable().addListener(SWT.KeyUp, new Listener() {
            @Override
            public void handleEvent(Event event) {
                if (event.character == SWT.DEL || event.character == SWT.BS) {
                    removeAction.run();
                }
            }
        });

        // Create an action to remove the selection
        ActionContributionItem aci = new ActionContributionItem(this.removeAction = new RemoveProductAction());
        aci.setMode(ActionContributionItem.MODE_FORCE_TEXT);
        aci.fill(composite);

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

        return comp;
    }

    /**
     * Create the widget to select a product and eventually create a production event.
     * 
     * @param compEvent
     *            the composite parent
     * @return the control
     */
    private Control createProductViewer(Composite compEvent) {
        // Create a text proposal viewer
        this.productViewer = new TextProposalViewer(compEvent);

        // Add a listener on the selection to add it to the list
        this.productViewer.addSelectionChangedListener(new ISelectionChangedListener() {
            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                Object element = ((IStructuredSelection) event.getSelection()).getFirstElement();
                if (element instanceof Product) {
                    handleAddEvent((Product) element);
                }
                // Clear the selection
                event.getSelectionProvider().setSelection(new StructuredSelection());
            }
        });

        return this.productViewer.getControl();
    }

    /**
     * Returns the edited product list.
     * 
     * @return the products
     */
    public Collection<ProductionEvent> getProductionEvents() {
        Set<ProductionEvent> eventSet = new HashSet<ProductionEvent>();
        for (Object obj : this.events) {
            ProductionEvent event = (ProductionEvent) obj;
            eventSet.add(event);
        }
        return eventSet;
    }

    /**
     * Returns the current start date.
     * 
     * @return
     */
    public Shift getShift() {
        return (Shift) this.shift.getValue();
    }

    /**
     * Add a production event to the list.
     * 
     * @param element
     */
    protected void handleAddEvent(Product product) {
        // Add the product to the list
        final ProductionEvent event = new ProductionEvent();
        event.setProduct(product);
        this.events.add(event);

        Display.getDefault().asyncExec(new Runnable() {
            @Override
            public void run() {
                if (productionEventsViewer != null) {
                    productionEventsViewer.setSelection(new StructuredSelection(event));
                    productionEventsViewer.reveal(event);
                }
            }
        });

    }

    /**
     * This implementation return true.
     */
    @Override
    protected boolean isResizable() {
        return true;
    }

    /**
     * Sets the product list to be edited.
     * 
     * @param products
     *            the events
     */
    public void setProductionEvents(Collection<ProductionEvent> products) {
        this.events.clear();
        if (products != null) {
            this.events.addAll(products);
        }
    }

    /**
     * Sets the start date.
     * 
     * @param start
     */
    public void setShift(Shift shift) {
        this.shift.setValue(shift);
    }

}
