/*
 * Copyright (c), Zeriph Enterprises
 * All rights reserved.
 * 
 * Contributor(s):
 * Zechariah Perez, omni (at) zeriph (dot) com
 * 
 * THIS SOFTWARE IS PROVIDED BY ZERIPH AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL ZERIPH AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
#include <omni/sync/conditional.hpp>
#include <omni/sync/scoped_lock.hpp>
#include <omni/system.hpp> // OMNI_GLE_PRNT
#include <omni/exception.hpp>
#include <omni/sync/this_thread.hpp>

omni::sync::conditional::conditional() :
    OMNI_CTOR_FW(omni::sync::conditional)
    m_signal(),
    m_wait(),
    m_waitreq(0),
    m_signaled(0)
{
    this->_init();
}

omni::sync::conditional::conditional(bool initially_signaled) :
    OMNI_CTOR_FW(omni::sync::conditional)
    m_signal(),
    m_wait(),
    m_waitreq(0),
    m_signaled(0)
{
    this->_init();
    if (initially_signaled) { this->signal(); }
}

omni::sync::conditional::~conditional() OMNI_DTOR_THROWS
{
    OMNI_TRY_FW
    OMNI_DTOR_FW
    omni::sync::mutex_lock(this->m_wait);
    bool await = this->m_waitreq > 0;
    omni::sync::mutex_unlock(this->m_wait);
    if (await) {
        OMNI_ERR_FW("the conditional has active waits on call to dispose", omni::exceptions::active_wait_exception())
    }
    #if defined(OMNI_OS_WIN)
        #if defined(OMNI_WIN_USE_EVENT_CONDITIONAL)
            ::CloseHandle(this->m_signal);
            this->m_signal = 0;
        #endif
    #else
        int ret = ::pthread_cond_destroy(&this->m_signal);
        if (ret != 0) {
            OMNI_ERRV_FW("An error occurred destroying the conditional: ", ret, omni::exceptions::conditional_exception(ret))
        }
    #endif
    omni::sync::mutex_destroy(this->m_wait);
    OMNI_CATCH_FW
}

void omni::sync::conditional::broadcast()
{
    #if defined(OMNI_OS_WIN)
        #if defined(OMNI_WIN_USE_EVENT_CONDITIONAL)
            omni::sync::mutex_lock(this->m_wait);
            uint32_t wreq = this->m_waitreq+1;
            this->m_signaled = 1;
            while (--wreq > 0) {
                if (::SetEvent(this->m_signal) == 0) {
                    omni::sync::mutex_unlock(this->m_wait);
                    OMNI_ERRV_RET_FW("An error occurred on the conditional: ", OMNI_GLE, omni::exceptions::conditional_exception(OMNI_GLE))
                }
                omni::sync::mutex_unlock(this->m_wait);
                omni::sync::yield_thread();
                omni::sync::mutex_lock(this->m_wait);
            }
            omni::sync::mutex_unlock(this->m_wait);
        #else
            omni::sync::mutex_lock(this->m_wait);
            this->m_signaled = 1;
            ::WakeAllConditionVariable(&this->m_signal);
            omni::sync::mutex_unlock(this->m_wait);
        #endif
    #else
        omni::sync::mutex_lock(this->m_wait);
        this->m_signaled = 1;
        int ret = ::pthread_cond_broadcast(&this->m_signal);
        omni::sync::mutex_unlock(this->m_wait);
        if (ret != 0) {
            OMNI_ERRV_FW("An error occurred on the conditional: ", ret, omni::exceptions::conditional_exception(ret))
        }
    #endif
}

void omni::sync::conditional::reset()
{
    omni::sync::mutex_lock(this->m_wait);
    this->m_signaled = 0;
    #if defined(OMNI_WIN_USE_EVENT_CONDITIONAL)
        ::ResetEvent(this->m_signal);
    #endif
    omni::sync::mutex_unlock(this->m_wait);
}

void omni::sync::conditional::signal()
{
    #if defined(OMNI_OS_WIN)
        #if defined(OMNI_WIN_USE_EVENT_CONDITIONAL)
            omni::sync::mutex_lock(this->m_wait);
            this->m_signaled = 1;
            if (::SetEvent(this->m_signal) == 0) {
                omni::sync::mutex_unlock(this->m_wait);
                OMNI_ERRV_RET_FW("An error occurred on the conditional: ", OMNI_GLE, omni::exceptions::conditional_exception(OMNI_GLE))
            }
            omni::sync::mutex_unlock(this->m_wait);
        #else
            omni::sync::mutex_lock(this->m_wait);
            this->m_signaled = 1;
            ::WakeConditionVariable(&this->m_signal);
            omni::sync::mutex_unlock(this->m_wait);
        #endif
    #else
        omni::sync::mutex_lock(this->m_wait);
        this->m_signaled = 1;
        int ret = ::pthread_cond_signal(&this->m_signal);
        omni::sync::mutex_unlock(this->m_wait);
        if (ret != 0) {
            OMNI_ERRV_FW("An error occurred on the conditional: ", ret, omni::exceptions::conditional_exception(ret))
        }
    #endif
}

bool omni::sync::conditional::wait()
{
    omni::sync::mutex_lock(this->m_wait);
    if (this->m_signaled == 1) {
        omni::sync::mutex_unlock(this->m_wait);
        return false;
    }
    ++this->m_waitreq;
    omni::sync::mutex_unlock(this->m_wait);
    #if defined(OMNI_OS_WIN)
        #if defined(OMNI_WIN_USE_EVENT_CONDITIONAL)
            switch (::WaitForSingleObject(this->m_signal, INFINITE)) {
                case WAIT_OBJECT_0: { // 0x00000000L
                    omni::sync::mutex_lock(this->m_wait);
                    if (this->m_signaled == 1) { --this->m_waitreq; }
                    omni::sync::mutex_unlock(this->m_wait);
                    return true;
                } break;
                #if defined(OMNI_DBG_L3)
                    case WAIT_FAILED: OMNI_DV3_FW("a system error occurred on the conditional object: ", OMNI_GLE_PRNT); break;
                    OMNI_DSW_FW(WAIT_ABANDONED); // 0x00000080L
                    OMNI_DSW_FW(WAIT_TIMEOUT); // 0x00000102L
                #endif
                default: break;
            }
        #else
            DWORD ret = 0;
            while (!this->signaled()) {
                omni::sync::scoped_lock<omni::sync::mutex_t> mlock(&this->m_wait);
                if (!::SleepConditionVariableCS(&this->m_signal, &this->m_wait, INFINITE)) {
                    #if defined(OMNI_DBG_L3)
                    OMNI_DV3_FW("a system error occurred on the conditional object: ", OMNI_GLE);
                    #endif
                }
                // spurious wake up?
                if (this->m_signaled == 0) { continue; }
                --this->m_waitreq;
                return true;
            }
        #endif
    #else
        int ret = 0;
        while (!this->signaled()) {
            omni::sync::scoped_lock<omni::sync::mutex_t> mlock(&this->m_wait);
            ret = ::pthread_cond_wait(&this->m_signal, &this->m_wait);
            if (ret == 0) {
                // spurious wake up?
                if (this->m_signaled == 0) { continue; }
                --this->m_waitreq;
                return true;
            }
            #if defined(OMNI_DBG_L3)
            else { OMNI_DV3_FW("a system error occurred on the conditional object: ", ret); }
            #endif
        }
    #endif
    omni::sync::mutex_lock(this->m_wait);
    --this->m_waitreq;
    omni::sync::mutex_unlock(this->m_wait);
    return false;
}

bool omni::sync::conditional::wait(uint32_t timeout_ms)
{
    omni::sync::mutex_lock(this->m_wait);
    if (this->m_signaled == 1) {
        omni::sync::mutex_unlock(this->m_wait);
        return false;
    }
    ++this->m_waitreq;
    omni::sync::mutex_unlock(this->m_wait);
    #if defined(OMNI_OS_WIN)
        #if defined(OMNI_WIN_USE_EVENT_CONDITIONAL)
            switch (::WaitForSingleObject(this->m_signal, timeout_ms)) {
                case WAIT_OBJECT_0: { // 0x00000000L
                    omni::sync::mutex_lock(this->m_wait);
                    if (this->m_signaled == 1) { --this->m_waitreq; }
                    omni::sync::mutex_unlock(this->m_wait);
                    return true;
                } break;
                #if defined(OMNI_DBG_L3)
                    case WAIT_FAILED: OMNI_DBGEV("a system error occurred on the conditional object: ", OMNI_GLE_PRNT); break;
                    OMNI_DSW_FW(WAIT_ABANDONED); // 0x00000080L
                    OMNI_DSW_FW(WAIT_TIMEOUT); // 0x00000102L
                #endif
                default: break;
            }
        #else
            DWORD ret = 0;
            while (!this->signaled()) {
                omni::sync::scoped_lock<omni::sync::mutex_t> mlock(&this->m_wait);
                if (!::SleepConditionVariableCS(&this->m_signal, &this->m_wait, timeout_ms)) {
                    ret = OMNI_GLE;
                    if (ret == ERROR_TIMEOUT) { // time out while waiting
                        --this->m_waitreq;
                        return false;
                    }
                    #if defined(OMNI_DBG_L3)
                    else { OMNI_DV3_FW("a system error occurred on the conditional object: ", ret); }
                    #endif
                }
                // spurious wake up?
                if (this->m_signaled == 0) { continue; }
                --this->m_waitreq;
                return true;
            }
        #endif
    #else
        int ret = 0;
        struct timespec tm;
        #if defined(OMNI_OS_APPLE)
            struct timeval atv;
            if (::gettimeofday(&atv, NULL) != 0) {
                OMNI_ERRV_RETV_FW("An error occurred getting the clock time: ", errno, omni::exceptions::clock_exception(errno), false)
            }
            tm.tv_sec = atv.tv_sec;
            tm.tv_nsec = atv.tv_usec * 1000;
        #else
            if (::clock_gettime(CLOCK_REALTIME, &tm) != 0) {
                OMNI_ERRV_RETV_FW("An error occurred getting the clock time: ", errno, omni::exceptions::clock_exception(errno), false)
            }
        #endif
        if (timeout_ms > 0) {
            if (timeout_ms >= 1000) { tm.tv_sec += (timeout_ms / 1000); }
            tm.tv_nsec += ((timeout_ms % 1000) * 1000);
        }
        while (!this->signaled()) {
            omni::sync::scoped_lock<omni::sync::mutex_t> mlock(&this->m_wait);
            ret = ::pthread_cond_timedwait(&this->m_signal, &this->m_wait, &tm);
            if (ret == 0) {
                // spurious wake up?
                if (this->m_signaled == 0) { continue; }
                --this->m_waitreq;
                return true;
            } else if (ret == ETIMEDOUT) { // time out while waiting
                --this->m_waitreq;
                return false;
            }
            #if defined(OMNI_DBG_L3)
            else { OMNI_DV3_FW("a system error occurred on the conditional object: ", ret); }
            #endif
        }
    #endif
    omni::sync::mutex_lock(this->m_wait);
    --this->m_waitreq;
    omni::sync::mutex_unlock(this->m_wait);
    return false;
}

bool omni::sync::conditional::operator==(const omni::sync::conditional& o)
{
    if (this == &o) { return true; }
    omni::sync::scoped_lock<omni::sync::mutex_t> mlock(&this->m_wait);
    omni::sync::scoped_lock<omni::sync::mutex_t> mlock2(&o.m_wait);
    return (this->m_signaled == o.m_signaled &&
           &this->m_signal == &o.m_signal &&
           this->m_waitreq == o.m_waitreq)
           OMNI_EQUAL_FW(o);
}

bool omni::sync::conditional::operator!=(const omni::sync::conditional& o)
{
    return !(*this == o);
}

///////// start private methods /////////

void omni::sync::conditional::_init()
{
    omni::sync::mutex_init(this->m_wait);
    #if defined(OMNI_OS_WIN)
        #if defined(OMNI_WIN_USE_EVENT_CONDITIONAL)
            // NULL security attributes, auto-reset event, initially non-signalled, no name
            this->m_signal = ::CreateEvent(NULL, FALSE, FALSE, NULL);
            // auto-reset == FALSE gives the same functionality as a pthread_cond_t
            if (this->m_signal == NULL) {
                OMNI_ERRV_FW("The event could not be created: ", OMNI_GLE, omni::exceptions::conditional_exception(OMNI_GLE))
            }
        #else
            ::InitializeConditionVariable(&this->m_signal);
        #endif
    #else
        int ret = ::pthread_cond_init(&this->m_signal, NULL);
        if (ret != 0) {
            OMNI_ERRV_FW("The event could not be created: ", ret, omni::exceptions::conditional_exception(ret))
        }
    #endif
}
