// Copyright (C) 2012-2016 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#include <config.h>

#include <util/threads/sync.h>

#include <exceptions/exceptions.h>

#include <cstring>
#include <memory>
#include <cerrno>
#include <cassert>

#include <pthread.h>

using std::unique_ptr;

namespace isc {
namespace util {
namespace thread {

class Mutex::Impl {
public:
    Impl()
#ifdef ENABLE_DEBUG
        : locked_count(0)
#endif // ENABLE_DEBUG
    {}

    pthread_mutex_t mutex;
#ifdef ENABLE_DEBUG
    size_t locked_count;
#endif // ENABLE_DEBUG
};

namespace {

struct Deinitializer {
    Deinitializer(pthread_mutexattr_t& attributes):
        attributes_(attributes)
    {}
    ~Deinitializer() {
        const int result = pthread_mutexattr_destroy(&attributes_);
        // This should never happen. According to the man page,
        // if there's error, it's our fault.
        assert(result == 0);
    }
    pthread_mutexattr_t& attributes_;
};

}

Mutex::Mutex() :
    impl_(NULL)
{
    pthread_mutexattr_t attributes;
    int result = pthread_mutexattr_init(&attributes);
    switch (result) {
        case 0: // All 0K
            break;
        case ENOMEM:
            throw std::bad_alloc();
        default:
            isc_throw(isc::InvalidOperation, std::strerror(result));
    }
    Deinitializer deinitializer(attributes);

    // If debug mode is enabled in compilation, use the slower
    // error-checking mutexes that detect deadlocks. Otherwise, use fast
    // mutexes which don't. See the pthread_mutexattr_settype() POSIX
    // documentation which describes these type attributes.
#ifdef ENABLE_DEBUG
    result = pthread_mutexattr_settype(&attributes, PTHREAD_MUTEX_ERRORCHECK);
#else
    result = pthread_mutexattr_settype(&attributes, PTHREAD_MUTEX_NORMAL);
#endif // ENABLE_DEBUG
    if (result != 0) {
        isc_throw(isc::InvalidOperation, std::strerror(result));
    }

    unique_ptr<Impl> impl(new Impl);
    result = pthread_mutex_init(&impl->mutex, &attributes);
    switch (result) {
        case 0: // All 0K
            impl_ = impl.release();
            break;
        case ENOMEM:
        case EAGAIN:
            throw std::bad_alloc();
        default:
            isc_throw(isc::InvalidOperation, std::strerror(result));
    }
}

Mutex::~Mutex() {
    if (impl_ != NULL) {
        const int result = pthread_mutex_destroy(&impl_->mutex);

#ifdef ENABLE_DEBUG
        const bool locked = impl_->locked_count != 0;
#endif // ENABLE_DEBUG

        delete impl_;
        // We don't want to throw from the destructor. Also, if this ever
        // fails, something is really screwed up a lot.
        assert(result == 0);

#ifdef ENABLE_DEBUG
        // We should not try to destroy a locked mutex, bad threaded monsters
        // could get loose if we ever do and it is also forbidden by pthreads.

        // This should not be possible to happen, since the
        // pthread_mutex_destroy should check for it already. But it seems
        // there are systems that don't check it.
        assert(!locked);
#endif // ENABLE_DEBUG
    }
}

#ifdef ENABLE_DEBUG

void
Mutex::postLockAction() {
    assert(impl_->locked_count == 0);
    ++impl_->locked_count;
}

void
Mutex::preUnlockAction(bool throw_ok) {
    if (impl_->locked_count == 0) {
        if (throw_ok) {
            isc_throw(isc::InvalidOperation,
                      "Unlock attempt for unlocked mutex");
        } else {
            assert(false);
        }
    }
    --impl_->locked_count;
}

bool
Mutex::locked() const {
    return (impl_->locked_count != 0);
}

#endif // ENABLE_DEBUG

void
Mutex::lock() {
    assert(impl_ != NULL);
    const int result = pthread_mutex_lock(&impl_->mutex);
    if (result != 0) {
        isc_throw(isc::InvalidOperation, std::strerror(result));
    }
#ifdef ENABLE_DEBUG
    postLockAction();           // Only in debug mode
#endif // ENABLE_DEBUG
}

bool
Mutex::tryLock() {
    assert(impl_ != NULL);
    const int result = pthread_mutex_trylock(&impl_->mutex);
    // In the case of pthread_mutex_trylock(), if it is called on a
    // locked mutex from the same thread, some platforms (such as fedora
    // and debian) return EBUSY whereas others (such as centos 5) return
    // EDEADLK. We return false and don't pass the lock attempt in both
    // cases.
    if (result == EBUSY || result == EDEADLK) {
        return (false);
    } else if (result != 0) {
        isc_throw(isc::InvalidOperation, std::strerror(result));
    }
#ifdef ENABLE_DEBUG
    postLockAction();           // Only in debug mode
#endif // ENABLE_DEBUG
    return (true);
}

void
Mutex::unlock() {
    assert(impl_ != NULL);
#ifdef ENABLE_DEBUG
    preUnlockAction(false);     // Only in debug mode.  Ensure no throw.
#endif // ENABLE_DEBUG
    const int result = pthread_mutex_unlock(&impl_->mutex);
    assert(result == 0); // This should never be possible
}

class CondVar::Impl {
public:
    Impl() {
        const int result = pthread_cond_init(&cond_, NULL);
        if (result != 0) {
            isc_throw(isc::Unexpected, "pthread_cond_init failed: "
                      << std::strerror(result));
        }
    }
    ~Impl() {
        const int result = pthread_cond_destroy(&cond_);

        // This can happen if we try to destroy cond_ while some other thread
        // is waiting on it.  assert() may be too strong for such a case,
        // but we cannot safely destroy cond_ anyway.  In order to avoid
        // throwing from a destructor we simply let the process die.
        assert(result == 0);
    }

    // For convenience allow the main class to access this directly.
    pthread_cond_t cond_;
};

CondVar::CondVar() : impl_(new Impl)
{}

CondVar::~CondVar() {
    delete impl_;
}

void
CondVar::wait(Mutex& mutex) {
#ifdef ENABLE_DEBUG
    mutex.preUnlockAction(true);    // Only in debug mode
    const int result = pthread_cond_wait(&impl_->cond_, &mutex.impl_->mutex);
    mutex.postLockAction();     // Only in debug mode
#else
    const int result = pthread_cond_wait(&impl_->cond_, &mutex.impl_->mutex);
#endif
    // pthread_cond_wait should normally succeed unless mutex is completely
    // broken.
    if (result != 0) {
        isc_throw(isc::BadValue, "pthread_cond_wait failed unexpectedly: " <<
                  std::strerror(result));
    }
}

void
CondVar::signal() {
    const int result = pthread_cond_signal(&impl_->cond_);

    // pthread_cond_signal() can only fail when if cond_ is invalid.  It
    //should be impossible as long as this is a valid CondVar object.
    assert(result == 0);
}

}
}
}
