/*******************************************************************************
 * Copyright (c) 2014, 2017 Ericsson and others
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Bernd Hufmann - Initial API and implementation
 *******************************************************************************/

package org.eclipse.tracecompass.tmf.core.tests.signal;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.tracecompass.tmf.core.component.TmfComponent;
import org.eclipse.tracecompass.tmf.core.signal.TmfEndSynchSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
import org.eclipse.tracecompass.tmf.core.signal.TmfStartSynchSignal;
import org.junit.After;
import org.junit.Test;

/**
 * Test suite for {@link TmfSignalManager}
 *
 * @author Bernd Hufmann
 */
public class TmfSignalManagerTest {

    private final @NonNull TestSignalSender signalSender = new TestSignalSender();

    /**
     * After-test cleanup
     */
    @After
    public void tearDown() {
        signalSender.dispose();
    }

    // ------------------------------------------------------------------------
    // Test cases
    // ------------------------------------------------------------------------

    /**
     * Test send and receive including synch signals.
     */
    @Test
    public void testSendReceive() {
        final int NB_HANDLERS = 10;
        TestSignalHandler[] signalReceivers = new TestSignalHandler[NB_HANDLERS];
        for (int i = 0; i < NB_HANDLERS; i++) {
            signalReceivers[i] = new TestSignalHandler();
        }
        final TestSignal1 firstSignal = new TestSignal1(signalSender);
        final TestSignal2 secondSignal = new TestSignal2(signalSender);

        final Class<?>[] expectedOrder = new Class[] {
                TmfStartSynchSignal.class, TestSignal1.class, TmfEndSynchSignal.class,
                TmfStartSynchSignal.class, TestSignal2.class, TmfEndSynchSignal.class };

        try {
            signalSender.sendSignal(firstSignal);
            signalSender.sendSignal(secondSignal);

            for (int i = 0; i < NB_HANDLERS; i++) {
                assertEquals(expectedOrder.length, signalReceivers[i].receivedSignals.size());

                for (int k = 0; k < expectedOrder.length; k++) {
                    assertEquals(signalReceivers[i].receivedSignals.get(k).getClass(), expectedOrder[k]);
                }

                for (int k = 0; k < expectedOrder.length; k += 3) {
                    // Verify signal IDs
                    int startSyncId = signalReceivers[i].receivedSignals.get(k).getReference();
                    int signalId = signalReceivers[i].receivedSignals.get(k + 1).getReference();
                    int endSyncId = signalReceivers[i].receivedSignals.get(k + 2).getReference();

                    assertEquals(startSyncId, signalId);
                    assertEquals(startSyncId, endSyncId);
                }
            }
        } finally {
            // Make sure that handlers are disposed in any case (success or not success)
            for (int i = 0; i < NB_HANDLERS; i++) {
                signalReceivers[i].dispose();
            }
        }
    }

    /**
     * Test inbound blacklisting
     */
    @Test
    public void testInboundBlacklisting() {
        testInboundBlacklisting(Arrays.asList(),
                Arrays.asList(
                        TmfStartSynchSignal.class, TestSignal1.class, TmfEndSynchSignal.class,
                        TmfStartSynchSignal.class, TestSignal2.class, TmfEndSynchSignal.class));
        testInboundBlacklisting(Arrays.asList(TestSignal1.class),
                Arrays.asList(
                        TmfStartSynchSignal.class, TmfEndSynchSignal.class,
                        TmfStartSynchSignal.class, TestSignal2.class, TmfEndSynchSignal.class));
        testInboundBlacklisting(Arrays.asList(TestSignal2.class),
                Arrays.asList(
                        TmfStartSynchSignal.class, TestSignal1.class, TmfEndSynchSignal.class,
                        TmfStartSynchSignal.class, TmfEndSynchSignal.class));
        testInboundBlacklisting(Arrays.asList(TestSignal1.class, TestSignal2.class),
                Arrays.asList(
                        TmfStartSynchSignal.class, TmfEndSynchSignal.class,
                        TmfStartSynchSignal.class, TmfEndSynchSignal.class));
        testInboundBlacklisting(Arrays.asList(TmfSignal.class),
                Arrays.asList());
    }

    private void testInboundBlacklisting(List<@NonNull Class<? extends TmfSignal>> ignoredSignals, List<Class<? extends TmfSignal>> expectedSignals) {
        TestSignalHandler receiver = new TestSignalHandler();

        /* Set the inbound blacklisting */
        for (@NonNull Class<? extends TmfSignal> ignoredSignal : ignoredSignals) {
            TmfSignalManager.addIgnoredInboundSignal(receiver, ignoredSignal);
        }

        /* Send signals */
        signalSender.sendSignal(new TestSignal1(signalSender));
        signalSender.sendSignal(new TestSignal2(signalSender));

        /* Check received signals */
        assertEquals(expectedSignals.size(), receiver.receivedSignals.size());
        for (int i = 0; i < expectedSignals.size(); i++) {
            assertEquals(expectedSignals.get(i), receiver.receivedSignals.get(i).getClass());
        }

        receiver.dispose();
    }

    /**
     * Test outbound blacklisting
     */
    @Test
    public void testOutboundBlacklisting() {
        testOutboundBlacklisting(Arrays.asList(),
                Arrays.asList(
                        TmfStartSynchSignal.class, TestSignal1.class, TmfEndSynchSignal.class,
                        TmfStartSynchSignal.class, TestSignal2.class, TmfEndSynchSignal.class));
        testOutboundBlacklisting(Arrays.asList(TestSignal1.class),
                Arrays.asList(
                        TmfStartSynchSignal.class, TestSignal2.class, TmfEndSynchSignal.class));
        testOutboundBlacklisting(Arrays.asList(TestSignal2.class),
                Arrays.asList(
                        TmfStartSynchSignal.class, TestSignal1.class, TmfEndSynchSignal.class));
        testOutboundBlacklisting(Arrays.asList(TestSignal1.class, TestSignal2.class),
                Arrays.asList());
        testOutboundBlacklisting(Arrays.asList(TmfSignal.class),
                Arrays.asList());
    }

    private void testOutboundBlacklisting(List<@NonNull Class<? extends TmfSignal>> blockedSignals, List<Class<? extends TmfSignal>> expectedSignals) {
        TestSignalHandler receiver = new TestSignalHandler();

        /* Set the outbound blacklisting */
        for (@NonNull Class<? extends TmfSignal> blockedSignal : blockedSignals) {
            TmfSignalManager.addIgnoredOutboundSignal(signalSender, blockedSignal);
        }

        /* Send signals */
        signalSender.sendSignal(new TestSignal1(signalSender));
        signalSender.sendSignal(new TestSignal2(signalSender));

        /* Check received signals */
        assertEquals(expectedSignals.size(), receiver.receivedSignals.size());
        for (int i = 0; i < expectedSignals.size(); i++) {
            assertEquals(expectedSignals.get(i), receiver.receivedSignals.get(i).getClass());
        }

        receiver.dispose();
        TmfSignalManager.clearIgnoredOutboundSignalList(signalSender);
    }

    /**
     * Test nesting signals. Verify that they are handled in the same thread.
     */
    @Test
    public void testNestedSignals() {
        TestSignalHandlerNested signalResender = new TestSignalHandlerNested();
        TestSignalHandler signalReceiver = new TestSignalHandler();

        final TestSignal1 mainSignal = new TestSignal1(signalSender);

        final Class<?>[] expectedOrder = new Class[] {
                TmfStartSynchSignal.class,
                    TmfStartSynchSignal.class,
                    TestSignal2.class,
                    TmfEndSynchSignal.class,
                    TmfStartSynchSignal.class,
                        TmfStartSynchSignal.class,
                        TestSignal4.class,
                        TmfEndSynchSignal.class,
                    TestSignal3.class,
                    TmfEndSynchSignal.class,
                TestSignal1.class,
                TmfEndSynchSignal.class
        };

        /*
         *  Index of signals in array signalReceiver.receivedSignals which have
         *  to have the same signal ID.
         */

        final int[] sameSigNoIndex = new int[] {
                0, 10, 11, 1, 2, 3, 4, 8, 9, 5, 6, 7
        };

        try {
            signalSender.sendSignal(mainSignal);

            assertEquals(expectedOrder.length, signalReceiver.receivedSignals.size());

            for (int i = 0; i < expectedOrder.length; i++) {
                assertEquals(signalReceiver.receivedSignals.get(i).getClass(), expectedOrder[i]);
            }

            for (int i = 0; i < sameSigNoIndex.length; i+=3) {
                // Verify signal IDs
                int startSyncId = signalReceiver.receivedSignals.get(sameSigNoIndex[i]).getReference();
                int signalId = signalReceiver.receivedSignals.get(sameSigNoIndex[i + 1]).getReference();
                int endSyncId = signalReceiver.receivedSignals.get(sameSigNoIndex[i + 2]).getReference();
                assertEquals(startSyncId, signalId);
                assertEquals(startSyncId, endSyncId);
            }
        } finally {
            // Make sure that handlers are disposed in any case (success or not success)
            signalResender.dispose();
            signalReceiver.dispose();
        }
    }

    /**
     * Test concurrent signals. Verify that they are handled one after each
     * other.
     */
    @Test
    public void testConcurrentSignals() {

        TestSignalHandlerNested signalResender = new TestSignalHandlerNested();
        TestSignalHandler signalReceiver = new TestSignalHandler(false, null);

        /*
         * Test of synchronization of signal manager.
         *
         * The order of received signals is either set of signals triggered by
         * thread1 before set of signals triggered by thread2 or the other way
         * around.
         *
         * If both received sets were interleaved then the synchronization of
         * the signal manager would be not working.
         */

        final Class<?>[] expectedOrder1 = new Class[] {
                TestSignal2.class, TestSignal4.class, TestSignal3.class, TestSignal1.class, // signals triggered by thread 1
                TestSignal4.class // signal triggered by thread2
        };

        final Class<?>[] expectedOrder2 = new Class[] {
                TestSignal4.class, // signal triggered by thread2
                TestSignal2.class, TestSignal4.class, TestSignal3.class, TestSignal1.class // signals triggered by thread 1
        };

        /*
         *  Run it multiple times so that both expected order are triggered
         */
        try {
            for (int k = 0; k < 10; k++) {
                // Latch to ensure that both threads are started
                final CountDownLatch startLatch = new CountDownLatch(2);
                // Latch to ensure that signals are send roughly at the same time
                final CountDownLatch sendLatch = new CountDownLatch(1);
                // Latch to ensure that both treads are finished
                final CountDownLatch endLatch = new CountDownLatch(2);

                signalReceiver.receivedSignals.clear();

                Thread senderThread1 = new Thread() {
                    @Override
                    public void run() {
                        startLatch.countDown();
                        try {
                            sendLatch.await();
                        } catch (InterruptedException e) {
                        }
                        signalSender.sendSignal(new TestSignal1(signalSender));
                        endLatch.countDown();
                    }
                };

                Thread senderThread2 = new Thread() {
                    @Override
                    public void run() {
                        startLatch.countDown();
                        try {
                            sendLatch.await();
                        } catch (InterruptedException e) {
                        }
                        signalSender.sendSignal(new TestSignal4(signalSender));
                        endLatch.countDown();
                    }
                };

                senderThread1.start();
                senderThread2.start();
                try {
                    startLatch.await();
                } catch (InterruptedException e) {
                }
                sendLatch.countDown();

                try {
                    endLatch.await();
                } catch (InterruptedException e) {
                }

                assertEquals(expectedOrder1.length, signalReceiver.receivedSignals.size());
                boolean pass = true;
                for (int i = 0; i < expectedOrder1.length; i++) {
                    if (!signalReceiver.receivedSignals.get(i).getClass().equals(expectedOrder1[i])) {
                        pass = false;
                        break;
                    }
                }

                if (!pass) {
                    for (int i = 0; i < expectedOrder2.length; i++) {
                        if (!signalReceiver.receivedSignals.get(i).getClass().equals(expectedOrder2[i])) {
                            fail("Concurrent signal test failure!");
                        }
                    }
                }
            }
        } finally {
            // Make sure that handlers are disposed in any case (success or not success)
            signalResender.dispose();
            signalReceiver.dispose();
        }
    }

    /**
     * Test broadcastAsync()
     */
    @Test
    public void testBroadcastAsync() {
        TestSignalHandlerNested signalResender = new TestSignalHandlerNested(false);

        final int NB_HANDLERS = 10;
        final CountDownLatch latch = new CountDownLatch(NB_HANDLERS);
        TestSignalHandler[] signalReceivers = new TestSignalHandler[NB_HANDLERS];
        for (int i = 0; i < NB_HANDLERS; i++) {
            signalReceivers[i] = new TestSignalHandler(false, latch);
        }

        final Class<?>[] expectedOrder = new Class[] {
                TestSignal1.class, TestSignal2.class, TestSignal3.class, TestSignal4.class
        };

        try {
            final TestSignal1 mainSignal = new TestSignal1(signalSender);
            signalSender.sendSignal(mainSignal);

            try {
                latch.await();
            } catch (InterruptedException e) {
            }

            for (int i = 0; i < NB_HANDLERS; i++) {
                assertEquals(expectedOrder.length, signalReceivers[i].receivedSignals.size());
                for (int k = 0; k < expectedOrder.length; k++) {
                    assertEquals(signalReceivers[i].receivedSignals.get(k).getClass(), expectedOrder[k]);
                }
            }
        } finally {
            // Make sure that handlers are disposed in any case (success or not success)
            for (int i = 0; i < NB_HANDLERS; i++) {
                signalReceivers[i].dispose();
            }
            signalResender.dispose();
        }
    }

    // ------------------------------------------------------------------------
    // Helper classes
    // ------------------------------------------------------------------------

    /**
     * Signal sender
     */
    private class TestSignalSender extends TmfComponent {

        TestSignalSender() {
            super("TestSignalSender");
        }

        /**
         * Send a signal
         *
         * @param signal
         *            main signal to send
         */
        public void sendSignal(TmfSignal signal) {
            broadcast(signal);
        }
    }

    /**
     * Signal handler implementation for testing nested signals.
     * Needs to be public so TmfSignalManager can see it.
     */
    public class TestSignalHandlerNested extends AbstractBaseSignalHandler {

        private boolean sync;

        /**
         * Constructor
         */
        private TestSignalHandlerNested() {
            this(true);
        }

        /**
         * Constructor
         *
         * @param sync
         *            log sync signals
         *
         */
        private TestSignalHandlerNested(boolean sync) {
            super("TestSignalHandlerNested", false);
            this.sync = sync;
            TmfSignalManager.deregister(this);
            TmfSignalManager.registerVIP(this);
        }

        /**
         * Receive a signal of type TestSignal1.
         *
         * @param signal
         *            Signal received
         */
        @TmfSignalHandler
        public void receiveSignal1(final TestSignal1 signal) {
            if (sync) {
                broadcast(new TestSignal2(signal.getSource()));
                broadcast(new TestSignal3(signal.getSource()));
            } else {
                broadcastAsync(new TestSignal2(signal.getSource()));
                broadcastAsync(new TestSignal3(signal.getSource()));
            }
        }

        /**
         * Receive a signal of type TestSignal3.
         *
         * @param signal
         *            Signal received
         */
        @TmfSignalHandler
        public void receiveSignal3(final TestSignal3 signal) {
            if (sync) {
                broadcast(new TestSignal4(signal.getSource()));
            } else {
                broadcastAsync(new TestSignal4(signal.getSource()));
            }
        }
    }

    /**
     * Signal handler implementation for testing of sending and receiving
     * signals.
     */
    public class TestSignalHandler extends AbstractBaseSignalHandler {

        private CountDownLatch latch;

        /**
         * Constructor
         *
         */
        private TestSignalHandler() {
            this(true, null);
        }

        /**
         * Constructor
         *
         * @param logSyncSigs
         *            log sync signals
         * @param latch
         *            latch to count down when receiving last signal
         *            (TmfSingal4)
         */
        private TestSignalHandler(boolean logSyncSigs, CountDownLatch latch) {
            super("TestSignalHandler", logSyncSigs);
            this.latch = latch;
        }

        /**
         * Receive a signal of type TestSignal1.
         *
         * @param signal
         *            Signal received
         */
        @TmfSignalHandler
        public void receiveSignal1(final TestSignal1 signal) {
            receivedSignals.add(signal);
        }

        /**
         * Receive a signal of type TestSignal2.
         *
         * @param signal
         *            Signal received
         */
        @TmfSignalHandler
        public void receiveSignal2(final TestSignal2 signal) {
            receivedSignals.add(signal);
        }

        /**
         * Receive a signal of type TestSignal3.
         *
         * @param signal
         *            Signal received
         */
        @TmfSignalHandler
        public void receiveSignal3(final TestSignal3 signal) {
            receivedSignals.add(signal);
        }

        /**
         * Receive a signal of type TestSignal4.
         *
         * @param signal
         *            Signal received
         */
        @TmfSignalHandler
        public void receiveSignal4(final TestSignal4 signal) {
            receivedSignals.add(signal);
            if (latch != null) {
                latch.countDown();
            }
        }
    }

    /**
     * Base signal handler for start and end sync signals.
     */
    public abstract class AbstractBaseSignalHandler extends TmfComponent {
        List<TmfSignal> receivedSignals = new ArrayList<>();
        private boolean logSyncSigs;

        private AbstractBaseSignalHandler(String name, boolean logSyncSignal) {
            super(name);
            this.logSyncSigs = logSyncSignal;
        }

        /**
         * Receive a signal of type TmfStartSynchSignal.
         *
         * @param signal
         *            Signal received
         */
        @TmfSignalHandler
        public void receiveStartSynch(final TmfStartSynchSignal signal) {
            if (logSyncSigs) {
                receivedSignals.add(signal);
            }
        }

        /**
         * Receive a signal of type TmfEndSynchSignal.
         *
         * @param signal
         *            Signal received
         */
        @TmfSignalHandler
        public void receiveEndSynch(final TmfEndSynchSignal signal) {
            if (logSyncSigs) {
                receivedSignals.add(signal);
            }
        }
    }


    /**
     * Test Signal object
     */
    private class TestSignal1 extends TmfSignal {

        public TestSignal1(Object source) {
            super(source);
        }
    }

    /**
     * Test Signal object
     */
    private class TestSignal2 extends TmfSignal {

        public TestSignal2(Object source) {
            super(source);
        }
    }

    /**
     * Test Signal object
     */
    private class TestSignal3 extends TmfSignal {

        public TestSignal3(Object source) {
            super(source);
        }
    }

    /**
     * Test Signal object
     */
    private class TestSignal4 extends TmfSignal {

        public TestSignal4(Object source) {
            super(source);
        }
    }
}
