/*    Copyright 2014 MongoDB Inc.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

#pragma once

#include <boost/thread/mutex.hpp>

#include "mongo/base/disallow_copying.h"
#include "mongo/db/jsobj.h"
#include "mongo/platform/atomic_word.h"

namespace mongo {
/**
 * A simple thread-safe fail point implementation that can be activated and
 * deactivated, as well as embed temporary data into it.
 *
 * The fail point has a static instance, which is represented by a FailPoint
 * object, and dynamic instance, which are all the threads in between
 * shouldFailOpenBlock and shouldFailCloseBlock.
 *
 * Sample use:
 * // Declared somewhere:
 * FailPoint makeBadThingsHappen;
 *
 * // Somewhere in the code
 * return false || MONGO_FAIL_POINT(makeBadThingsHappen);
 *
 * or
 *
 * // Somewhere in the code
 * MONGO_FAIL_POINT_BLOCK(makeBadThingsHappen, blockMakeBadThingsHappen) {
 *     const BSONObj& data = blockMakeBadThingsHappen.getData();
 *     // Do something
 * }
 *
 * Invariants:
 *
 * 1. Always refer to _fpInfo first to check if failPoint is active or not before
 *    entering fail point or modifying fail point.
 * 2. Client visible fail point states are read-only when active.
 */
class FailPoint {
    MONGO_DISALLOW_COPYING(FailPoint);

public:
    typedef AtomicUInt32::WordType ValType;
    enum Mode { off, alwaysOn, random, nTimes, numModes };
    enum RetCode { fastOff = 0, slowOff, slowOn };

    FailPoint();

    /**
     * Note: This is not side-effect free - it can change the state to OFF after calling.
     *
     * @return true if fail point is active.
     */
    inline bool shouldFail() {
        RetCode ret = shouldFailOpenBlock();

        if (MONGO_likely(ret == fastOff)) {
            return false;
        }

        shouldFailCloseBlock();
        return ret == slowOn;
    }

    /**
     * Checks whether fail point is active and increments the reference counter without
     * decrementing it. Must call shouldFailCloseBlock afterwards when the return value
     * is not fastOff. Otherwise, this will remain read-only forever.
     *
     * @return slowOn if fail point is active.
     */
    inline RetCode shouldFailOpenBlock() {
        if (MONGO_likely((_fpInfo.loadRelaxed() & ACTIVE_BIT) == 0)) {
            return fastOff;
        }

        return slowShouldFailOpenBlock();
    }

    /**
     * Decrements the reference counter.
     * @see #shouldFailOpenBlock
     */
    void shouldFailCloseBlock();

    /**
     * Changes the settings of this fail point. This will turn off the fail point
     * and waits for all dynamic instances referencing this fail point to go away before
     * actually modifying the settings.
     *
     * @param mode the new mode for this fail point.
     * @param val the value that can have different usage depending on the mode:
     *
     *     - off, alwaysOn: ignored
     *     - random:
     *     - nTimes: the number of times this fail point will be active when
     *         #shouldFail or #shouldFailOpenBlock is called.
     *
     * @param extra arbitrary BSON object that can be stored to this fail point
     *     that can be referenced afterwards with #getData. Defaults to an empty
     *     document.
     */
    void setMode(Mode mode, ValType val = 0, const BSONObj& extra = BSONObj());

    /**
     * @returns a BSON object showing the current mode and data stored.
     */
    BSONObj toBSON() const;

private:
    static const ValType ACTIVE_BIT = 1 << 31;
    static const ValType REF_COUNTER_MASK = ~ACTIVE_BIT;

    // Bit layout:
    // 31: tells whether this fail point is active.
    // 0~30: unsigned ref counter for active dynamic instances.
    AtomicUInt32 _fpInfo;

    // Invariant: These should be read only if ACTIVE_BIT of _fpInfo is set.
    Mode _mode;
    AtomicInt32 _timesOrPeriod;
    BSONObj _data;

    // protects _mode, _timesOrPeriod, _data
    mutable boost::mutex _modMutex;

    /**
     * Enables this fail point.
     */
    void enableFailPoint();

    /**
     * Disables this fail point.
     */
    void disableFailPoint();

    /**
     * slow path for #shouldFailOpenBlock
     */
    RetCode slowShouldFailOpenBlock();

    /**
     * @return the stored BSONObj in this fail point. Note that this cannot be safely
     *      read if this fail point is off.
     */
    const BSONObj& getData() const;

    friend class ScopedFailPoint;
};

/**
 * Helper class for making sure that FailPoint#shouldFailCloseBlock is called when
 * FailPoint#shouldFailOpenBlock was called. This should only be used within the
 * MONGO_FAIL_POINT_BLOCK macro.
 */
class ScopedFailPoint {
    MONGO_DISALLOW_COPYING(ScopedFailPoint);

public:
    ScopedFailPoint(FailPoint* failPoint);
    ~ScopedFailPoint();

    /**
     * @return true if fail point is on. This will be true at most once.
     */
    inline bool isActive() {
        if (_once) {
            return false;
        }

        _once = true;

        FailPoint::RetCode ret = _failPoint->shouldFailOpenBlock();
        _shouldClose = ret != FailPoint::fastOff;
        return ret == FailPoint::slowOn;
    }

    /**
     * @return the data stored in the fail point. #isActive must be true
     *     before you can call this.
     */
    const BSONObj& getData() const;

private:
    FailPoint* _failPoint;
    bool _once;
    bool _shouldClose;
};

#define MONGO_FAIL_POINT(symbol) MONGO_unlikely(symbol.shouldFail())

/**
 * Macro for creating a fail point with block context. Also use this when
 * you want to access the data stored in the fail point.
 */
#define MONGO_FAIL_POINT_BLOCK(symbol, blockSymbol) \
    for (mongo::ScopedFailPoint blockSymbol(&symbol); MONGO_unlikely(blockSymbol.isActive());)
}
