#include "wrap-map.hpp"
#include "wrap-sstream.hpp"
#include "wrap-deque.hpp"

#include "idalloc.hpp"

#include <ostream>


// This is used in some unit tests to make sure that the deque contains the expected values.
static bool ranges_equal(const eng::deque<int64_t> &dq, int64_t a, int64_t b, int64_t c) {
    if (dq.size() != 3) return false;
    if (dq[0] != a) return false;
    if (dq[1] != b) return false;
    if (dq[2] != c) return false;
    return true;
}

IdGlobalPool::IdGlobalPool() {
    salvaged_.clear();
    next_batch_ = 0;
    next_id_ = 0;
    next_seqno_ = 0;
}

IdGlobalPool::~IdGlobalPool() {
}

void IdGlobalPool::init_master() {
    salvaged_.clear();
    next_batch_ = 1000000000000000;
    next_id_    = 5000000000000000;
}

void IdGlobalPool::init_synch() {
    salvaged_.clear();
    next_batch_ = 0;
    next_id_    = 8000000000000000;
}

int64_t IdGlobalPool::get_one() {
    return next_id_++;
}

int64_t IdGlobalPool::get_batch() {
    int64_t batch;
    if (salvaged_.empty()) {
        if (next_batch_ == 0) {
            batch = 0;
        } else {
            batch = next_batch_;
            next_batch_ += 256;
        }
    } else {
        batch = salvaged_.back();
        salvaged_.pop_back();
    }
    return batch;
}

void IdGlobalPool::salvage(int64_t batch) {
    if (batch == 0) return;
    if (next_batch_ == 0) return;
    if ((batch & 0xFF) >= 128) return;
    salvaged_.push_back(batch);
}

void IdGlobalPool::serialize(StreamBuffer *sb) const {
    sb->write_int64(next_batch_);
    sb->write_int64(next_id_);
    sb->write_uint64(next_seqno_);
    sb->write_uint32(salvaged_.size());
    for (int64_t batch : salvaged_) {
        sb->write_int64(batch);
    }
}

void IdGlobalPool::deserialize(StreamBuffer *sb) {
    next_batch_ = sb->read_int64();
    next_id_ = sb->read_int64();
    next_seqno_ = sb->read_uint64();
    uint32_t salvaged_size = sb->read_uint32();
    salvaged_.resize(salvaged_size);
    for (int i=0; i < int(salvaged_size); i++) {
        salvaged_[i] = sb->read_int64();
    }
}

eng::string IdGlobalPool::debug_string() const {
    eng::ostringstream oss;
    oss << "next_batch:" << util::hex64.val(next_batch_) << " ";
    oss << "next_id:" << util::hex64.val(next_id_) << " ";
    oss << "next_seqno: " << util::hex64.val(next_seqno_) << " ";
    oss << "salvaged:";
    for (const int64_t val : salvaged_) {
        oss << " " << util::hex64.val(val);
    }
    return oss.str();
}

IdPlayerPool::IdPlayerPool(IdGlobalPool *g) {
    global_ = g;
    fifo_capacity_ = 0;
    next_seqno_ = 0;
}

IdPlayerPool::~IdPlayerPool() {
}

void IdPlayerPool::set_fifo_capacity(int n) {
    assert((n >= 0) && (n <= 250));
    fifo_capacity_ = n;
    while (int(ranges_.size()) > n) {
        global_->salvage(ranges_.back());
        ranges_.pop_back();
    }
}

void IdPlayerPool::refill() {
    while (int(ranges_.size()) < fifo_capacity_) {
        int64_t batch = global_->get_batch();
        if (batch == 0) break;
        ranges_.push_back(batch);
    }
}

void IdPlayerPool::test_push_back(int64_t range) {
    ranges_.push_back(range);
}

void IdPlayerPool::test_pop_front() {
    ranges_.pop_front();
}

void IdPlayerPool::test_clear_ranges() {
    ranges_.clear();
}

int64_t IdPlayerPool::get_one() {
    refill();
    if (ranges_.empty()) {
        return global_->get_one();
    } else {
        int64_t id = ranges_.front();
        if ((id & 0xFF) == 0xFF) {
            ranges_.pop_front();
            refill();
        } else {
            ranges_.front() = id + 1;
        }
        return id;
    }
}

void IdPlayerPool::serialize(StreamBuffer *sb) const {
    sb->write_uint8(fifo_capacity_);
    sb->write_uint8(ranges_.size());
    sb->write_uint64(next_seqno_);
    for (int64_t batch : ranges_) {
        sb->write_int64(batch);
    }
}

void IdPlayerPool::deserialize(StreamBuffer *sb) {
    fifo_capacity_ = sb->read_uint8();
    int ranges_size = sb->read_uint8();
    next_seqno_ = sb->read_uint64();
    ranges_.resize(ranges_size);
    for (int i=0; i < ranges_size; i++) {
        ranges_[i] = sb->read_int64();
    }
}

bool IdPlayerPool::exactly_equal(const IdPlayerPool &other) const {
    if (fifo_capacity_ != other.fifo_capacity_) return false;
    if (ranges_.size() != other.ranges_.size()) return false;
    if (next_seqno_ != other.next_seqno_) return false;
    for (int i = 0; i < int(ranges_.size()); i++) {
        if (ranges_[i] != other.ranges_[i]) {
            return false;
        }
    }
    return true;
}

bool IdPlayerPool::valid() const {
    if ((fifo_capacity_ < 0) || (fifo_capacity_ > 250)) return false;
    if (int(ranges_.size()) > fifo_capacity_) return false;
    return true;
}

void IdPlayerPool::diff(const IdPlayerPool &auth, StreamBuffer *sb) const {
    assert(valid());
    assert(auth.valid());

    // Special case: there's nothing to fix.
    if (exactly_equal(auth)) {
        sb->write_uint8(255);
        return;
    }

    // Write the fifo capacity, nranges, and next seqno
    assert(auth.fifo_capacity_ != 255);
    sb->write_uint8(auth.fifo_capacity_);
    sb->write_uint8(auth.ranges_.size());
    sb->write_uint64(auth.next_seqno_);
    
    // Build up an index of the known IDs.
    eng::map<int64_t, int> index;
    for (int i = 0; i < int(ranges_.size()); i++) {
        index[ranges_[i]] = i;
    }

    // Write the ranges, but encode known IDs in one byte.
    for (int i = 0; i < int(auth.ranges_.size()); i++) {
        int64_t n = auth.ranges_[i];
        auto iter = index.find(n);
        if (iter == index.end()) {
            sb->write_uint8(255);
            sb->write_int64(n);
        } else {
            int slot = iter->second;
            sb->write_uint8(slot);
        }
    }
}

void IdPlayerPool::patch(StreamBuffer *sb, DebugCollector *dbc) {
    // read the header byte
    int fifo_cap = sb->read_uint8();
    // If the fifo_cap is 255, it means there's nothing to fix.
    if (fifo_cap == 255) {
        return;
    }
    DebugLine(dbc) << "IdPlayerPool modified";
    fifo_capacity_ = fifo_cap;
    int nranges = sb->read_uint8();
    next_seqno_ = sb->read_uint64();
    eng::deque<int64_t> old = std::move(ranges_);
    ranges_.clear();
    for (int i = 0; i < nranges; i++) {
        int index = sb->read_uint8();
        if (index < 255) {
            assert(index < int(old.size()));
            ranges_.push_back(old[index]);
        } else {
            ranges_.push_back(sb->read_int64());
        }
    }
}

eng::string IdPlayerPool::debug_string() const {
    eng::ostringstream oss;
    oss << "cap:" << fifo_capacity_ << " ids:";
    for (int i = 0; i < int(ranges_.size()); i++) {
        if (i > 0) oss << ",";
        oss << util::hex64.val(ranges_[i]);
    }
    oss << " seqno:" << util::hex64.val(next_seqno_);
    return oss.str();
}

static int64_t nthbatch(int64_t n) {
    return int64_t(1000000000000000) + n*256;
}

LuaDefine(unittests_idalloc, "", "some unit tests") {
    IdGlobalPool gp;
    IdPlayerPool pp(&gp);
    IdGlobalPool gpds;
    IdPlayerPool ppds(&gpds);
    StreamBuffer sb;

    // Synchronous pools produce IDs starting at 0x001E000000000000
    gp.init_synch();
    LuaAssert(L, gp.get_one() == 8000000000000000);
    LuaAssert(L, gp.get_one() == 8000000000000001);
    LuaAssert(L, gp.get_one() == 8000000000000002);

    // Master pools produce IDs starting at 0x0010000000000000
    gp.init_master();
    LuaAssert(L, gp.get_one() == 5000000000000000);
    LuaAssert(L, gp.get_one() == 5000000000000001);
    LuaAssert(L, gp.get_one() == 5000000000000002);

    // Synchronous pools produce only null batches.
    gp.init_synch();
    LuaAssert(L, gp.get_batch() == 0);
    LuaAssert(L, gp.get_batch() == 0);
    gp.salvage(nthbatch(5));
    LuaAssert(L, gp.get_batch() == 0);
    
    // Simple fetch batches with a few salvages.
    gp.init_master();
    LuaAssert(L, gp.get_batch() == nthbatch(0));
    LuaAssert(L, gp.get_batch() == nthbatch(1));
    LuaAssert(L, gp.get_batch() == nthbatch(2));
    gp.salvage(nthbatch(182));
    gp.salvage(nthbatch(183));
    LuaAssert(L, gp.get_batch() == nthbatch(183));
    LuaAssert(L, gp.get_batch() == nthbatch(182));
    LuaAssert(L, gp.get_batch() == nthbatch(3));
    
    // Salvage of a zero-batch does nothing.
    gp.init_master();
    LuaAssert(L, gp.get_batch() == nthbatch(0));
    LuaAssert(L, gp.get_batch() == nthbatch(1));
    gp.salvage(0);
    LuaAssert(L, gp.get_batch() == nthbatch(2));

    // Salvage of a partial batch.
    gp.init_master();
    LuaAssert(L, gp.get_batch() == nthbatch(0));
    LuaAssert(L, gp.get_batch() == nthbatch(1));
    gp.salvage(nthbatch(142) + 10);
    LuaAssert(L, gp.get_batch() == nthbatch(142) + 10);
    LuaAssert(L, gp.get_batch() == nthbatch(2));

    // Salvage of a half-empty batch does nothing.
    gp.init_master();
    LuaAssert(L, gp.get_batch() == nthbatch(0));
    LuaAssert(L, gp.get_batch() == nthbatch(1));
    gp.salvage(nthbatch(142) + 145);
    LuaAssert(L, gp.get_batch() == nthbatch(2));

    // In the synchronous model, refill should do nothing.
    // The player pool should shunt through to the global.
    pp.test_clear_ranges();
    gp.init_synch();
    pp.set_fifo_capacity(3);
    pp.refill();
    LuaAssert(L, pp.size() == 0);
    LuaAssert(L, pp.get_one() == 8000000000000000);
    LuaAssert(L, pp.size() == 0);

    // In the master model, with fifo disabled.  Fifo should remain
    // empty, and the player pool should shunt through to the global.
    gp.init_master();
    pp.test_clear_ranges();
    pp.set_fifo_capacity(0);
    pp.refill();
    LuaAssert(L, pp.size() == 0);
    LuaAssert(L, pp.get_one() == 5000000000000000);
    LuaAssert(L, pp.size() == 0);

    // Test refill from master (with enabled fifo).
    gp.init_master();
    pp.test_clear_ranges();
    pp.set_fifo_capacity(3);
    pp.refill();
    LuaAssert(L, ranges_equal(pp.ranges_, nthbatch(0), nthbatch(1), nthbatch(2)));

    // Now test that get_one() keeps the pool filled from master.
    for (int i = 0; i < 256; i++) {
        LuaAssert(L, pp.get_one() == nthbatch(0) + i);
    }
    for (int i = 0; i < 256; i++) {
        LuaAssert(L, pp.get_one() == nthbatch(1) + i);
    }
    LuaAssert(L, ranges_equal(pp.ranges_, nthbatch(2), nthbatch(3), nthbatch(4)));

    // Test unqueueing the batches.
    LuaAssert(L, gp.get_batch() == nthbatch(5));
    LuaAssert(L, gp.get_batch() == nthbatch(6));
    pp.set_fifo_capacity(0);
    LuaAssert(L, gp.get_batch() == nthbatch(2));
    LuaAssert(L, gp.get_batch() == nthbatch(3));
    LuaAssert(L, gp.get_batch() == nthbatch(4));
    LuaAssert(L, gp.get_batch() == nthbatch(7));

    // Serialize and deserialize a global pool.
    gp.init_master();
    gpds.init_master();
    LuaAssert(L, gp.get_one() == 5000000000000000);
    LuaAssert(L, gp.get_batch() == nthbatch(0));
    gp.salvage(nthbatch(182));
    gp.salvage(nthbatch(183));
    gp.serialize(&sb);
    gpds.deserialize(&sb);
    LuaAssert(L, gpds.get_one() == 5000000000000001);
    LuaAssert(L, gpds.get_batch() == nthbatch(183));
    LuaAssert(L, gpds.get_batch() == nthbatch(182));
    LuaAssert(L, gpds.get_batch() == nthbatch(1));

    // Serialize and deserialize a player pool.
    gp.init_master();
    gpds.init_synch();
    LuaAssert(L, gp.get_batch() == nthbatch(0));
    pp.test_clear_ranges();
    pp.set_fifo_capacity(3);
    pp.refill();
    ppds.test_clear_ranges();
    pp.serialize(&sb);
    ppds.deserialize(&sb);
    LuaAssert(L, ppds.get_fifo_capacity()==3);
    LuaAssert(L, ppds.size() == 3);
    LuaAssert(L, ranges_equal(ppds.ranges_, nthbatch(1), nthbatch(2), nthbatch(3)));

    // Difference transmit compare two empty pools.
    gp.init_master();
    gpds.init_master();
    pp.test_clear_ranges();
    ppds.test_clear_ranges();
    pp.set_fifo_capacity(3);
    ppds.set_fifo_capacity(3);
    
    // Check case: no differences.
    sb.clear();
    ppds.diff(pp, &sb);
    ppds.patch(&sb, nullptr);
    LuaAssert(L, ppds.exactly_equal(pp));

    // Add some values to master pool
    pp.test_push_back(123);
    pp.test_push_back(456);

    // transmit and compare.
    sb.clear();
    ppds.diff(pp, &sb);
    ppds.patch(&sb, nullptr);
    LuaAssert(L, ppds.exactly_equal(pp));

    // Pop a value from master pool
    pp.test_pop_front();
    pp.test_push_back(789);

    // transmit and compare.
    sb.clear();
    ppds.diff(pp, &sb);
    ppds.patch(&sb, nullptr);
    LuaAssert(L, ppds.exactly_equal(pp));

    return 0;
}
