/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors 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:
 *     Original authors and others - initial API and implementation
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.examples.examples;

import static org.eclipse.nebula.widgets.nattable.util.ObjectUtils.isNotNull;

import java.beans.PropertyChangeListener;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.blink.BlinkConfigAttributes;
import org.eclipse.nebula.widgets.nattable.blink.BlinkingCellResolver;
import org.eclipse.nebula.widgets.nattable.blink.IBlinkingCellResolver;
import org.eclipse.nebula.widgets.nattable.columnChooser.command.DisplayColumnChooserCommandHandler;
import org.eclipse.nebula.widgets.nattable.config.CellConfigAttributes;
import org.eclipse.nebula.widgets.nattable.config.ConfigRegistry;
import org.eclipse.nebula.widgets.nattable.config.DefaultNatTableStyleConfiguration;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.config.IConfiguration;
import org.eclipse.nebula.widgets.nattable.data.IRowDataProvider;
import org.eclipse.nebula.widgets.nattable.data.IRowIdAccessor;
import org.eclipse.nebula.widgets.nattable.data.ListDataProvider;
import org.eclipse.nebula.widgets.nattable.dataset.fixture.data.BlinkingRowDataFixture;
import org.eclipse.nebula.widgets.nattable.dataset.fixture.data.RowDataListFixture;
import org.eclipse.nebula.widgets.nattable.examples.AbstractNatExample;
import org.eclipse.nebula.widgets.nattable.examples.PersistentNatExampleWrapper;
import org.eclipse.nebula.widgets.nattable.examples.examples._110_Editing.EditableGridExample;
import org.eclipse.nebula.widgets.nattable.examples.examples._131_Filtering.FilterRowGridExample;
import org.eclipse.nebula.widgets.nattable.examples.fixtures.FullFeaturedBodyLayerStack;
import org.eclipse.nebula.widgets.nattable.examples.fixtures.FullFeaturedColumnHeaderLayerStack;
import org.eclipse.nebula.widgets.nattable.examples.runner.StandaloneNatExampleRunner;
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.data.DefaultSummaryRowHeaderDataProvider;
import org.eclipse.nebula.widgets.nattable.grid.layer.CornerLayer;
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.group.ColumnGroupModel;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.cell.ColumnOverrideLabelAccumulator;
import org.eclipse.nebula.widgets.nattable.sort.config.SingleClickSortConfiguration;
import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.style.Style;
import org.eclipse.nebula.widgets.nattable.summaryrow.DefaultSummaryRowConfiguration;
import org.eclipse.nebula.widgets.nattable.summaryrow.ISummaryProvider;
import org.eclipse.nebula.widgets.nattable.summaryrow.SummaryRowConfigAttributes;
import org.eclipse.nebula.widgets.nattable.summaryrow.SummaryRowLayer;
import org.eclipse.nebula.widgets.nattable.summaryrow.SummationSummaryProvider;
import org.eclipse.nebula.widgets.nattable.ui.menu.HeaderMenuConfiguration;
import org.eclipse.nebula.widgets.nattable.ui.menu.PopupMenuBuilder;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.nebula.widgets.nattable.widget.WaitDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BusyIndicator;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;

import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.FilterList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.ObservableElementList;
import ca.odell.glazedlists.SortedList;

public class _900_Everything_but_the_kitchen_sink extends AbstractNatExample {

    public static void main(String[] args) {
        StandaloneNatExampleRunner.run(1000, 800,
                new PersistentNatExampleWrapper(
                        new _900_Everything_but_the_kitchen_sink()));
    }

    @Override
    public String getDescription() {
        return "The example with everything. Well, not quite everything, but a lot.";
    }

    private static final String BLINK_UP_CONFIG_LABEL = "blinkUpConfigLabel";
    private static final String BLINK_DOWN_CONFIG_LABEL = "blinkDownConfigLabel";

    private EventList<BlinkingRowDataFixture> baseEventList;
    private PropertyChangeListener propertyChangeListener;
    private ListDataProvider<BlinkingRowDataFixture> bodyDataProvider;
    private NatTable natTable;
    private ScheduledExecutorService scheduledThreadPool;

    @Override
    public Control createExampleControl(Composite parent) {
        final String[] propertyNames = RowDataListFixture.getPropertyNames();
        final Map<String, String> propertyToLabelMap = RowDataListFixture.getPropertyToLabelMap();

        ConfigRegistry configRegistry = new ConfigRegistry();
        ColumnGroupModel columnGroupModel = new ColumnGroupModel();

        // Body

        LinkedList<BlinkingRowDataFixture> rowData = new LinkedList<>();
        this.baseEventList = GlazedLists.threadSafeList(GlazedLists.eventList(rowData));
        ObservableElementList<BlinkingRowDataFixture> observableElementList = new ObservableElementList<>(
                this.baseEventList,
                GlazedLists.beanConnector(BlinkingRowDataFixture.class));
        FilterList<BlinkingRowDataFixture> filterList = new FilterList<>(observableElementList);
        SortedList<BlinkingRowDataFixture> sortedList = new SortedList<>(filterList, null);

        FullFeaturedBodyLayerStack<BlinkingRowDataFixture> bodyLayer =
                new FullFeaturedBodyLayerStack<>(
                        sortedList,
                        new IRowIdAccessor<BlinkingRowDataFixture>() {

                            @Override
                            public Serializable getRowId(BlinkingRowDataFixture rowObject) {
                                return rowObject.getSecurity_description();
                            }
                        },
                        propertyNames, configRegistry, columnGroupModel);

        this.bodyDataProvider = bodyLayer.getBodyDataProvider();
        this.propertyChangeListener = bodyLayer.getGlazedListEventsLayer();

        // blinking
        registerBlinkingConfigCells(configRegistry);

        // Column header
        FullFeaturedColumnHeaderLayerStack<BlinkingRowDataFixture> columnHeaderLayer =
                new FullFeaturedColumnHeaderLayerStack<>(
                        sortedList,
                        filterList,
                        propertyNames,
                        propertyToLabelMap,
                        bodyLayer,
                        bodyLayer.getSelectionLayer(),
                        columnGroupModel,
                        configRegistry);

        // column groups
        setUpColumnGroups(columnHeaderLayer);

        // Row header
        final DefaultRowHeaderDataProvider rowHeaderDataProvider =
                new DefaultSummaryRowHeaderDataProvider(this.bodyDataProvider);
        DefaultRowHeaderDataLayer rowHeaderDataLayer =
                new DefaultRowHeaderDataLayer(rowHeaderDataProvider);
        rowHeaderDataLayer.setDefaultColumnWidth(50);
        ILayer rowHeaderLayer = new RowHeaderLayer(rowHeaderDataLayer, bodyLayer, bodyLayer.getSelectionLayer());

        // Corner
        final DefaultCornerDataProvider cornerDataProvider =
                new DefaultCornerDataProvider(
                        columnHeaderLayer.getColumnHeaderDataProvider(),
                        rowHeaderDataProvider);
        DataLayer cornerDataLayer = new DataLayer(cornerDataProvider);
        ILayer cornerLayer = new CornerLayer(cornerDataLayer, rowHeaderLayer, columnHeaderLayer);

        // Grid
        GridLayer gridLayer = new GridLayer(bodyLayer, columnHeaderLayer, rowHeaderLayer, cornerLayer);

        this.natTable = new NatTable(parent, gridLayer, false);

        this.natTable.setConfigRegistry(configRegistry);

        this.natTable.addConfiguration(new DefaultNatTableStyleConfiguration());
        // Popup menu
        this.natTable.addConfiguration(new HeaderMenuConfiguration(this.natTable) {
            @Override
            protected PopupMenuBuilder createColumnHeaderMenu(NatTable natTable) {
                return super.createColumnHeaderMenu(natTable)
                        .withColumnChooserMenuItem();
            }
        });

        this.natTable.addConfiguration(new SingleClickSortConfiguration());

        // Editing
        ColumnOverrideLabelAccumulator columnLabelAccumulator =
                new ColumnOverrideLabelAccumulator(bodyLayer.getBodyDataLayer());
        bodyLayer.getBodyDataLayer().setConfigLabelAccumulator(columnLabelAccumulator);
        this.natTable.addConfiguration(
                EditableGridExample.editableGridConfiguration(columnLabelAccumulator, this.bodyDataProvider));
        this.natTable.addConfiguration(new FilterRowGridExample.FilterRowCustomConfiguration());

        // Column chooser
        DisplayColumnChooserCommandHandler columnChooserCommandHandler = new DisplayColumnChooserCommandHandler(
                bodyLayer.getSelectionLayer(),
                bodyLayer.getColumnHideShowLayer(),
                columnHeaderLayer.getColumnHeaderLayer(),
                columnHeaderLayer.getColumnHeaderDataLayer(),
                columnHeaderLayer.getColumnGroupHeaderLayer(), columnGroupModel);
        bodyLayer.registerCommandHandler(columnChooserCommandHandler);

        // Summary row configuration
        this.natTable.addConfiguration(new MySummaryRow<>(this.bodyDataProvider));
        this.natTable.configure();

        return this.natTable;
    }

    private void setUpColumnGroups(FullFeaturedColumnHeaderLayerStack<BlinkingRowDataFixture> headerLayer) {
        headerLayer.getColumnGroupHeaderLayer().addColumnsIndexesToGroup("TestGroup", 1, 2);
        headerLayer.getColumnGroupHeaderLayer().addColumnsIndexesToGroup("TestGroup1", 5, 6, 7);
        headerLayer.getColumnGroupHeaderLayer().setGroupUnbreakable(5);
        headerLayer.getColumnGroupHeaderLayer().setGroupAsCollapsed(5);
    }

    @Override
    public void onStart() {
        Display.getDefault().asyncExec(new Runnable() {
            @Override
            public void run() {
                _900_Everything_but_the_kitchen_sink.this.scheduledThreadPool = Executors.newScheduledThreadPool(1);
                System.out.println("Starting data load.");
                _900_Everything_but_the_kitchen_sink.this.scheduledThreadPool.schedule(new DataLoader(
                        _900_Everything_but_the_kitchen_sink.this.propertyChangeListener, _900_Everything_but_the_kitchen_sink.this.baseEventList), 100L,
                        TimeUnit.MILLISECONDS);
                _900_Everything_but_the_kitchen_sink.this.scheduledThreadPool.scheduleAtFixedRate(new DataUpdater(
                        _900_Everything_but_the_kitchen_sink.this.bodyDataProvider), 100L, 5000L, TimeUnit.MILLISECONDS);
            }
        });
    }

    @Override
    public void onStop() {
        if (isNotNull(this.scheduledThreadPool)) {
            this.scheduledThreadPool.shutdownNow();
        }
    }

    private void registerBlinkingConfigCells(ConfigRegistry configRegistry) {
        configRegistry.registerConfigAttribute(
                BlinkConfigAttributes.BLINK_RESOLVER,
                getBlinkResolver(),
                DisplayMode.NORMAL);

        // Styles
        Style cellStyle = new Style();
        cellStyle.setAttributeValue(
                CellStyleAttributes.BACKGROUND_COLOR,
                GUIHelper.COLOR_GREEN);
        configRegistry.registerConfigAttribute(
                CellConfigAttributes.CELL_STYLE,
                cellStyle,
                DisplayMode.NORMAL,
                BLINK_UP_CONFIG_LABEL);

        cellStyle = new Style();
        cellStyle.setAttributeValue(
                CellStyleAttributes.BACKGROUND_COLOR,
                GUIHelper.COLOR_RED);
        configRegistry.registerConfigAttribute(
                CellConfigAttributes.CELL_STYLE,
                cellStyle,
                DisplayMode.NORMAL,
                BLINK_DOWN_CONFIG_LABEL);
    }

    private IBlinkingCellResolver getBlinkResolver() {
        return new BlinkingCellResolver() {
            private final String[] configLabels = new String[1];

            @Override
            public String[] resolve(Object oldValue, Object newValue) {
                double old = ((Double) oldValue).doubleValue();
                double latest = ((Double) newValue).doubleValue();
                this.configLabels[0] = (latest > old
                        ? BLINK_UP_CONFIG_LABEL
                        : BLINK_DOWN_CONFIG_LABEL);
                return this.configLabels;
            }
        };
    }

    private final Random random = new Random();
    private static final int DATASET_SIZE = 52000;

    /**
     * Initial data load. Loads data in batches.
     *
     * Note: It is a significantly more efficient to do the load in batches i.e
     * using {@link List#addAll(java.util.Collection)} instead of adding objects
     * individually.
     *
     * @See https://sourceforge.net/projects/nattable/forums/forum/744994/topic/
     *      3410065
     */
    class DataLoader implements Runnable {
        private final PropertyChangeListener changeListener;
        private final EventList<BlinkingRowDataFixture> list;
        private final String waitMsg = "Loading data. Please wait... ";
        private WaitDialog dialog;

        public DataLoader(PropertyChangeListener changeListener, EventList<BlinkingRowDataFixture> baseEventList) {
            this.changeListener = changeListener;
            this.list = baseEventList;
        }

        @Override
        public void run() {
            try {
                // Display wait dialog
                Display.getDefault().asyncExec(new Runnable() {
                    @Override
                    public void run() {
                        final Image icon = new Image(Display.getDefault(),
                                getClass().getResourceAsStream("waiting.gif"));
                        DataLoader.this.dialog = new WaitDialog(_900_Everything_but_the_kitchen_sink.this.natTable.getShell(), SWT.NONE,
                                DataLoader.this.waitMsg, icon);
                        DataLoader.this.dialog.setBlockOnOpen(true);
                        DataLoader.this.dialog.open();
                    }
                });

                // Load data
                while (this.list.size() < DATASET_SIZE) {
                    // Add to buffer
                    List<BlinkingRowDataFixture> buffer = new ArrayList<>();
                    for (int i = 0; i < 100; i++) {
                        buffer.addAll(BlinkingRowDataFixture.getList(this.changeListener));
                    }
                    // Load as a batch
                    this.list.addAll(buffer);
                    // Update wait dialog
                    Display.getDefault().syncExec(new Runnable() {
                        @Override
                        public void run() {
                            DataLoader.this.dialog.setMsg(DataLoader.this.waitMsg + DataLoader.this.list.size());
                        }
                    });
                }

                // Close wait dialog
                Display.getDefault().asyncExec(new Runnable() {
                    @Override
                    public void run() {
                        DataLoader.this.dialog.close();
                    }
                });
            } catch (Exception e) {
                e.printStackTrace(System.err);
            }
        }
    }

    public Runnable runWithBusyIndicator(final Runnable runnable) {
        return new Runnable() {
            @Override
            public void run() {
                Display.getDefault().asyncExec(new Runnable() {
                    @Override
                    public void run() {
                        BusyIndicator.showWhile(Display.getDefault(), runnable);
                    }
                });
            }
        };
    }

    /**
     * Pumps data updates to drive blinking
     */
    class DataUpdater implements Runnable {
        ListDataProvider<BlinkingRowDataFixture> dataProvider;
        int counter = 0;

        DataUpdater(ListDataProvider<BlinkingRowDataFixture> dataProvider) {
            this.dataProvider = dataProvider;
        }

        @Override
        public void run() {
            Display.getDefault().asyncExec(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 5; i++) {
                        int index = _900_Everything_but_the_kitchen_sink.this.random.nextInt(13);
                        int nextAsk = _900_Everything_but_the_kitchen_sink.this.random.nextInt(1000);

                        if (DataUpdater.this.dataProvider.getRowCount() > index) {
                            BlinkingRowDataFixture rowObject = DataUpdater.this.dataProvider
                                    .getRowObject(index);
                            // System.out.println("Ask:
                            // "+rowObject.getAsk_price()+" --> "+nextAsk);
                            rowObject.setAsk_price(nextAsk);
                            rowObject.setBid_price(-1d * nextAsk);
                        }
                    }
                }
            });
        }
    }

    class MySummaryRow<T> extends DefaultSummaryRowConfiguration implements IConfiguration {

        private IRowDataProvider<T> dataProvider;

        public MySummaryRow(IRowDataProvider<T> dataProvider) {
            this.dataProvider = dataProvider;
        }

        @Override
        public void addSummaryProviderConfig(IConfigRegistry configRegistry) {
            // Add summaries to ask price column
            configRegistry.registerConfigAttribute(
                    SummaryRowConfigAttributes.SUMMARY_PROVIDER,
                    new SummationSummaryProvider(this.dataProvider),
                    DisplayMode.NORMAL,
                    SummaryRowLayer.DEFAULT_SUMMARY_COLUMN_CONFIG_LABEL_PREFIX
                            + RowDataListFixture.getColumnIndexOfProperty(RowDataListFixture.ASK_PRICE_PROP_NAME));

            // No Summary by default
            configRegistry.registerConfigAttribute(
                    SummaryRowConfigAttributes.SUMMARY_PROVIDER,
                    ISummaryProvider.NONE,
                    DisplayMode.NORMAL,
                    SummaryRowLayer.DEFAULT_SUMMARY_ROW_CONFIG_LABEL);
        }
    }

}
