#include <cassert>
#include <clocale>
#include <ios>

#include "restc-cpp/restc-cpp.h"
#include "restc-cpp/DataReader.h"
#include "restc-cpp/DataReaderStream.h"
#include "restc-cpp/error.h"
#include "restc-cpp/logging.h"

using namespace std;

namespace restc_cpp {


class ChunkedReaderImpl : public DataReader {
public:

    ChunkedReaderImpl(add_header_fn_t&& fn, unique_ptr<DataReaderStream>&& source)
    : stream_{move(source)}, add_header_(move(fn))
    {
    }

    bool IsEof() const override {
        return stream_->IsEof();
    }

    void Finish() override {
        ReadSome();
        if (!IsEof()) {
            throw ProtocolException("Failed to finish chunked payload");
        }

        if (stream_) {
            stream_->Finish();
        }
    }

    string ToPrintable(boost::string_ref buf) const {
        ostringstream out;
        locale loc;
        auto pos = 0;
        out << endl;

        for(const auto ch : buf) {
            if (!(++pos % line_length)) {
                out << endl;
            }
            if (std::isprint(ch, loc)) {
                out << ch;
            } else {
                out << '.';
            }
        }

        return out.str();
    }

    void Log(const boost::asio::const_buffers_1 buffers, const char *tag) {
        const auto buf_len = boost::asio::buffer_size(*buffers.begin());

        // At the time of the implementation, there are never multiple buffers.
        RESTC_CPP_LOG_TRACE_(tag << ' ' << "# " << buf_len
            << " bytes: "
            << ToPrintable({
                boost::asio::buffer_cast<const char *>(*buffers.begin()),
                           buf_len}));
    }

    boost::asio::const_buffers_1 ReadSome() override {

        EatPadding();

        if (stream_->IsEof()) {
            return {nullptr, 0};
        }

        if (chunk_len_ == 0) {
            RESTC_CPP_LOG_TRACE_("ChunkedReaderImpl::ReadSome(): Need new chunk.");
            chunk_len_ = GetNextChunkLen();
            RESTC_CPP_LOG_TRACE_("ChunkedReaderImpl::ReadSome(): "
                << "Next chunk is " << chunk_len_ << " bytes ("
                << hex << chunk_len_ << " hex)");
            if (chunk_len_ == 0) {
                // Read the trailer
                RESTC_CPP_LOG_TRACE_("ChunkedReaderImpl::ReadSome(): End of chunked stream - reading headers");
                stream_->ReadHeaderLines(add_header_);
                stream_->SetEof();
                RESTC_CPP_LOG_TRACE_("ChunkedReaderImpl::ReadSome(): End of chunked stream. Done.");
                return {nullptr, 0};
            }
        }

        auto data = GetData();

        Log(data, "ChunkedReaderImpl::ReadSome()");

        return data;
    }

private:
    void EatPadding() {
        if (eat_chunk_padding_) {
            eat_chunk_padding_ = false;

            char ch = {};
            if ((ch = stream_->Getc()) != '\r') {
                throw ParseException("Chunk: Missing padding CR!");
            }

            if ((ch = stream_->Getc()) != '\n') {
                throw ParseException("Chunk: Missing padding LF!");
            }
        }
    }

    boost::asio::const_buffers_1 GetData() {

        auto rval = stream_->GetData(chunk_len_);
        const auto seg_len = boost::asio::buffer_size(rval);
        chunk_len_ -= seg_len;

        if (chunk_len_ == 0) {
                eat_chunk_padding_ = true;
        }

        return rval;
    }

    size_t GetNextChunkLen() {
        static constexpr size_t magic_16 = 16;
        static constexpr size_t magic_10 = 10;
        size_t chunk_len = 0;
        char ch = stream_->Getc();

        if (!isxdigit(ch)) {
            throw ParseException("Missing chunk-length in new chunk.");
        }

        for(; isxdigit(ch); ch = stream_->Getc()) {
            chunk_len *= magic_16;
            if (ch >= 'a') {
                chunk_len += magic_10 + (ch - 'a');
            } else if (ch >= 'A') {
                chunk_len += magic_10 + (ch - 'A');
            } else {
                chunk_len += ch - '0';
            }
        }

        for(; ch != '\r'; ch = stream_->Getc())
            ;

        if (ch != '\r') {
            throw ParseException("Missing CR in first chunk line");
        }

        if ((ch = stream_->Getc()) != '\n') {
            throw ParseException("Missing LF in first chunk line");
        }

        return chunk_len;
    }

    size_t chunk_len_ = 0;
    bool eat_chunk_padding_ = false;
    std::unique_ptr<DataReaderStream> stream_;
    //boost::string_ref buffer;
    add_header_fn_t add_header_;
};

DataReader::ptr_t
DataReader::CreateChunkedReader(add_header_fn_t fn, unique_ptr<DataReaderStream>&& source) {
    return make_unique<ChunkedReaderImpl>(move(fn), move(source));
}


} // namespace