/* This file is part of reckless logging
 * Copyright 2015-2020 Mattias Flodin <git@codepentry.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
#ifndef RECKLESS_OUTPUT_BUFFER_HPP
#define RECKLESS_OUTPUT_BUFFER_HPP

#include "detail/platform.hpp"  // likely
#include "detail/spsc_event.hpp"

#include <cstddef>  // size_t
#include <new>      // bad_alloc
#include <cstring>  // strlen, memcpy
#include <functional>   // function
#include <mutex>
#include <system_error> // system_error, error_code
#include <cassert>

namespace reckless {
class writer;
class output_buffer;

enum class error_policy {
    ignore,
    notify_on_recovery,
    block,
    fail_immediately
    // TODO: policy to invoke callback from background thread
};

// Thrown if output_buffer::reserve() is used to allocate more than can fit in
// the output buffer during formatting of a single input frame. If this happens
// then you need to either enlarge the output buffer or reduce the amount of
// data produced by your formatter.
class excessive_output_by_frame : public std::bad_alloc {
public:
    char const* what() const noexcept override;
};

// Thrown if output_buffer::flush fails. This inherits from bad_alloc because
// it makes sense in the context where the formatter calls
// output_buffer::reserve(), and a flush intended to create more space in the
// buffer fails. In that context, it is essentially an allocation error. The
// formatter may catch this if it wishes, but it is expected that most will
// just let it fall through to the worker thread, where it will be dealt with
// appropriately.
//
// TODO we need to make flush() private, clients can't go around calling it in
// the formatter because if it fails and the exception reaches the worker
// thread then we'll end up assuming the buffer ran out of space and something
// was lost.
class flush_error : public std::bad_alloc {
public:
    flush_error(std::error_code const& error_code) :
        error_code_(error_code)
    {
    }
    char const* what() const noexcept override;
    std::error_code const& code() const
    {
        return error_code_;
    }

private:
    std::error_code error_code_;
};

using writer_error_callback_t = std::function<void (output_buffer* pbuffer, std::error_code ec, unsigned lost_record_count)>;

class output_buffer {
public:
    output_buffer();
    // TODO hide functions that are not relevant to the client, e.g. move
    // assignment, empty(), flush etc?
    output_buffer(writer* pwriter, std::size_t max_capacity);
    ~output_buffer();

    char* reserve(std::size_t size)
    {
        std::size_t remaining = pbuffer_end_ - pcommit_end_;
        if(detail::likely(size <= remaining))
            return pcommit_end_;
        else
            return reserve_slow_path(size);
    }

    void commit(std::size_t size)
    {
        pcommit_end_ += size;
    }

    void write(void const* buf, std::size_t count);

    void write(char const* s)
    {
        write(s, std::strlen(s));
    }

    void write(char c)
    {
        char* p = reserve(1);
        *p = c;
        commit(1);
    }

    unsigned output_buffer_full_count() const
    {
        return detail::atomic_load_relaxed(&output_buffer_full_count_);
    }

    std::size_t output_buffer_high_watermark() const
    {
        return detail::atomic_load_relaxed(&output_buffer_high_watermark_);
    }

protected:
    void reset() noexcept;
    // throw bad_alloc if unable to malloc() the buffer.
    void reset(writer* pwriter, std::size_t max_capacity);

    // Put a watermark indicating where the last complete output frame ends.
    void frame_end()
    {
        pframe_end_ = pcommit_end_;
    }
    // Notify that an input frame was lost because of a flush error.
    void lost_frame()
    {
        ++lost_input_frames_;
        revert_frame();
    }

    void revert_frame()
    {
        // Undo everything that has been written during the current input frame.
        pcommit_end_ = pframe_end_;
    }

    bool has_complete_frame() const
    {
        return pframe_end_ != pbuffer_;
    }

    // Need to make flush() public because of g++ bug 66957
    // <https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66957>
#ifdef __GNUC__
public:
#endif
    void flush();
#ifdef __GNUC__
protected:
#endif

    // Must not write to the log since it may cause a deadlock. May not throw
    // exceptions.
    void writer_error_callback(writer_error_callback_t callback = writer_error_callback_t())
    {
        std::lock_guard<std::mutex> lk(writer_error_callback_mutex_);
        writer_error_callback_ = move(callback);
    }

    error_policy temporary_error_policy() const
    {
        return temporary_error_policy_.load(std::memory_order_relaxed);
    }

    void temporary_error_policy(error_policy ep)
    {
        temporary_error_policy_.store(ep, std::memory_order_relaxed);
    }

    error_policy permanent_error_policy() const
    {
        return permanent_error_policy_.load(std::memory_order_relaxed);
    }

    void permanent_error_policy(error_policy ep)
    {
        assert(ep != error_policy::notify_on_recovery
                && ep != error_policy::block);
        permanent_error_policy_.store(ep, std::memory_order_relaxed);
    }

    detail::spsc_event shared_input_queue_full_event_; // FIXME rename to something that indicates this is used for all "notifications" to the worker thread

    std::atomic<error_policy> temporary_error_policy_{error_policy::ignore};
    std::atomic<error_policy> permanent_error_policy_{error_policy::fail_immediately};
    std::error_code error_code_;    // error code for error state
    bool error_flag_ = false;   // error state
    bool panic_flush_ = false;

private:
    output_buffer(output_buffer const&) = delete;
    output_buffer& operator=(output_buffer const&) = delete;

    char* reserve_slow_path(std::size_t size);
    void increment_output_buffer_full_count()
    {
        detail::atomic_increment_fetch_relaxed(&output_buffer_full_count_);
    }

    writer* pwriter_ = nullptr;
    char* pbuffer_ = nullptr;
    char* pframe_end_ = nullptr;
    char* pcommit_end_ = nullptr;
    char* pbuffer_end_ = nullptr;
    unsigned lost_input_frames_ = 0;
    std::error_code initial_error_;         // Keeps track of the first error that caused lost_input_frames_ to become non-zero.
    std::mutex writer_error_callback_mutex_;
    writer_error_callback_t writer_error_callback_;

    unsigned output_buffer_full_count_ = 0;
    std::size_t output_buffer_high_watermark_ = 0;
};

}

#endif  // RECKLESS_OUTPUT_BUFFER_HPP
