/*******************************************************************************
 * Copyright (c) 2013, 2020 Dirk Fauth and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *    Dirk Fauth <dirk.fauth@googlemail.com> - initial API and implementation
 *******************************************************************************/
package org.eclipse.nebula.widgets.nattable.examples._500_Layers._504_Viewport;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.command.AbstractLayerCommandHandler;
import org.eclipse.nebula.widgets.nattable.config.AbstractUiBindingConfiguration;
import org.eclipse.nebula.widgets.nattable.config.DefaultNatTableStyleConfiguration;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.data.ExtendedReflectiveColumnPropertyAccessor;
import org.eclipse.nebula.widgets.nattable.data.IColumnPropertyAccessor;
import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
import org.eclipse.nebula.widgets.nattable.data.ListDataProvider;
import org.eclipse.nebula.widgets.nattable.dataset.person.PersonService;
import org.eclipse.nebula.widgets.nattable.dataset.person.PersonWithAddress;
import org.eclipse.nebula.widgets.nattable.examples.AbstractNatExample;
import org.eclipse.nebula.widgets.nattable.examples.runner.StandaloneNatExampleRunner;
import org.eclipse.nebula.widgets.nattable.grid.data.DefaultColumnHeaderDataProvider;
import org.eclipse.nebula.widgets.nattable.grid.data.DefaultCornerDataProvider;
import org.eclipse.nebula.widgets.nattable.grid.data.DefaultRowHeaderDataProvider;
import org.eclipse.nebula.widgets.nattable.grid.layer.ColumnHeaderLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.CornerLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.DefaultColumnHeaderDataLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.DefaultRowHeaderDataLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.GridLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.RowHeaderLayer;
import org.eclipse.nebula.widgets.nattable.hideshow.ColumnHideShowLayer;
import org.eclipse.nebula.widgets.nattable.layer.AbstractLayer;
import org.eclipse.nebula.widgets.nattable.layer.AbstractLayerTransform;
import org.eclipse.nebula.widgets.nattable.layer.CompositeLayer;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.LayerUtil;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.painter.IOverlayPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.ICellPainter;
import org.eclipse.nebula.widgets.nattable.painter.layer.CellLayerPainter;
import org.eclipse.nebula.widgets.nattable.print.command.MultiTurnViewportOffCommandHandler;
import org.eclipse.nebula.widgets.nattable.print.command.MultiTurnViewportOnCommandHandler;
import org.eclipse.nebula.widgets.nattable.reorder.ColumnReorderLayer;
import org.eclipse.nebula.widgets.nattable.reorder.action.ColumnReorderDragMode;
import org.eclipse.nebula.widgets.nattable.reorder.command.ColumnReorderCommand;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayerPainter;
import org.eclipse.nebula.widgets.nattable.ui.action.AggregateDragMode;
import org.eclipse.nebula.widgets.nattable.ui.action.CellDragMode;
import org.eclipse.nebula.widgets.nattable.ui.binding.UiBindingRegistry;
import org.eclipse.nebula.widgets.nattable.ui.matcher.MouseEventMatcher;
import org.eclipse.nebula.widgets.nattable.ui.menu.HeaderMenuConfiguration;
import org.eclipse.nebula.widgets.nattable.util.ClientAreaAdapter;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.nebula.widgets.nattable.viewport.SliderScroller;
import org.eclipse.nebula.widgets.nattable.viewport.ViewportLayer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
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.Slider;

/**
 * Example showing how to implement NatTable that contains two horizontal split
 * viewports in a grid.
 */
public class _5044_HorizontalSplitViewportGridExample extends AbstractNatExample {

    public static final int SPLIT_COLUMN_INDEX = 5;

    public static void main(String[] args) throws Exception {
        StandaloneNatExampleRunner.run(600, 400, new _5044_HorizontalSplitViewportGridExample());
    }

    @Override
    public String getDescription() {
        return "This example shows a NatTable that contains two separately scrollable "
                + "horzizontal split viewports in a grid.";
    }

    @Override
    public Control createExampleControl(Composite parent) {
        // property names of the Person class
        String[] propertyNames = { "firstName", "lastName", "gender", "married", "birthday",
                "address.street", "address.housenumber", "address.postalCode", "address.city" };

        // mapping from property to label, needed for column header labels
        Map<String, String> propertyToLabelMap = new HashMap<>();
        propertyToLabelMap.put("firstName", "Firstname");
        propertyToLabelMap.put("lastName", "Lastname");
        propertyToLabelMap.put("gender", "Gender");
        propertyToLabelMap.put("married", "Married");
        propertyToLabelMap.put("birthday", "Birthday");
        propertyToLabelMap.put("address.street", "Street");
        propertyToLabelMap.put("address.housenumber", "Housenumber");
        propertyToLabelMap.put("address.postalCode", "Postal Code");
        propertyToLabelMap.put("address.city", "City");

        IColumnPropertyAccessor<PersonWithAddress> columnPropertyAccessor =
                new ExtendedReflectiveColumnPropertyAccessor<>(propertyNames);

        final BodyLayerStack<PersonWithAddress> bodyLayer = new BodyLayerStack<>(
                PersonService.getPersonsWithAddress(50), columnPropertyAccessor);

        // build the row header layer
        IDataProvider rowHeaderDataProvider = new DefaultRowHeaderDataProvider(
                bodyLayer.getBodyDataProvider());
        DataLayer rowHeaderDataLayer = new DefaultRowHeaderDataLayer(
                rowHeaderDataProvider);
        final ILayer rowHeaderLayer = new RowHeaderLayer(rowHeaderDataLayer,
                bodyLayer, bodyLayer.getSelectionLayer());

        // build the column header layer
        IDataProvider columnHeaderDataProvider =
                new DefaultColumnHeaderDataProvider(propertyNames, propertyToLabelMap);
        final DataLayer columnHeaderDataLayer =
                new DefaultColumnHeaderDataLayer(columnHeaderDataProvider);
        final AbstractLayer columnHeaderLayer =
                new ColumnHeaderLayer(columnHeaderDataLayer, bodyLayer, bodyLayer.getSelectionLayer());

        // Use this special layer painter that supports rendering of split
        // viewports although the ColumnHeaderLayer is not split. Here is some
        // custom calculation included that might not work correctly in case
        // there are column groups or other spanning involved.
        columnHeaderLayer.setLayerPainter(new CellLayerPainter() {
            @Override
            protected boolean isClipLeft(int position) {
                // check position-1 because of the row header column count
                // as the body is a composite layer, the default transformation
                // for the grid is not working correctly
                int index = LayerUtil.convertColumnPosition(
                        columnHeaderLayer, position - 1, columnHeaderDataLayer);
                return (index > SPLIT_COLUMN_INDEX);
            }

            @Override
            protected void paintCell(ILayerCell cell, GC gc, IConfigRegistry configRegistry) {
                ILayer layer = cell.getLayer();
                int columnPosition = cell.getColumnPosition();
                int rowPosition = cell.getRowPosition();
                ICellPainter cellPainter = layer.getCellPainter(
                        columnPosition, rowPosition, cell, configRegistry);
                Rectangle adjustedCellBounds = layer
                        .getLayerPainter()
                        .adjustCellBounds(columnPosition, rowPosition, cell.getBounds());
                if (cellPainter != null) {
                    Rectangle originalClipping = gc.getClipping();

                    int startX = getStartXOfColumnPosition(columnPosition);
                    int startY = getStartYOfRowPosition(rowPosition);

                    int endX = getStartXOfColumnPosition(cell.getOriginColumnPosition() + cell.getColumnSpan());
                    int endY = getStartYOfRowPosition(cell.getOriginRowPosition() + cell.getRowSpan());

                    // correct position of first column in right region
                    // find the last visible column in left region
                    int viewportBorderX = bodyLayer.getViewportLayerLeft().getClientAreaWidth()
                            + rowHeaderLayer.getWidth();
                    if (isClipLeft(columnPosition) && startX < viewportBorderX) {
                        startX = viewportBorderX;
                    }
                    if (!isClipLeft(columnPosition - 1)
                            && startX > viewportBorderX) {
                        startX = viewportBorderX;
                    }
                    if (isClipLeft(cell.getOriginColumnPosition() + cell.getColumnSpan())
                            && endX < viewportBorderX) {
                        endX = viewportBorderX;
                    }

                    Rectangle cellClipBounds = originalClipping.intersection(
                            new Rectangle(startX, startY, endX - startX, endY - startY));
                    gc.setClipping(cellClipBounds.intersection(adjustedCellBounds));

                    cellPainter.paintCell(cell, gc, adjustedCellBounds, configRegistry);

                    gc.setClipping(originalClipping);
                }
            }
        });

        // build the corner layer
        IDataProvider cornerDataProvider =
                new DefaultCornerDataProvider(columnHeaderDataProvider, rowHeaderDataProvider);
        DataLayer cornerDataLayer =
                new DataLayer(cornerDataProvider);
        final ILayer cornerLayer =
                new CornerLayer(cornerDataLayer, rowHeaderLayer, columnHeaderLayer);

        // build the grid layer
        GridLayer gridLayer =
                new GridLayer(bodyLayer, columnHeaderLayer, rowHeaderLayer, cornerLayer);

        // in order to make printing and exporting work correctly you need to
        // register the following command handlers
        gridLayer.registerCommandHandler(
                new MultiTurnViewportOnCommandHandler(
                        bodyLayer.getViewportLayerLeft(),
                        bodyLayer.getViewportLayerRight()));
        gridLayer.registerCommandHandler(
                new MultiTurnViewportOffCommandHandler(
                        bodyLayer.getViewportLayerLeft(),
                        bodyLayer.getViewportLayerRight()));

        // Wrap NatTable in composite so we can slap on the external horizontal
        // sliders
        Composite composite = new Composite(parent, SWT.NONE);
        GridLayout gridLayout = new GridLayout(1, false);
        gridLayout.marginHeight = 0;
        gridLayout.marginWidth = 0;
        gridLayout.horizontalSpacing = 0;
        gridLayout.verticalSpacing = 0;
        composite.setLayout(gridLayout);

        NatTable natTable = new NatTable(composite, gridLayer, false);
        GridData gridData = new GridData();
        gridData.horizontalAlignment = GridData.FILL;
        gridData.verticalAlignment = GridData.FILL;
        gridData.grabExcessHorizontalSpace = true;
        gridData.grabExcessVerticalSpace = true;
        natTable.setLayoutData(gridData);

        createSplitSliders(composite, rowHeaderLayer,
                bodyLayer.getViewportLayerLeft(),
                bodyLayer.getViewportLayerRight());

        // add an IOverlayPainter to ensure the right border of the left
        // viewport always
        // this is necessary because the left border of layer stacks is not
        // rendered by default
        natTable.addOverlayPainter(new IOverlayPainter() {

            @Override
            public void paintOverlay(GC gc, ILayer layer) {
                Color beforeColor = gc.getForeground();
                gc.setForeground(GUIHelper.COLOR_GRAY);
                int viewportBorderX = bodyLayer.getViewportLayerLeft().getWidth()
                        + rowHeaderLayer.getWidth() - 1;
                gc.drawLine(viewportBorderX, 0, viewportBorderX, layer.getHeight() - 1);
                gc.setForeground(beforeColor);
            }
        });

        natTable.addConfiguration(new DefaultNatTableStyleConfiguration());
        natTable.addConfiguration(new HeaderMenuConfiguration(natTable));
        natTable.configure();

        return composite;
    }

    private void createSplitSliders(
            Composite natTableParent, final ILayer rowHeaderLayer,
            final ViewportLayer left, final ViewportLayer right) {
        Composite sliderComposite = new Composite(natTableParent, SWT.NONE);
        GridData gridData = new GridData();
        gridData.horizontalAlignment = GridData.FILL;
        gridData.grabExcessHorizontalSpace = true;
        gridData.grabExcessVerticalSpace = false;
        gridData.heightHint = 17;
        sliderComposite.setLayoutData(gridData);

        GridLayout gridLayout = new GridLayout(2, false);
        gridLayout.marginHeight = 0;
        gridLayout.marginWidth = 0;
        gridLayout.horizontalSpacing = 0;
        gridLayout.verticalSpacing = 0;
        sliderComposite.setLayout(gridLayout);

        // Slider Left
        // Need a composite here to set preferred size because Slider can't be
        // subclassed.
        Composite sliderLeftComposite = new Composite(sliderComposite, SWT.NONE) {
            @Override
            public Point computeSize(int wHint, int hHint, boolean changed) {
                int width = ((ClientAreaAdapter) left.getClientAreaProvider()).getWidth();
                width += rowHeaderLayer.getWidth();
                return new Point(width, 17);
            }
        };
        sliderLeftComposite.setLayout(new FillLayout());
        gridData = new GridData();
        gridData.horizontalAlignment = GridData.BEGINNING;
        gridData.verticalAlignment = GridData.BEGINNING;
        sliderLeftComposite.setLayoutData(gridData);

        Slider sliderLeft = new Slider(sliderLeftComposite, SWT.HORIZONTAL);
        gridData = new GridData();
        gridData.horizontalAlignment = GridData.FILL;
        gridData.verticalAlignment = GridData.FILL;
        sliderLeft.setLayoutData(gridData);

        left.setHorizontalScroller(new SliderScroller(sliderLeft));

        // Slider Right
        Slider sliderRight = new Slider(sliderComposite, SWT.HORIZONTAL);
        gridData = new GridData();
        gridData.horizontalAlignment = GridData.FILL;
        gridData.verticalAlignment = GridData.BEGINNING;
        gridData.grabExcessHorizontalSpace = true;
        gridData.grabExcessVerticalSpace = true;
        sliderRight.setLayoutData(gridData);

        right.setHorizontalScroller(new SliderScroller(sliderRight));
    }

    /**
     * The body layer stack that supports column hide/show, column reordering,
     * selection and split viewports.
     */
    class BodyLayerStack<T> extends AbstractLayerTransform {

        private final IDataProvider bodyDataProvider;
        private final ColumnHideShowLayer columnHideShowLayer;
        private final SelectionLayer selectionLayer;
        private final ViewportLayer viewportLayerLeft;
        private final ViewportLayer viewportLayerRight;

        public BodyLayerStack(List<T> values, IColumnPropertyAccessor<T> columnPropertyAccessor) {
            this.bodyDataProvider = new ListDataProvider<>(values, columnPropertyAccessor);
            DataLayer bodyDataLayer = new DataLayer(getBodyDataProvider());

            // use our custom reorder drag mode configuration instead of the
            // default to suppress the ability to move a column from one
            // viewport to the other
            ColumnReorderLayer columnReorderLayer = new ColumnReorderLayer(bodyDataLayer, false);
            columnReorderLayer.addConfiguration(new AbstractUiBindingConfiguration() {

                @Override
                public void configureUiBindings(UiBindingRegistry uiBindingRegistry) {
                    uiBindingRegistry.registerMouseDragMode(
                            MouseEventMatcher.columnHeaderLeftClick(SWT.NONE),
                            new AggregateDragMode(
                                    new CellDragMode(),
                                    new SplitViewportColumnReorderDragMode()));
                }
            });

            this.columnHideShowLayer = new ColumnHideShowLayer(columnReorderLayer);
            this.selectionLayer = new SelectionLayer(this.columnHideShowLayer);

            // use a cell layer painter that is configured for left clipping
            // this ensures that the rendering works correctly for split
            // viewports
            this.selectionLayer.setLayerPainter(new SelectionLayerPainter(true, false));

            // create a ViewportLayer for the left part of the table and
            // configure it to only contain the first 5 columns
            this.viewportLayerLeft = new ViewportLayer(this.selectionLayer) {
                @Override
                public int getMaxColumnPosition() {
                    return getNumberOfLeftColumns();
                }
            };

            // create a ViewportLayer for the right part of the table and
            // configure it to only contain the last 4 columns
            this.viewportLayerRight = new ViewportLayer(this.selectionLayer) {
                @Override
                public int getMinColumnPosition() {
                    return getNumberOfLeftColumns();
                }
            };
            // as the min column position is calculated dynamically we need to
            // set the minimum origin manually
            int newMinOriginX = this.selectionLayer.getStartXOfColumnPosition(getNumberOfLeftColumns());
            this.viewportLayerRight.setMinimumOriginX(newMinOriginX);

            // create a CompositeLayer that contains both ViewportLayers
            CompositeLayer compositeLayer = new CompositeLayer(2, 1);
            compositeLayer.setChildLayer("REGION_A", getViewportLayerLeft(), 0, 0);
            compositeLayer.setChildLayer("REGION_B", getViewportLayerRight(), 1, 0);

            // set the width of the left viewport to only showing 2 columns at
            // the same time
            int leftWidth = bodyDataLayer.getStartXOfColumnPosition(2);

            // as the CompositeLayer is setting a IClientAreaProvider for the
            // composition
            // we need to set a special ClientAreaAdapter after the creation of
            // the CompositeLayer to support split viewports
            ClientAreaAdapter leftClientAreaAdapter =
                    new ClientAreaAdapter(getViewportLayerLeft().getClientAreaProvider());
            leftClientAreaAdapter.setWidth(leftWidth);
            getViewportLayerLeft().setClientAreaProvider(leftClientAreaAdapter);

            // register configuration to avoid reordering of columns between the
            // split viewports
            registerCommandHandler(
                    new SplitViewportColumnReorderCommandHandler(getViewportLayerLeft()));

            setUnderlyingLayer(compositeLayer);
        }

        /**
         * To support hide/show correctly the min/max column positions in the
         * split viewports need to be calculated regarding the hide state.
         *
         * @return The number of visible columns in the left viewport.
         */
        private int getNumberOfLeftColumns() {
            int fixedColumns = SPLIT_COLUMN_INDEX;
            for (int i = 0; i < (SPLIT_COLUMN_INDEX); i++) {
                if (this.columnHideShowLayer.isColumnIndexHidden(i)) {
                    fixedColumns--;
                }
            }
            return fixedColumns;
        }

        public IDataProvider getBodyDataProvider() {
            return this.bodyDataProvider;
        }

        public SelectionLayer getSelectionLayer() {
            return this.selectionLayer;
        }

        public ViewportLayer getViewportLayerLeft() {
            return this.viewportLayerLeft;
        }

        public ViewportLayer getViewportLayerRight() {
            return this.viewportLayerRight;
        }
    }

    /**
     * ILayerCommandHandler for ColumnReorderCommands that ensures that no
     * reordering can be performed programmatically or via dialog, that would
     * reorder columns between the split viewports.
     */
    class SplitViewportColumnReorderCommandHandler extends
            AbstractLayerCommandHandler<ColumnReorderCommand> {

        private ViewportLayer viewportLeft;

        public SplitViewportColumnReorderCommandHandler(ViewportLayer viewportLeft) {
            this.viewportLeft = viewportLeft;
        }

        @Override
        protected boolean doCommand(ColumnReorderCommand command) {
            if ((command.getFromColumnPosition() < this.viewportLeft.getColumnCount()) != (command.getToColumnPosition() < this.viewportLeft.getColumnCount())) {
                // Bail out if trying to reorder from region A to B or vice
                // versa
                return true;
            }
            return false;
        }

        @Override
        public Class<ColumnReorderCommand> getCommandClass() {
            return ColumnReorderCommand.class;
        }

    }

    /**
     * Reorder drag mode that avoids reordering between split viewports via
     * dragging.
     */
    class SplitViewportColumnReorderDragMode extends ColumnReorderDragMode {

        @Override
        protected boolean isValidTargetColumnPosition(ILayer natLayer,
                int dragFromGridColumnPosition, int dragToGridColumnPosition) {

            if (((NatTable) natLayer).getCursor() == null) {
                int fromColumnIndex = natLayer.getColumnIndexByPosition(dragFromGridColumnPosition);
                int toColumnIndex = natLayer.getColumnIndexByPosition(dragToGridColumnPosition);

                // ensure that dragging over split viewport borders is not
                // allowed
                if ((fromColumnIndex < SPLIT_COLUMN_INDEX && toColumnIndex < SPLIT_COLUMN_INDEX)
                        || (fromColumnIndex >= (SPLIT_COLUMN_INDEX) && toColumnIndex >= (SPLIT_COLUMN_INDEX))) {

                    return super.isValidTargetColumnPosition(natLayer,
                            dragFromGridColumnPosition,
                            dragToGridColumnPosition);
                }
            }

            return false;
        }
    }
}
