/*
 * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.testing.tests.distributedservers.rcm.broadcast;

import org.eclipse.persistence.exceptions.ExceptionHandler;
import org.eclipse.persistence.exceptions.RemoteCommandManagerException;
import org.eclipse.persistence.internal.sessions.AbstractSession;
import org.eclipse.persistence.internal.sessions.UnitOfWorkChangeSet;
import org.eclipse.persistence.sessions.SessionEvent;
import org.eclipse.persistence.sessions.SessionEventAdapter;
import org.eclipse.persistence.testing.framework.TestProblemException;

// This is a helper class used by TestWrapper.
// In case uow merge happened on source, it allows to wait until
// either the distributed merge is completed on target
// or something goes wrong.
// lock could be triggered by
//   merge into uow event in the source session.
//   direct call to lock method.
// If locked, waitUntilUnlocked method waits until unlocked, which could be triggered by:
//   error propagating remote command on the source session;
//   lack of localConnection on the source session (happens if connection removed on error);
//   distributed merge into uow event in the target session;
//   direct call to unlock method.
// Used by TestWrapper in the following pattern:
//   eventLock.initialize();
//   internalTest.test();
//   eventLock.waitUntilUnlocked();
// After ventLock.waitUntilUnlocked() returns, the reason for unlocking could be obtained from
//   eventLock.getState()
public class BroadcastEventLock {
    // states
    // locked states are negative ints
    // postMergeEvent with changes on the source
    public static final int LOCKED_BY_SOURCE_LISTENER = -2;
    // states required to be uninterrupted sequence of ints, minState equals the lowest state value
    protected static final int minState = LOCKED_BY_SOURCE_LISTENER;
    // lock() method was called
    public static final int LOCKED_DIRECTLY = -1;
    // unlocked states are zero and positive ints
    // initial state - set by initialize()
    public static final int UNLOCKED_INITIAL = 0;
    // unlock() method was called
    public static final int UNLOCKED_DIRECTLY = 1;
    // specified time ran out
    public static final int UNLOCKED_BY_TIMER = 2;
    // postDistributedMergeEvent on the target
    public static final int UNLOCKED_BY_TARGET_LISTENER = 3;
    // error propagating command on the source
    public static final int UNLOCKED_BY_SOURCE_EXCEPTION_HANDLER = 4;
    // source session doesn't have remote connection
    public static final int UNLOCKED_BY_SOURCE_SESSION = 5;
    // states required to be uninterrupted sequence of ints, minState equals the lowest state value
    protected static final int maxState = UNLOCKED_BY_SOURCE_SESSION;
    // num ber of tests
    protected static final int numberOfStates = maxState - minState + 1;

    // inner classes
    // base class for SourceListener and TargetListener

    class Listener extends SessionEventAdapter {
        public void attach(AbstractSession session) {
            session.getEventManager().addListener(this);
        }

        public void detach(AbstractSession session) {
            session.getEventManager().removeListener(this);
        }
    }
    // locks the lock after source uow merges

    class SourceListener extends Listener {
        public void postMergeUnitOfWorkChangeSet(SessionEvent event) {
            UnitOfWorkChangeSet uowChangeSet = (UnitOfWorkChangeSet)event.getProperty("UnitOfWorkChangeSet");
            if (uowChangeSet.hasChanges()) {
                lock(LOCKED_BY_SOURCE_LISTENER);
            }
        }
    }
    // unlocks the lock after target uow distributely merges

    class TargetListener extends Listener {
        public void postDistributedMergeUnitOfWorkChangeSet(SessionEvent event) {
            unlock(UNLOCKED_BY_TARGET_LISTENER);
        }
    }
    // unlocks the lock when remote command propagation fails on source

    class SourceExceptionHandler implements ExceptionHandler {
        ExceptionHandler originalHandler;

        public void attach(AbstractSession session) {
            originalHandler = session.getExceptionHandler();
            session.setExceptionHandler(this);
        }

        public void detach(AbstractSession session) {
            session.setExceptionHandler(originalHandler);
        }

        public Object handleException(RuntimeException exception) {
            if (exception instanceof RemoteCommandManagerException) {
                if (((RemoteCommandManagerException)exception).getErrorCode() == RemoteCommandManagerException.ERROR_PROPAGATING_COMMAND) {
                    unlock(UNLOCKED_BY_SOURCE_EXCEPTION_HANDLER);
                }
            }
            if (originalHandler != null) {
                return originalHandler.handleException(exception);
            } else {
                throw exception;
            }
        }
    }

    // attributes
    private int state;
    private boolean[] enabled;
    SourceListener sourceListener;
    TargetListener targetListener;
    SourceExceptionHandler sourceExceptionHandler;
    AbstractSession sourceSession;

    public BroadcastEventLock() {
        sourceListener = new SourceListener();
        targetListener = new TargetListener();
        sourceExceptionHandler = new SourceExceptionHandler();
        enabled = new boolean[numberOfStates];
        enableAllStates();
    }

    // public setup methods - used by BroadcastSetupHelper
    // attach - detach

    public boolean isAttached() {
        return sourceSession != null;
    }

    public void attach(AbstractSession session, boolean isSource) {
        if (isSource) {
            attachSource(session);
        } else {
            attachTarget(session);
        }
    }

    public void detach(AbstractSession session) {
        if (session == sourceSession) {
            detachSource();
        } else {
            detachTarget(session);
        }
    }

    // public methods used TestWrapper

    public void initialize() {
        state = UNLOCKED_INITIAL;
    }

    public void waitUntilUnlocked(long timeToWaitBeforeVerify) throws InterruptedException {
        if (isLocked()) {
            long n = timeToWaitBeforeVerify / 100;
            for (int i = 0; i < n && isLocked(); i++) {
                if (isStateEnabled(UNLOCKED_BY_SOURCE_SESSION)) {
                    // source session remote connection has been removed
                    if (sourceSession.getCommandManager().getTransportManager().getConnectionsToExternalServices().isEmpty()) {
                        // the following line unlocks, unless UNLOCKED_BY_SOURCE_SESSION state is disabled
                        unlock(UNLOCKED_BY_SOURCE_SESSION);
                    }
                }
                if (isLocked()) {
                    Thread.sleep(100);
                }
            }
            if (isLocked()) {
                unlock(UNLOCKED_BY_TIMER);
            } else if (state == UNLOCKED_BY_TARGET_LISTENER) {
                Thread.sleep(100);
            }
        }
    }

    // enable/disable state

    public void enableAllStates() {
        for (int i = 0; i < numberOfStates; i++) {
            enabled[i] = true;
        }
    }
    // return index for the passed state in enabled array

    protected int getStateIndex(int state) {
        return state - minState;
    }

    public boolean isStateEnabled(int state) {
        return enabled[getStateIndex(state)];
    }

    public void enableState(int state) {
        enabled[getStateIndex(state)] = true;
    }

    public void disableState(int state) {
        enabled[getStateIndex(state)] = false;
    }

    // returns the state

    public int getState() {
        return state;
    }

    // other public methods: setting and checking states

    public boolean isLocked() {
        return isLockedState(state);
    }

    public void lock() {
        lock(LOCKED_DIRECTLY);
    }

    public void unlock() {
        unlock(UNLOCKED_DIRECTLY);
    }

    protected void attachSource(AbstractSession session) {
        sourceListener.attach(session);
        sourceExceptionHandler.attach(session);
        sourceSession = session;
    }

    protected void attachTarget(AbstractSession session) {
        targetListener.attach(session);
    }

    protected void detachSource() {
        sourceListener.attach(sourceSession);
        sourceExceptionHandler.detach(sourceSession);
        sourceSession = null;
    }

    protected void detachTarget(AbstractSession session) {
        targetListener.detach(session);
    }

    protected static boolean isLockedState(int stateToExamine) {
        return stateToExamine < 0;
    }

    protected void verifyState(boolean isNewStateSupposedToBeLocked, int newState) {
        if (isNewStateSupposedToBeLocked != isLockedState(newState)) {
            throw new TestProblemException("Locked states should be negative; unlocked - non-negative");
        }
    }

    protected void lock(int newState) {
        verifyState(true, newState);
        changeState(newState);
    }

    protected void unlock(int newState) {
        verifyState(false, newState);
        changeState(newState);
    }
    // changes from locked to unlocked state or vise versa only.
    // would not change from one locked state to another.

    protected void changeState(int newState) {
        if ((isLocked() != isLockedState(newState)) && isStateEnabled(newState)) {
            state = newState;
        }
    }
}
