//          Copyright Nat Goodspeed 2015.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

#include <boost/fiber/all.hpp>
#include <iostream>
#include <sstream>
#include <exception>
#include <string>
#include <algorithm>                // std::min()
#include <errno.h>                  // EWOULDBLOCK
#include <cassert>
#include <cstdio>

/*****************************************************************************
*   example nonblocking API
*****************************************************************************/
//[NonblockingAPI
class NonblockingAPI {
public:
    NonblockingAPI();

    // nonblocking operation: may return EWOULDBLOCK
    int read( std::string & data, std::size_t desired);

/*=    ...*/
//<-
    // for simulating a real nonblocking API
    void set_data( std::string const& data, std::size_t chunksize);
    void inject_error( int ec);

private:
    std::string data_;
    int         injected_;
    unsigned    tries_;
    std::size_t chunksize_;
//->
};
//]

/*****************************************************************************
*   fake NonblockingAPI implementation... pay no attention to the little man
*   behind the curtain...
*****************************************************************************/
NonblockingAPI::NonblockingAPI() :
    injected_( 0),
    tries_( 0),
    chunksize_( 9999) {
}

void NonblockingAPI::set_data( std::string const& data, std::size_t chunksize) {
    data_ = data;
    chunksize_ = chunksize;
    // This delimits the start of a new test. Reset state.
    injected_ = 0;
    tries_ = 0;
}

void NonblockingAPI::inject_error( int ec) {
    injected_ = ec;
}

int NonblockingAPI::read( std::string & data, std::size_t desired) {
    // in case of error
    data.clear();

    if ( injected_) {
        // copy injected_ because we're about to reset it
        auto injected( injected_);
        injected_ = 0;
        // after an error situation, restart success count
        tries_ = 0;
        return injected;
    }

    if ( ++tries_ < 5) {
        // no injected error, but the resource isn't yet ready
        return EWOULDBLOCK;
    }

    // tell caller there's nothing left
    if ( data_.empty() ) {
        return EOF;
    }

    // okay, finally have some data
    // but return minimum of desired and chunksize_
    std::size_t size( ( std::min)( desired, chunksize_) );
    data = data_.substr( 0, size);
    // strip off what we just returned
    data_ = data_.substr( size);
    // reset I/O retries count for next time
    tries_ = 0;
    // success
    return 0;
}

/*****************************************************************************
*   adapters
*****************************************************************************/
//[nonblocking_read_chunk
// guaranteed not to return EWOULDBLOCK
int read_chunk( NonblockingAPI & api, std::string & data, std::size_t desired) {
    int error;
    while ( EWOULDBLOCK == ( error = api.read( data, desired) ) ) {
        // not ready yet -- try again on the next iteration of the
        // application's main loop
        boost::this_fiber::yield();
    }
    return error;
}
//]

//[nonblocking_read_desired
// keep reading until desired length, EOF or error
// may return both partial data and nonzero error
int read_desired( NonblockingAPI & api, std::string & data, std::size_t desired) {
    // we're going to accumulate results into 'data'
    data.clear();
    std::string chunk;
    int error = 0;
    while ( data.length() < desired &&
           ( error = read_chunk( api, chunk, desired - data.length() ) ) == 0) {
        data.append( chunk);
    }
    return error;
}
//]

//[nonblocking_IncompleteRead
// exception class augmented with both partially-read data and errorcode
class IncompleteRead : public std::runtime_error {
public:
    IncompleteRead( std::string const& what, std::string const& partial, int ec) :
        std::runtime_error( what),
        partial_( partial),
        ec_( ec) {
    }

    std::string get_partial() const {
        return partial_;
    }

    int get_errorcode() const {
        return ec_;
    }

private:
    std::string partial_;
    int         ec_;
};
//]

//[nonblocking_read
// read all desired data or throw IncompleteRead
std::string read( NonblockingAPI & api, std::size_t desired) {
    std::string data;
    int ec( read_desired( api, data, desired) );

    // for present purposes, EOF isn't a failure
    if ( 0 == ec || EOF == ec) {
        return data;
    }

    // oh oh, partial read
    std::ostringstream msg;
    msg << "NonblockingAPI::read() error " << ec << " after "
        << data.length() << " of " << desired << " characters";
    throw IncompleteRead( msg.str(), data, ec);
}
//]

int main( int argc, char *argv[]) {
    NonblockingAPI api;
    const std::string sample_data("abcdefghijklmnopqrstuvwxyz");

    // Try just reading directly from NonblockingAPI
    api.set_data( sample_data, 5);
    std::string data;
    int ec = api.read( data, 13);
    // whoops, underlying resource not ready
    assert(ec == EWOULDBLOCK);
    assert(data.empty());

    // successful read()
    api.set_data( sample_data, 5);
    data = read( api, 13);
    assert(data == "abcdefghijklm");

    // read() with error
    api.set_data( sample_data, 5);
    // don't accidentally pick either EOF or EWOULDBLOCK
    assert(EOF != 1);
    assert(EWOULDBLOCK != 1);
    api.inject_error(1);
    int thrown = 0;
    try {
        data = read( api, 13);
    } catch ( IncompleteRead const& e) {
        thrown = e.get_errorcode();
    }
    assert(thrown == 1);

    std::cout << "done." << std::endl;

    return EXIT_SUCCESS;
}
