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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
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.config.ConfigRegistry;
import org.eclipse.nebula.widgets.nattable.config.DefaultNatTableStyleConfiguration;
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.data.ReflectiveColumnPropertyAccessor;
import org.eclipse.nebula.widgets.nattable.dataset.person.Address;
import org.eclipse.nebula.widgets.nattable.dataset.person.DataModelConstants;
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.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.reorder.ColumnReorderLayer;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.sort.ISortModel;
import org.eclipse.nebula.widgets.nattable.sort.SortDirectionEnum;
import org.eclipse.nebula.widgets.nattable.sort.SortHeaderLayer;
import org.eclipse.nebula.widgets.nattable.sort.command.SortCommandHandler;
import org.eclipse.nebula.widgets.nattable.sort.config.SingleClickSortConfiguration;
import org.eclipse.nebula.widgets.nattable.viewport.ViewportLayer;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;

/**
 * Simple example showing how to add the {@link SortHeaderLayer} to the layer
 * composition of a grid.
 */
public class _509_SortHeaderLayerExample extends AbstractNatExample {

    public static void main(String[] args) throws Exception {
        StandaloneNatExampleRunner.run(new _509_SortHeaderLayerExample());
    }

    @Override
    public String getDescription() {
        return "This example shows the usage of the SortHeaderLayer within a grid that uses"
                + " a custom ISortModel.";
    }

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

        // 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");

        // build the body layer stack
        // Usually you would create a new layer stack by extending
        // AbstractIndexLayerTransform and
        // setting the ViewportLayer as underlying layer. But in this case using
        // the ViewportLayer
        // directly as body layer is also working.
        List<PersonWithAddress> data = PersonService.getPersonsWithAddress(10);

        IColumnPropertyAccessor<PersonWithAddress> accessor =
                new ReflectiveColumnPropertyAccessor<>(propertyNames);
        IDataProvider bodyDataProvider =
                new ListDataProvider<>(data, accessor);
        DataLayer bodyDataLayer = new DataLayer(bodyDataProvider);
        ColumnReorderLayer columnReorderLayer = new ColumnReorderLayer(bodyDataLayer);
        ColumnHideShowLayer columnHideShowLayer = new ColumnHideShowLayer(columnReorderLayer);
        SelectionLayer selectionLayer = new SelectionLayer(columnHideShowLayer);
        ViewportLayer viewportLayer = new ViewportLayer(selectionLayer);

        // build the column header layer
        IDataProvider columnHeaderDataProvider =
                new DefaultColumnHeaderDataProvider(propertyNames, propertyToLabelMap);
        DataLayer columnHeaderDataLayer =
                new DefaultColumnHeaderDataLayer(columnHeaderDataProvider);
        ILayer columnHeaderLayer =
                new ColumnHeaderLayer(columnHeaderDataLayer, viewportLayer, selectionLayer);

        ConfigRegistry configRegistry = new ConfigRegistry();
        SortHeaderLayer<PersonWithAddress> sortHeaderLayer =
                new SortHeaderLayer<>(columnHeaderLayer, new PersonWithAddressSortModel(data));

        // build the row header layer
        IDataProvider rowHeaderDataProvider =
                new DefaultRowHeaderDataProvider(bodyDataProvider);
        DataLayer rowHeaderDataLayer =
                new DefaultRowHeaderDataLayer(rowHeaderDataProvider);
        ILayer rowHeaderLayer =
                new RowHeaderLayer(rowHeaderDataLayer, viewportLayer, selectionLayer);

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

        // build the grid layer
        GridLayer gridLayer =
                new GridLayer(viewportLayer, sortHeaderLayer, rowHeaderLayer, cornerLayer);

        // turn the auto configuration off as we want to add custom
        // configurations
        NatTable natTable = new NatTable(parent, gridLayer, false);

        natTable.setConfigRegistry(configRegistry);

        // as the autoconfiguration of the NatTable is turned off, we have to
        // add the DefaultNatTableStyleConfiguration manually
        natTable.addConfiguration(new DefaultNatTableStyleConfiguration());
        // enable sorting on single click on the column header
        natTable.addConfiguration(new SingleClickSortConfiguration());

        natTable.configure();

        return natTable;
    }

    /**
     * A simple implementation for {@link ISortModel} that can be used to sort
     * {@link PersonWithAddress} objects. It uses it's own {@link Comparator}
     * and doesn't support multiple column sorting and it won't be possible to
     * remove the sorting. Of course this can be implemented to work like the
     * default behaviour of the NatTable.
     */
    class PersonWithAddressSortModel implements ISortModel {

        /**
         * Array that contains the sort direction for every column. Needed to
         * access the current sort state of a column.
         */
        protected SortDirectionEnum[] sortDirections;

        /**
         * Array that contains the sorted flags for every column. Needed to
         * access the current sort state of a column.
         */
        protected boolean[] sorted;

        /**
         * As this implementation only supports single column sorting, this
         * property contains the the column index of the column that is
         * currently used for sorting. Initial value = -1 for no sort column
         */
        protected int currentSortColumn = -1;

        /**
         * As this implementation only supports single column sorting, this
         * property contains the current sort direction of the column that is
         * currently used for sorting.
         */
        protected SortDirectionEnum currentSortDirection = SortDirectionEnum.ASC;

        /**
         * Data list that is sorted
         */
        private List<PersonWithAddress> persons;

        /**
         * Creates a new {@link PersonWithAddressSortModel} for the list of
         * objects.
         *
         * @param persons
         *            the list of objects that should be sorted
         */
        public PersonWithAddressSortModel(List<PersonWithAddress> persons) {
            this.persons = persons;

            this.sortDirections = new SortDirectionEnum[DataModelConstants.PERSONWITHADDRESS_NUMBER_OF_COLUMNS];
            Arrays.fill(this.sortDirections, SortDirectionEnum.NONE);

            this.sorted = new boolean[DataModelConstants.PERSONWITHADDRESS_NUMBER_OF_COLUMNS];
            Arrays.fill(this.sorted, false);

            // call initial sorting
            sort(0, SortDirectionEnum.ASC, false);
        }

        /**
         * As this is a simple implementation of an {@link ISortModel} and we
         * don't support multiple column sorting, this list returns either a
         * list with one entry for the current sort column or an empty list.
         */
        @Override
        public List<Integer> getSortedColumnIndexes() {
            List<Integer> indexes = new ArrayList<>();
            if (this.currentSortColumn > -1) {
                indexes.add(Integer.valueOf(this.currentSortColumn));
            }
            return indexes;
        }

        /**
         * @return TRUE if the column with the given index is sorted at the
         *         moment.
         */
        @Override
        public boolean isColumnIndexSorted(int columnIndex) {
            return this.sorted[columnIndex];
        }

        /**
         * @return the direction in which the column with the given index is
         *         currently sorted
         */
        @Override
        public SortDirectionEnum getSortDirection(int columnIndex) {
            return this.sortDirections[columnIndex];
        }

        /**
         * @return 0 as we don't support multiple column sorting.
         */
        @Override
        public int getSortOrder(int columnIndex) {
            return 0;
        }

        /**
         * Remove all sorting
         */
        @Override
        public void clear() {
            Arrays.fill(this.sortDirections, SortDirectionEnum.NONE);
            Arrays.fill(this.sorted, false);
            this.currentSortColumn = -1;
        }

        /**
         * This method is called by the {@link SortCommandHandler} in response
         * to a sort command. It is responsible for sorting the requested
         * column.
         */
        @Override
        public void sort(int columnIndex, SortDirectionEnum sortDirection, boolean accumulate) {
            if (!isColumnIndexSorted(columnIndex)) {
                clear();
            }

            if (sortDirection.equals(SortDirectionEnum.NONE)) {
                // we don't support NONE as user action
                sortDirection = SortDirectionEnum.ASC;
            }

            Collections.sort(this.persons, new PersonWithAddressComparator(columnIndex, sortDirection));
            this.sortDirections[columnIndex] = sortDirection;
            this.sorted[columnIndex] = sortDirection.equals(SortDirectionEnum.NONE) ? false : true;

            this.currentSortColumn = columnIndex;
            this.currentSortDirection = sortDirection;
        }

        @SuppressWarnings("rawtypes")
        @Override
        public List<Comparator> getComparatorsForColumnIndex(int columnIndex) {
            return null;
        }

        @Override
        public Comparator<?> getColumnComparator(int columnIndex) {
            return null;
        }

        /**
         * Custom comparator for {@link PersonWithAddress} objects
         */
        class PersonWithAddressComparator implements Comparator<PersonWithAddress> {

            int colIdx = 0;
            SortDirectionEnum sortDirection;

            public PersonWithAddressComparator(int columnIndex, SortDirectionEnum sortDirection) {
                this.colIdx = columnIndex;
                this.sortDirection = sortDirection;
            }

            @SuppressWarnings({ "rawtypes", "unchecked" })
            @Override
            public int compare(PersonWithAddress pwa1, PersonWithAddress pwa2) {
                Comparable compareObject1 = null;
                Comparable compareObject2 = null;
                Address address1 = pwa1.getAddress();
                Address address2 = pwa2.getAddress();

                switch (this.colIdx) {
                    case DataModelConstants.FIRSTNAME_COLUMN_POSITION:
                        compareObject1 = pwa1.getFirstName();
                        compareObject2 = pwa2.getFirstName();
                        break;
                    case DataModelConstants.LASTNAME_COLUMN_POSITION:
                        compareObject1 = pwa1.getLastName();
                        compareObject2 = pwa2.getLastName();
                        break;
                    case DataModelConstants.GENDER_COLUMN_POSITION:
                        compareObject1 = pwa1.getGender().ordinal();
                        compareObject2 = pwa2.getGender().ordinal();
                        break;
                    case DataModelConstants.MARRIED_COLUMN_POSITION:
                        compareObject1 = pwa1.isMarried();
                        compareObject2 = pwa2.isMarried();
                        break;
                    case DataModelConstants.BIRTHDAY_COLUMN_POSITION:
                        compareObject1 = pwa1.getBirthday();
                        compareObject2 = pwa2.getBirthday();
                        break;
                    case DataModelConstants.STREET_COLUMN_POSITION:
                        compareObject1 = address1.getStreet();
                        compareObject2 = address2.getStreet();
                        break;
                    case DataModelConstants.HOUSENUMBER_COLUMN_POSITION:
                        compareObject1 = address1.getHousenumber();
                        compareObject2 = address2.getHousenumber();
                        break;
                    case DataModelConstants.POSTALCODE_COLUMN_POSITION:
                        compareObject1 = address1.getPostalCode();
                        compareObject2 = address2.getPostalCode();
                        break;
                    case DataModelConstants.CITY_COLUMN_POSITION:
                        compareObject1 = address1.getCity();
                        compareObject2 = address2.getCity();
                        break;
                }

                int result = 0;

                // make null safe compare
                if (compareObject1 == null) {
                    if (compareObject2 != null) {
                        result = -1;
                    } else {
                        result = 0;
                    }
                } else {
                    if (compareObject2 != null) {
                        result = compareObject1.compareTo(compareObject2);
                    } else {
                        result = 1;
                    }
                }

                // negate compare result if sort direction is descending
                if (this.sortDirection.equals(SortDirectionEnum.DESC)) {
                    result = result * -1;
                }

                return result;
            }

        }
    }
}
