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

import java.beans.PropertyChangeListener;
import java.io.Serializable;
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.BlinkLayer;
import org.eclipse.nebula.widgets.nattable.blink.BlinkingCellResolver;
import org.eclipse.nebula.widgets.nattable.blink.IBlinkingCellResolver;
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.data.IColumnPropertyAccessor;
import org.eclipse.nebula.widgets.nattable.data.IRowIdAccessor;
import org.eclipse.nebula.widgets.nattable.data.ListDataProvider;
import org.eclipse.nebula.widgets.nattable.data.ReflectiveColumnPropertyAccessor;
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.runner.StandaloneNatExampleRunner;
import org.eclipse.nebula.widgets.nattable.extension.glazedlists.GlazedListsEventLayer;
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.DefaultColumnHeaderDataLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.DefaultGridLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.DefaultRowHeaderDataLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.GridLayer;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
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.util.GUIHelper;
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.GlazedLists;
import ca.odell.glazedlists.ObservableElementList;

public class BlinkingGridExample extends AbstractNatExample {

    public static void main(String[] args) {
        StandaloneNatExampleRunner.run(800, 400, new BlinkingGridExample());
    }

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

    private ListDataProvider<BlinkingRowDataFixture> bodyDataProvider;

    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();

        // Body
        LinkedList<BlinkingRowDataFixture> rowData = new LinkedList<>();
        EventList<BlinkingRowDataFixture> eventList = GlazedLists.eventList(rowData);
        ObservableElementList<BlinkingRowDataFixture> observableElementList =
                new ObservableElementList<>(
                        eventList,
                        GlazedLists.beanConnector(BlinkingRowDataFixture.class));
        IColumnPropertyAccessor<BlinkingRowDataFixture> columnPropertyAccessor =
                new ReflectiveColumnPropertyAccessor<>(propertyNames);
        this.bodyDataProvider =
                new ListDataProvider<>(observableElementList, columnPropertyAccessor);

        final DataLayer bodyLayer = new DataLayer(this.bodyDataProvider);

        GlazedListsEventLayer<BlinkingRowDataFixture> glazedListsEventLayer =
                new GlazedListsEventLayer<>(bodyLayer, observableElementList);
        BlinkLayer<BlinkingRowDataFixture> blinkingLayer =
                new BlinkLayer<>(
                        glazedListsEventLayer,
                        this.bodyDataProvider,
                        new IRowIdAccessor<BlinkingRowDataFixture>() {
                            @Override
                            public Serializable getRowId(BlinkingRowDataFixture rowObject) {
                                return rowObject.getSecurity_description();
                            }
                        },
                        columnPropertyAccessor,
                        configRegistry);
        registerBlinkingConfigCells(configRegistry);
        insertRowData(glazedListsEventLayer, this.bodyDataProvider);

        // Column header
        final DefaultColumnHeaderDataProvider defaultColumnHeaderDataProvider =
                new DefaultColumnHeaderDataProvider(propertyNames, propertyToLabelMap);

        // Row header
        final DefaultRowHeaderDataProvider rowHeaderDataProvider =
                new DefaultRowHeaderDataProvider(this.bodyDataProvider);

        // Corner
        final DefaultCornerDataProvider cornerDataProvider =
                new DefaultCornerDataProvider(defaultColumnHeaderDataProvider, rowHeaderDataProvider);

        // Grid
        GridLayer gridLayer = new DefaultGridLayer(
                blinkingLayer,
                new DefaultColumnHeaderDataLayer(defaultColumnHeaderDataProvider),
                new DefaultRowHeaderDataLayer(rowHeaderDataProvider),
                new DataLayer(cornerDataProvider));

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

        natTable.setConfigRegistry(configRegistry);
        natTable.addConfiguration(new DefaultNatTableStyleConfiguration());

        natTable.configure();

        return natTable;
    }

    /**
     * Start threads to fire data updates (at intervals)
     */
    @Override
    public void onStart() {
        Display.getDefault().asyncExec(new Runnable() {
            @Override
            public void run() {
                BlinkingGridExample.this.scheduledThreadPool = Executors.newScheduledThreadPool(1);

                // Fire updates to indexes 1,3,5
                BlinkingGridExample.this.scheduledThreadPool.scheduleAtFixedRate(
                        new DataPumper(BlinkingGridExample.this.bodyDataProvider, 1, 3, 5),
                        500L,
                        5000L,
                        TimeUnit.MILLISECONDS);

                // while they are still blinking update index 1
                BlinkingGridExample.this.scheduledThreadPool.scheduleAtFixedRate(
                        new DataPumper(BlinkingGridExample.this.bodyDataProvider, 1),
                        750L,
                        5000L,
                        TimeUnit.MILLISECONDS);

                // While the above are still blinking update indexes 2,8
                BlinkingGridExample.this.scheduledThreadPool.scheduleAtFixedRate(
                        new DataPumper(BlinkingGridExample.this.bodyDataProvider, 2, 8),
                        1000L,
                        5000L,
                        TimeUnit.MILLISECONDS);
            }
        });
    }

    @Override
    public void onStop() {
        this.scheduledThreadPool.shutdown();
    }

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

        // Bg color styles to be used for blinking cells
        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);
    }

    /**
     * The blinking resolver decides how the cell should blink i.e what styles
     * should be applied depending on the update. This one returns a green color
     * label when the value goee up, a red one otherwise.
     */
    private IBlinkingCellResolver getBlinkResolver() {
        return new BlinkingCellResolver() {
            private 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 void insertRowData(
            PropertyChangeListener changeListener,
            ListDataProvider<BlinkingRowDataFixture> dataProvider) {
        List<BlinkingRowDataFixture> listFixture = BlinkingRowDataFixture.getList(changeListener);
        for (BlinkingRowDataFixture rowObject : listFixture) {
            dataProvider.getList().add(rowObject);
        }
    }

    final Random random = new Random();

    /**
     * Util class to fire periodic updates
     */
    class DataPumper implements Runnable {
        ListDataProvider<BlinkingRowDataFixture> dataProvider;
        private final int[] rowIndexes;

        DataPumper(ListDataProvider<BlinkingRowDataFixture> dataProvider, final int... rowIndexes) {
            this.dataProvider = dataProvider;
            this.rowIndexes = rowIndexes;
        }

        @Override
        public void run() {
            Display.getDefault().asyncExec(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < DataPumper.this.rowIndexes.length; i++) {
                        double nextPrice = BlinkingGridExample.this.random.nextInt(1000);
                        BlinkingRowDataFixture rowObject =
                                DataPumper.this.dataProvider.getRowObject(DataPumper.this.rowIndexes[i]);
                        rowObject.setAsk_price(nextPrice);
                    }
                }
            });
        }
    }

}
