/**
 *    Copyright (C) 2018-present MongoDB, Inc.
 *
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the Server Side Public License, version 1,
 *    as published by MongoDB, Inc.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    Server Side Public License for more details.
 *
 *    You should have received a copy of the Server Side Public License
 *    along with this program. If not, see
 *    <http://www.mongodb.com/licensing/server-side-public-license>.
 *
 *    As a special exception, the copyright holders give permission to link the
 *    code of portions of this program with the OpenSSL library under certain
 *    conditions as described in each individual source file and distribute
 *    linked combinations including the program with the OpenSSL library. You
 *    must comply with the Server Side Public License in all respects for
 *    all of the code used other than as permitted herein. If you modify file(s)
 *    with this exception, you may extend this exception to your version of the
 *    file(s), but you are not obligated to do so. If you do not wish to do so,
 *    delete this exception statement from your version. If you delete this
 *    exception statement from all source files in the program, then also delete
 *    it in the license file.
 */
#pragma once

#include <boost/move/utility_core.hpp>
#include <boost/optional/optional.hpp>
#include <cstdint>

#include "mongo/bson/bsonobjbuilder.h"
#include "mongo/db/service_context.h"
#include "mongo/platform/atomic_word.h"
#include "mongo/stdx/mutex.h"
#include "mongo/util/assert_util_core.h"
#include "mongo/util/concurrency/admission_context.h"
#include "mongo/util/tick_source.h"
#include "mongo/util/time_support.h"

namespace mongo {

class Ticket;
class SemaphoreTicketHolder;

/**
 * Maintains and distributes tickets across operations from a limited pool of tickets. The ticketing
 * mechanism is required for global lock acquisition to reduce contention on storage engine
 * resources.
 */
class TicketHolder {
    friend class Ticket;

public:
    TicketHolder(ServiceContext* svcCtx, int32_t numTickets, bool trackPeakUsed);
    virtual ~TicketHolder() {}

    /**
     * Adjusts the total number of tickets allocated for the ticket pool to 'newSize'.
     *
     * Returns 'true' if the resize completed without reaching the 'deadline', and 'false'
     * otherwise.
     */
    bool resize(OperationContext* opCtx, int32_t newSize, Date_t deadline = Date_t::max());

    /**
     * Attempts to acquire a ticket without blocking.
     * Returns a ticket if one is available, and boost::none otherwise.
     */
    boost::optional<Ticket> tryAcquire(AdmissionContext* admCtx);

    /**
     * Attempts to acquire a ticket. Blocks until a ticket is acquired or the OperationContext is
     * interrupted, throwing an AssertionException.
     */
    Ticket waitForTicket(OperationContext* opCtx, AdmissionContext* admCtx);

    /**
     * Attempts to acquire a ticket within a deadline, 'until'. Returns 'true' if a ticket is
     * acquired and 'false' if the deadline is reached, but the operation is retryable. Throws an
     * AssertionException if the OperationContext is interrupted.
     */
    boost::optional<Ticket> waitForTicketUntil(OperationContext* opCtx,
                                               AdmissionContext* admCtx,
                                               Date_t until);

    /**
     * The same as `waitForTicketUntil` except the wait will be uninterruptible. Please make every
     * effort to make your waiter interruptible, and try not to use this function! It only exists
     * as a stepping stone until we can complete the work in SERVER-68868 to ensure all work in the
     * server is interruptible.
     *
     * TODO(SERVER-68868): Remove this function completely
     */
    boost::optional<Ticket> waitForTicketUntilNoInterrupt_DO_NOT_USE(OperationContext* opCtx,
                                                                     AdmissionContext* admCtx,
                                                                     Date_t until);

    /**
     * The total number of tickets allotted to the ticket pool.
     */
    int32_t outof() const {
        return _outof.loadRelaxed();
    }

    /**
     * Instantaneous number of tickets that are checked out by an operation.
     */
    int32_t used() const {
        return outof() - available();
    }

    /**
     * Peak number of tickets checked out at once since the previous time this function was called.
     * Invariants that 'trackPeakUsed' has been passed to the TicketHolder,
     */
    int32_t getAndResetPeakUsed();

    /**
     * Instantaneous number of tickets 'available' (not checked out by an operation) in the ticket
     * pool.
     */
    virtual int32_t available() const = 0;

    /**
     * Instantaneous number of operations waiting in queue for a ticket.
     *
     * TODO SERVER-74082: Once the SemaphoreTicketHolder is removed, consider changing this metric
     * to int32_t.
     */
    virtual int64_t queued() const = 0;

    /**
     * The total number of operations that acquired a ticket, completed their work, and released the
     * ticket.
     */
    virtual int64_t numFinishedProcessing() const = 0;

    /**
     * Statistics for queueing mechanisms in the TicketHolder implementations. The term "Queue" is a
     * loose abstraction for the way in which operations are queued when there are no available
     * tickets.
     */
    struct QueueStats {
        AtomicWord<std::int64_t> totalAddedQueue{0};
        AtomicWord<std::int64_t> totalRemovedQueue{0};
        AtomicWord<std::int64_t> totalFinishedProcessing{0};
        AtomicWord<std::int64_t> totalNewAdmissions{0};
        AtomicWord<std::int64_t> totalTimeProcessingMicros{0};
        AtomicWord<std::int64_t> totalStartedProcessing{0};
        AtomicWord<std::int64_t> totalCanceled{0};
        AtomicWord<std::int64_t> totalTimeQueuedMicros{0};
    };

    /**
     * Append TicketHolder statistics to the provided builder.
     */
    void appendStats(BSONObjBuilder& b) const;

protected:
    virtual bool _resizeImpl(WithLock lock,
                             OperationContext* opCtx,
                             int32_t newSize,
                             Date_t deadline);

private:
    /**
     * Releases a ticket back into the ticketing pool.
     */
    virtual void _releaseToTicketPool(Ticket& ticket) noexcept;

    virtual void _releaseToTicketPoolImpl(AdmissionContext* admCtx) noexcept = 0;

    virtual boost::optional<Ticket> _tryAcquireImpl(AdmissionContext* admCtx) = 0;

    boost::optional<Ticket> _waitForTicketUntil(OperationContext* opCtx,
                                                AdmissionContext* admCtx,
                                                Date_t until,
                                                bool interruptible);
    virtual boost::optional<Ticket> _waitForTicketUntilImpl(OperationContext* opCtx,
                                                            AdmissionContext* admCtx,
                                                            Date_t until,
                                                            bool interruptible) = 0;

    virtual void _appendImplStats(BSONObjBuilder& b) const {}

    /**
     * Fetches the queueing statistics for the given priority. All statistics that are queue
     * specific should be updated through the resulting 'QueueStats'.
     */
    virtual QueueStats& _getQueueStatsToUse(AdmissionContext::Priority priority) noexcept = 0;

    void _updatePeakUsed();

    const bool _trackPeakUsed;

    void _updateQueueStatsOnRelease(TicketHolder::QueueStats& queueStats, const Ticket& ticket);
    void _updateQueueStatsOnTicketAcquisition(AdmissionContext* admCtx,
                                              TicketHolder::QueueStats& queueStats);

    stdx::mutex _resizeMutex;
    QueueStats _exemptQueueStats;

protected:
    /**
     * Appends the standard statistics stored in QueueStats to BSONObjBuilder b;
     */
    void _appendCommonQueueImplStats(BSONObjBuilder& b, const QueueStats& stats) const;

    /**
     * Creates a ticket for a non-exempt admission.
     */
    Ticket _makeTicket(AdmissionContext* admCtx);

    AtomicWord<int32_t> _outof;
    AtomicWord<int32_t> _peakUsed;

    ServiceContext* _serviceContext;
};

class MockTicketHolder : public TicketHolder {
public:
    MockTicketHolder(ServiceContext* svcCtx) : TicketHolder(svcCtx, 0, true) {}

    int32_t available() const override {
        return _available;
    }

    void setPeakUsed(int32_t used) {
        _peakUsed.store(used);
    }

    void setAvailable(int32_t available) {
        _available = available;
    }

    int64_t queued() const override {
        return 0;
    }

    int64_t numFinishedProcessing() const override {
        return _numFinishedProcessing;
    }

    void setNumFinishedProcessing(int32_t numFinishedProcessing) {
        _numFinishedProcessing = numFinishedProcessing;
    }

private:
    void _releaseToTicketPoolImpl(AdmissionContext* admCtx) noexcept override;

    boost::optional<Ticket> _tryAcquireImpl(AdmissionContext* admCtx) override;

    boost::optional<Ticket> _waitForTicketUntilImpl(OperationContext* opCtx,
                                                    AdmissionContext* admCtx,
                                                    Date_t until,
                                                    bool interruptible) override;

    QueueStats& _getQueueStatsToUse(AdmissionContext::Priority priority) noexcept override {
        return _stats;
    }

    QueueStats _stats;
    int32_t _available = 0;
    int32_t _numFinishedProcessing = 0;
};

/**
 * RAII-style movable token that gets generated when a ticket is acquired and is automatically
 * released when going out of scope.
 */
class Ticket {
    Ticket(const Ticket&) = delete;
    Ticket& operator=(const Ticket&) = delete;

    friend class TicketHolder;

public:
    Ticket(Ticket&& t)
        : _ticketholder(t._ticketholder),
          _admissionContext(t._admissionContext),
          _priority(t._priority),
          _acquisitionTime(t._acquisitionTime) {
        t._ticketholder = nullptr;
        t._admissionContext = nullptr;
    }

    Ticket& operator=(Ticket&& t) {
        if (&t == this) {
            return *this;
        }

        invariant(!valid(), "Attempting to overwrite a valid ticket with another one");
        _ticketholder = std::exchange(t._ticketholder, nullptr);
        _admissionContext = std::exchange(t._admissionContext, nullptr);
        _priority = t._priority;
        _acquisitionTime = t._acquisitionTime;

        return *this;
    };

    ~Ticket() {
        if (_ticketholder) {
            _ticketholder->_releaseToTicketPool(*this);
        }
    }

    /**
     * Returns whether or not a ticket is being held.
     */
    bool valid() {
        return _ticketholder != nullptr;
    }

    /**
     * Returns the ticket priority.
     */
    AdmissionContext::Priority getPriority() const {
        return _priority;
    }

private:
    Ticket(TicketHolder* ticketHolder, AdmissionContext* admissionContext)
        : _ticketholder(ticketHolder), _admissionContext(admissionContext) {
        _priority = admissionContext->getPriority();
        _acquisitionTime = ticketHolder->_serviceContext->getTickSource()->getTicks();
    }

    /**
     * Discards the ticket without releasing it back to the ticketholder.
     */
    void discard() {
        _admissionContext->markTicketReleased();
        _ticketholder = nullptr;
        _admissionContext = nullptr;
    }

    TicketHolder* _ticketholder;
    AdmissionContext* _admissionContext;
    AdmissionContext::Priority _priority;
    TickSource::Tick _acquisitionTime;
};

inline Ticket TicketHolder::_makeTicket(AdmissionContext* admCtx) {
    // TODO(SERVER-92647): Move this to the Ticket constructor so it also applies to exempt tickets
    admCtx->markTicketHeld();
    return Ticket{this, admCtx};
}

}  // namespace mongo
