/*******************************************************************************
 * Copyright (c) 2013, 2016 École Polytechnique de Montréal
 *
 * 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
 *******************************************************************************/

package org.eclipse.tracecompass.tmf.tests.stubs.analysis;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystemBuilder;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateValueTypeException;
import org.eclipse.tracecompass.statesystem.core.exceptions.TimeRangeException;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.statesystem.AbstractTmfStateProvider;
import org.eclipse.tracecompass.tmf.core.statesystem.ITmfStateProvider;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;

/**
 * Stub test provider for test state system analysis module
 *
 * @author Geneviève Bastien
 */
public class TestStateSystemProvider extends AbstractTmfStateProvider {

    /**
     * This interface allows unit tests to provide only the event handling part
     * of the state provider, without having to extend the analysis and the
     * classes
     */
    @FunctionalInterface
    public static interface TestStateProviderHandler {
        /**
         * Handles the event
         *
         * @param ss
         *            The state system builder
         * @param provider
         *            The state provider
         * @param event
         *            The event to handler
         * @return <code>true</code> if everything went fine, or
         *         <code>false</code> to cancel
         */
        boolean eventHandle(@NonNull ITmfStateSystemBuilder ss, AbstractTmfStateProvider provider, ITmfEvent event);
    }

    private static final int VERSION = 1;
    private static final String fString = "[]";
    private static int fCount = 0;
    private static final @NonNull TestStateProviderHandler DEFAULT_HANDLER = (ss, provider, event) -> {
        /* Just need something to fill the state system */
        if (fString.equals(event.getContent().getValue())) {
            try {
                int quarkId = ss.getQuarkAbsoluteAndAdd("String");
                int quark = ss.getQuarkRelativeAndAdd(quarkId, fString);
                ss.modifyAttribute(event.getTimestamp().getValue(), fCount++, quark);
            } catch (TimeRangeException | StateValueTypeException e) {

            }
        }
        return true;
    };
    private static @NonNull TestStateProviderHandler sfHandler = DEFAULT_HANDLER;

    /**
     * Set the event handler for the state provider
     *
     * @param handler
     *            The class containing the event handler for this state provider
     */
    public static void setEventHandler(TestStateProviderHandler handler) {
        if (handler == null) {
            sfHandler = DEFAULT_HANDLER;
            return;
        }
        sfHandler = handler;
    }

    private final Lock fLock = new ReentrantLock();
    private @Nullable Condition fNextEventSignal = null;

    /**
     * Constructor
     *
     * @param trace
     *            The trace this state provider is for
     */
    public TestStateSystemProvider(@NonNull ITmfTrace trace) {
        super(trace, "Stub State System");
    }

    /**
     * Constructor
     *
     * @param trace
     *            The trace this state provider is for
     * @param size
     *            The size of the queue, a.k.a the number of chunks that fit
     *            into the buffered queue.
     * @param chunk
     *            the size of a events that fit inside a chunk of the queue
     */
    public TestStateSystemProvider(@NonNull ITmfTrace trace, int size, int chunk) {
        super(trace, "Stub State System", size, chunk);
    }

    @Override
    public int getVersion() {
        return VERSION;
    }

    @Override
    public ITmfStateProvider getNewInstance() {
        return new TestStateSystemProvider(this.getTrace());
    }

    @Override
    protected void eventHandle(ITmfEvent event) {
        ITmfStateSystemBuilder ss = checkNotNull(getStateSystemBuilder());
        sfHandler.eventHandle(ss, this, event);
    }

    @Override
    public void processEvent(@NonNull ITmfEvent event) {
        fLock.lock();
        try {
            Condition cond = fNextEventSignal;
            if (cond != null) {
                cond.await();
            }
        } catch (InterruptedException e) {

        } finally {
            super.processEvent(event);
            fLock.unlock();
        }
    }

    /**
     * Set the processing of event to be one event at a time instead of the
     * default behavior. It will block until the next call to
     * {@link #signalNextEvent()} method call.
     *
     * @param throttleEvent
     *            Whether to wait for a signal to process the next event
     */
    public void setThrottling(boolean throttleEvent) {
        fLock.lock();
        try {
            if (throttleEvent) {
                Condition cond = fNextEventSignal;
                // If called for the first time, create a condition
                if (cond == null) {
                    cond = fLock.newCondition();
                    fNextEventSignal = cond;
                }

            } else {
                Condition cond = fNextEventSignal;
                if (cond != null) {
                    fNextEventSignal = null;
                    cond.signalAll();
                }
            }
        } finally {
            fLock.unlock();
        }

    }

    /**
     * Signal for the next event to be processed. Calling this method makes
     * sense only if {@link #setThrottling(boolean)} has been set to true
     */
    public void signalNextEvent() {
        fLock.lock();
        try {
            Condition cond = fNextEventSignal;
            if (cond != null) {
                cond.signalAll();
            }
        } finally {
            fLock.unlock();
        }
    }

}
