#include "wrap-sstream.hpp"

#include "printbuffer.hpp"

#include <algorithm>
#include <cstdio>
#include <cinttypes>

struct PrintBufferCore  : public eng::opnew {
    // The most recent lines printed.
    eng::deque<eng::string> lines_;

    // Line number of the first line in the buffer.  From this, all other
    // line numbers can be inferred.
    int first_line_;

    // Line number of the first unchecked line in the buffer.  All line
    // numbers including this one and beyond are unchecked.
    // "Unchecked" lines are lines in a non-authoritative model
    // that haven't been verified through difference transmission
    int first_unchecked_;

    // Constructor.
    PrintBufferCore() : first_line_(0), first_unchecked_(0) {}
};

static PrintBufferCore empty_core_;

PrintBuffer::PrintBuffer() {
    core_ = &empty_core_;
}

PrintBuffer::~PrintBuffer() {
    if (core_ != &empty_core_) delete core_;
}

int PrintBuffer::first_line() const {
    return core_->first_line_;
}

int PrintBuffer::last_line() const {
    return core_->first_line_ + core_->lines_.size();
}

int PrintBuffer::first_unchecked() const {
    return core_->first_unchecked_;
}

bool PrintBuffer::never_printed() const {
    return (core_->first_line_==0) && (core_->first_unchecked_==0) && (core_->lines_.size()==0);
}

const eng::string &PrintBuffer::nth(int n) const {
    return core_->lines_[n - core_->first_line_];
}

eng::string PrintBuffer::debug_string() const {
    eng::ostringstream oss;
    oss << core_->first_line_ << "," << core_->first_unchecked_ << ":";
    for (int i = 0; i < int(core_->lines_.size()); i++) {
        oss << core_->lines_[i] << ";";
    }
    return oss.str();
}

void PrintBuffer::clear() {
    if (core_ != &empty_core_) {
        delete core_;
        core_ = &empty_core_;
    }
}

static int first_line_len(const char *text, int len) {
    for (int i = 0; i < len; i++) {
        if (text[i] == '\n') return i;
    }
    return len;
}

static void add_line(PrintBufferCore *core, const char *text, int len, bool auth) {
    assert(core != &empty_core_);
    core->lines_.emplace_back(text, len);
    if (auth) {
        core->first_unchecked_ = core->first_line_ + int(core->lines_.size());
    }
}

void PrintBuffer::add_string(const eng::string &s, bool auth) {
    const char *text = s.c_str();
    int len = s.size();
    if (core_ == &empty_core_) core_ = new PrintBufferCore;
    while (true) {
        int fll = first_line_len(text, len);
        if (fll == len) {
            if (len > 0) {
                add_line(core_, text, len, auth);
            }
            return;
        } else {
            add_line(core_, text, fll, auth);
            text += (fll + 1);
            len -= (fll + 1);
        }
    }
}

void PrintBuffer::discard_upto(int n) {
    if (core_ == &empty_core_) return;
    while ((!core_->lines_.empty()) && (core_->first_line_ < n)) {
        core_->lines_.pop_front();
        core_->first_line_ += 1;
    }
    if (core_->first_unchecked_ < core_->first_line_) {
        core_->first_unchecked_ = core_->first_line_;
    }
}

void PrintBuffer::serialize(StreamBuffer *sb) const {
    if (never_printed()) {
        sb->write_bool(true);
    } else {
        sb->write_bool(false);
        sb->write_uint32(core_->first_line_);
        sb->write_uint32(core_->first_unchecked_);
        sb->write_uint32(core_->lines_.size());
        for (const eng::string &s : core_->lines_) {
            sb->write_string(s);
        }
    }
}

void PrintBuffer::deserialize(StreamBuffer *sb) {
    bool never_printed = sb->read_bool();
    if (never_printed) {
        clear();
    } else {
        if (core_ == &empty_core_) core_ = new PrintBufferCore;
        core_->first_line_ = sb->read_uint32();
        core_->first_unchecked_ = sb->read_uint32();
        int nlines = sb->read_uint32();
        core_->lines_.clear();
        for (int i = 0; i < nlines; i++) {
            core_->lines_.push_back(sb->read_string());
        }
    }
}

void PrintBuffer::diff(const PrintBuffer &auth, StreamBuffer *sb) const {
    // Currently, the implementation is simple.  The synchronous model discards
    // all prediction lines from its buffer, then the server retransmits all
    // those lines.  So this barely counts as difference transmission - it's
    // just transmission, regardless of what was in the synchronous model.  I
    // think that's okay for the text console.
    // Ask the client to discard anything that precedes auth_first.
    sb->write_int32(auth.first_line());
    sb->write_int32(auth.last_line());
    for (int i = core_->first_unchecked_; i < auth.last_line(); i++) {
        eng::string line;
        if (i >= auth.first_line()) line = auth.nth(i);
        sb->write_string(line);
    }
}

void PrintBuffer::patch(StreamBuffer *sb, DebugCollector *dbc) {
    DebugBlock dbb(dbc, "PrintBuffer::patch");
    if (core_ == &empty_core_) core_ = new PrintBufferCore;
    int auth_first = sb->read_int32();
    int auth_last = sb->read_int32();
    core_->lines_.resize(core_->first_unchecked_ - core_->first_line_);
    int nlines = auth_last - core_->first_unchecked_;
    if (nlines > 0) DebugLine(dbc) << "PrintBuffer received " << nlines << " lines.";
    for (int i = core_->first_unchecked_; i < auth_last; i++) {
        core_->lines_.emplace_back(sb->read_string());
    }
    core_->first_unchecked_ = core_->first_line_ + core_->lines_.size();
    discard_upto(auth_first);
    if (core_->first_line_ < auth_first) {
        core_->first_line_ = auth_first;
    }
}

bool PrintChanneler::have_prints(const PrintBuffer *printbuffer) const
{
    return ((printbuffer != nullptr) &&
            (printbuffer->first_line() < printbuffer->first_unchecked()) &&
            (line_ < printbuffer->first_unchecked()));
}

bool PrintChanneler::channel(const PrintBuffer *printbuffer, StreamBuffer *sb) {
    if (printbuffer == nullptr) return false;
    if (printbuffer->first_line() > line_) {
        line_ = printbuffer->first_line();
    }
    bool any = false;
    while (line_ < printbuffer->first_unchecked()) {
        sb->write_bytes(printbuffer->nth(line_));
        sb->write_bytes("\n");
        line_ += 1;
        any = true;
    }
    return any;
}

Invocation PrintChanneler::invocation(int64_t actor_id) {
    char buf[80];
    sprintf(buf, "%" PRId64, line_);
    return Invocation(AccessKind::INVOKE_FLUSH_PRINTS, actor_id, actor_id, buf);
}

LuaDefine(unittests_printbuffer, "", "some unit tests") {
    PrintBuffer pbm;
    PrintBuffer pbs;
    StreamBuffer sb;

    // Test authoritative add_string and discard_upto.
    LuaAssertStrEq(L, pbm.debug_string(), "0,0:");
    pbm.add_string("foo\nbar\nbaz\n", true);
    LuaAssertStrEq(L, pbm.debug_string(), "0,3:foo;bar;baz;");
    pbm.add_string("a\nb\nc", true);
    LuaAssertStrEq(L, pbm.debug_string(), "0,6:foo;bar;baz;a;b;c;");
    pbm.discard_upto(2);
    LuaAssertStrEq(L, pbm.debug_string(), "2,6:baz;a;b;c;");
    pbm.discard_upto(8);
    LuaAssertStrEq(L, pbm.debug_string(), "6,6:");

    // Test nonauthoritative add_string and discard_upto.
    LuaAssertStrEq(L, pbs.debug_string(), "0,0:");
    pbs.add_string("foo\nbar\nbaz\n", false);
    LuaAssertStrEq(L, pbs.debug_string(), "0,0:foo;bar;baz;");
    pbs.add_string("a\nb\nc", false);
    LuaAssertStrEq(L, pbs.debug_string(), "0,0:foo;bar;baz;a;b;c;");
    pbs.discard_upto(2);
    LuaAssertStrEq(L, pbs.debug_string(), "2,2:baz;a;b;c;");
    pbs.discard_upto(8);
    LuaAssertStrEq(L, pbs.debug_string(), "6,6:");

    // Test serialization and deserialization of a nonempty printbuffer.
    pbm.clear();
    sb.clear();
    pbm.add_string("boy\nhowdy\nyeah\n", true);
    pbm.add_string("baby\nget\ndown\n", false);
    LuaAssertStrEq(L, pbm.debug_string(), "0,3:boy;howdy;yeah;baby;get;down;");
    pbm.serialize(&sb);
    pbs.deserialize(&sb);
    LuaAssertStrEq(L, pbs.debug_string(), "0,3:boy;howdy;yeah;baby;get;down;");

    // Test serialization and deserialization of an empty printbuffer.
    pbm.clear();
    sb.clear();
    LuaAssert(L, pbm.never_printed());
    pbm.serialize(&sb);
    pbs.deserialize(&sb);
    LuaAssert(L, pbs.never_printed());
    
    pbm.clear();
    pbs.clear();
    sb.clear();
    pbm.add_string("foo\nbar\n", true);
    pbs.diff(pbm, &sb);
    pbs.patch(&sb, nullptr);
    LuaAssertStrEq(L, pbs.debug_string(), "0,2:foo;bar;");
    pbm.clear();
    pbm.add_string("foo\nyow\nding\ndong\n", true);
    pbs.diff(pbm, &sb);
    pbs.patch(&sb, nullptr);
    LuaAssertStrEq(L, pbs.debug_string(), "0,4:foo;bar;ding;dong;");
    pbs.discard_upto(2);
    LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;");
    pbs.diff(pbm, &sb);
    pbs.patch(&sb, nullptr);
    LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;");
    pbs.add_string("boy\nhowdy\n", false);
    LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;boy;howdy;");
    pbs.diff(pbm, &sb);
    pbs.patch(&sb, nullptr);
    LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;");
    pbs.add_string("boy\nhowdy\nyeah\nbaby\nget\ndown\n", false);
    LuaAssertStrEq(L, pbs.debug_string(), "2,4:ding;dong;boy;howdy;yeah;baby;get;down;");
    pbs.discard_upto(5);
    LuaAssertStrEq(L, pbs.debug_string(), "5,5:howdy;yeah;baby;get;down;");
    pbs.diff(pbm, &sb);
    pbs.patch(&sb, nullptr);
    LuaAssertStrEq(L, pbs.debug_string(), "5,5:");
    

    return 0;
}
