/*******************************************************************************
 * Copyright (c) 2014, 2020 Frank Mosebach.
 *
 * 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:
 *     Frank Mosebach <mosebach@patronas.de> - regression test for bug 447942
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.viewport;

import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.event.IStructuralChangeEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.StructuralRefreshEvent;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public final class ViewportLayerDisposalTest {

    private Shell shell;

    @Before
    public void init() {
        this.shell = new Shell(Display.getDefault());
    }

    @After
    public void dispose() {
        this.shell.dispose();
    }

    /**
     * Tests that an {@link IStructuralChangeEvent} will be safely handled by a
     * table's {@link ViewportLayer} after the table has been disposed (see: bug
     * <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=447942">Bug
     * 447942</a>).
     *
     * @throws InterruptedException
     *             on multi-threading issues
     */
    @Test
    public void testPostEventToDisposedLayer() throws InterruptedException {
        final DataLayer dataLayer = new TestDataLayer();
        final ViewportLayer viewportLayer = new ViewportLayer(dataLayer);
        final NatTable table = new NatTable(this.shell, viewportLayer);

        // Setting the table's size will cause the viewport layer to connect
        // itself to the table's scrollbars.
        table.setSize(500, 500);

        // Change a value in the data layer on a background thread.
        final Thread backgroundUpdater = new Thread(new Runnable() {
            @Override
            public void run() {
                // Calling "setDataValue" on a background thread will cause the
                // actual update to be executed asnchronously on the ui thread.
                dataLayer.setDataValue(4, 49, "VALUE");
            }
        }, "LayerDisposalTest.backgroundUpdater");

        backgroundUpdater.start();
        backgroundUpdater.join();

        // Disposing the table will also dispose all of its layers.
        table.dispose();

        // Process all pending ui tasks. This is supposed to disptach an event
        // to the (disposed) viewport layer.
        while (this.shell.getDisplay().readAndDispatch()) {
        }

        // Test that the background thread has successfully updated the table's
        // data layer.
        Assert.assertEquals("The table's data layer should have been updated.", "VALUE", dataLayer.getDataValue(4, 49));
    }

    private static final class TestDataLayer extends DataLayer {

        private static IDataProvider DATA_PROVIDER = new IDataProvider() {

            private final String[][] data = new String[100][10];

            @Override
            public Object getDataValue(final int columnIndex, final int rowIndex) {
                return this.data[rowIndex][columnIndex];
            }

            @Override
            public void setDataValue(final int columnIndex, final int rowIndex, final Object newValue) {
                this.data[rowIndex][columnIndex] = (newValue == null) ? null : newValue.toString();
            }

            @Override
            public int getRowCount() {
                return 100;
            }

            @Override
            public int getColumnCount() {
                return 10;
            }
        };

        private TestDataLayer() {
            super(DATA_PROVIDER);
        }

        @Override
        public void setDataValue(final int columnIndex, final int rowIndex, final Object newValue) {
            if (Display.getCurrent() == null) {
                // We are on a background thread: Schedule an update task to be
                // executed on the ui thread.

                Display.getDefault().asyncExec(new Runnable() {
                    @Override
                    public void run() {
                        setDataValue(columnIndex, rowIndex, newValue);
                    }
                });
            } else {
                // We are on the ui thread: Update the respective data value and
                // fire a refresh event.

                super.setDataValue(columnIndex, rowIndex, newValue);

                fireLayerEvent(new StructuralRefreshEvent(this));
            }
        }
    }

}
