#define _USE_MATH_DEFINES

#include "wrap-sstream.hpp"
#include "wrap-map.hpp"
#include "util.hpp"
#include "animqueue.hpp"
#include "luastack.hpp"
#include "streambuffer.hpp"

#include <limits>
#include <cmath>
#include <cstdlib>


static int64_t hash_encstep(int64_t prev, std::string_view s) {
    // We drop the most significant bit to ensure that the value
    // can be represented as a nonnegative int64.
    return int64_t(0x7FFFFFFF & util::hash_string(util::HashValue(123, prev), s).first);
}

// Parse a value.  This is meant for unit testing only.  The
// parser isn't powerful enough to express all possible values.
static void parse_value(std::string_view vstr, AnimValue *v) {
    // Try to interpret vstr as a boolean.
    bool is_true = (vstr == "true");
    bool is_false = (vstr == "false");
    if (is_true || is_false) {
        v->set_boolean(is_true);
        return;
    }
    // Try to interpret vstr as a number.
    if (sv::valid_number(vstr, true, true, true, false)) {
        v->set_number(std::atof(std::string(vstr).c_str()));
        return;
    }
    // Try to interpret vstr as a vector.
    eng::vector<eng::string> parts = util::split(eng::string(vstr), ',');
    if ((parts.size() == 3) &&
        (sv::valid_number(parts[0], true, true, true, false)) &&
        (sv::valid_number(parts[1], true, true, true, false)) &&
        (sv::valid_number(parts[2], true, true, true, false))) {
        double x = std::atof(parts[0].c_str());
        double y = std::atof(parts[1].c_str());
        double z = std::atof(parts[2].c_str());
        v->set_dxyz(util::DXYZ(x,y,z));
        return;
    }
    // If it doesn't parse as any of the above, it's a string.
    v->set_string(vstr);
}


static AnimValue parse_anim_value(LuaCoreStack &LS, LuaSlot val) {
    AnimValue result;
    int type = LS.type(val);
    switch (type) {
        case LUA_TBOOLEAN: {
            auto tbool = LS.tryboolean(val);
            if (tbool) result.set_boolean(*tbool);
        }
        case LUA_TNUMBER: {
            auto tnum = LS.trynumber(val);
            if (tnum) result.set_number(*tnum);
        }
        case LUA_TSTRING: {
            auto tstr = LS.trystringview(val);
            if (tstr) result.set_string(*tstr);
        }
        case LUA_TTABLE: {
            auto txyz = LS.tryxyz(val);
            if (txyz) result.set_dxyz(*txyz);
        }
        case LUA_TLIGHTUSERDATA: {
            auto ttoken = LS.trytoken(val);
            if (ttoken) result.set_token(*ttoken);
        }
    }
    return result;
}

void AnimStepEditor::set_persistent(const eng::string &name) {
    map_[name].persistent = true;
}

void AnimStepEditor::print_debug_string(eng::ostringstream &oss) {
    bool first = true;
    if (map_.empty()) {
        oss << "[empty]";
    }
    for (const auto &pair : map_) {
        if (!first) oss << " ";
        const eng::string &name = pair.first;
        const AnimValue &value = pair.second;
        oss << name;
        if (value.persistent) {
            oss << "=";
        } else {
            oss << ":";
        }
        switch (value.type) {
            case LuaValueType::UNINITIALIZED: oss << "UNINITIALIZED"; break;
            case LuaValueType::STRING: oss << value.s; break;
            case LuaValueType::TOKEN: oss << "[" << value.s << "]"; break;
            case LuaValueType::NUMBER: oss << value.x; break;
            case LuaValueType::BOOLEAN: oss << ((value.x == 1.0) ? "true":"false"); break;
            case LuaValueType::VECTOR: oss << value.x << "," << value.y << "," << value.z; break;
            default: assert(false);
        }
        first = false;
    }
}

eng::string AnimStepEditor::debug_string() {
    eng::ostringstream oss;
    print_debug_string(oss);
    return oss.str();
}

eng::string AnimStepEditor::encode() const {
    StreamBuffer sb;
    for (const auto &pair : map_) {
        const eng::string &name = pair.first;
        const AnimValue &value = pair.second;
        sb.write_string(name);
        sb.write_bool(value.persistent);
        sb.write_lua_value(value);
    }
    return eng::string(sb.view());
}

void AnimStepEditor::decode(std::string_view s) {
    map_.clear();
    StreamBuffer sb(s);
    while (!sb.empty()) {
        eng::string name = sb.read_string();
        AnimValue &value = map_[name];
        value.persistent = sb.read_bool();
        sb.read_lua_value(&value);
    }
}

void AnimStepEditor::decode_persistent(std::string_view s) {
    map_.clear();
    StreamBuffer sb(s);
    AnimValue dummy;
    while (!sb.empty()) {
        eng::string name = sb.read_string();
        bool persistent = sb.read_bool();
        if (persistent) {
            AnimValue &value = map_[name];
            value.persistent = persistent;
            sb.read_lua_value(&value);
        } else {
            sb.read_lua_value(&dummy);
        }
    }
}

eng::string AnimStepEditor::add_default(const eng::string &name, const AnimValue &def, const AnimStepEditor *other) {
    AnimValue &value = map_[name];
    value.persistent = true;
    if (value.type == LuaValueType::UNINITIALIZED) {
        if (other != nullptr) {
            auto otheriter = other->map_.find(name);
            if (otheriter != other->map_.end()) {
                if (otheriter->second.persistent && otheriter->second.type == def.type) {
                    value.copy_value(otheriter->second);
                    return "";
                }
            }
        }
        value.copy_value(def);
        return "";
    }
    if (value.type != def.type) {
        return util::ss("Animation key ", name, " must be a ", def.type_name());
    }
    return "";
}

eng::string AnimStepEditor::add_defaults(const AnimStepEditor *other) {
    eng::string err;
    AnimValue defval;

    defval.set_dxyz(util::DXYZ(0,0,0));
    err = add_default("xyz", defval, other);
    if (!err.empty()) return err;

    defval.set_string("nowhere");
    err = add_default("plane", defval, other);
    if (!err.empty()) return err;
    
    defval.set_number(0.0);
    err = add_default("facing", defval, other);
    if (!err.empty()) return err;
    
    defval.set_string("unknown");
    err = add_default("bp", defval, other);
    if (!err.empty()) return err;

    return "";
}


eng::string AnimStepEditor::from_lua(LuaCoreStack &LS0, LuaSlot tab, bool persistent, bool allowauto) {
    LuaVar key, val;
    LuaExtStack LS(LS0.state(), key, val);
    util::DXYZ xyz;
    
    clear();
    if (!LS.istable(tab)) {
        return "A lua animstate must be a table.";
    }
    LS.set(key, LuaNil);
    while (LS.next(tab, key, val)) {
        if (!LS.isstring(key)) {
            return "in animation key-value pairs, key must be a string.";
        }
        eng::string name = LS.ckstring(key);
        if (!sv::is_lua_id(name)) {
            return "in animation key-value pairs, key must be a valid lua identifier.";
        }
        AnimValue parsedvalue = parse_anim_value(LS, val);
        if (parsedvalue.type == LuaValueType::UNINITIALIZED) {
            return "in animation key-value pairs, value must be string, token, number, boolean, or xyz";
        }
        if (parsedvalue.is_token("auto") && !allowauto) {
            return "in animation key-value pairs, value must not be [auto] here.";
        }
        AnimValue &mapentry = map_[name];
        mapentry.copy_value(parsedvalue);
        mapentry.persistent = persistent;
    }
    return "";
}

eng::string AnimStepEditor::merge(const AnimStepEditor &previous, const AnimStepEditor &update) {
    // Copy everything over from the previous entry.
    map_ = previous.map_;

    for (const auto &pair : update.map_) {
        const eng::string &name = pair.first;
        AnimValue &dst = map_[name];
        const AnimValue &src = pair.second;

        // Handle autocalculation rules.
        if (src.is_token("auto")) {
            if (name == "facing") {
                if (!dst.persistent || dst.type != LuaValueType::NUMBER) {
                    return "Cannot auto-calculate facing because facing has not been specified as a persistent number";
                }
                const auto xyz_previous = previous.map_.find("xyz");
                const auto xyz_update = update.map_.find("xyz");
                if ((xyz_previous == previous.map_.end()) ||
                    (xyz_update == update.map_.end()) ||
                    (xyz_previous->second.type != LuaValueType::VECTOR) ||
                    (xyz_update->second.type != LuaValueType::VECTOR)) {
                    return "Cannot auto-calculate facing because before/after xyz coordinates are not present";
                }
                double dx = xyz_update->second.x - xyz_previous->second.x;
                double dy = xyz_update->second.y - xyz_previous->second.y;
                // If dx and dy are both zero, leave the facing unmodified.
                if ((dx != 0.0) || (dy != 0.0)) {
                    double facing = atan2(dy, dx) * 180.0 / M_PI;
                    dst.set_number(facing);
                }
            } else {
                return util::ss("No rule to automatically calculate ", name);
            }
            continue;
        }

        if (dst.persistent && (src.type != dst.type)) {
            return util::ss("Wrong data type for ", name, ", should be ", dst.type_name());
        }
        dst.copy_value(src);
    }
    return "";
}


void AnimStepEditor::to_lua(LuaCoreStack &LS0, LuaSlot tab, bool transient, bool persistent) {
    LuaVar name, val;
    LuaExtStack LS(LS0.state(), name, val);
    LS.newtable(tab);
    for (const auto &pair : map_) {
        if (pair.second.persistent) {
            if (!persistent) continue;
        } else {
            if (!transient) continue;
        }
        LS.set(name, pair.first);
        const AnimValue &value = pair.second;
        if (value.type == LuaValueType::BOOLEAN) {
            LS.set(val, (value.x == 1.0));
        } else if (value.type == LuaValueType::NUMBER) {
            LS.set(val, value.x);
        } else if (value.type == LuaValueType::STRING) {
            LS.set(val, std::string_view(value.s));
        } else if (value.type == LuaValueType::TOKEN) {
            LS.set(val, LuaToken(value.s));
        } else if (value.type == LuaValueType::VECTOR) {
            LS.newtable(val);
            LS.rawset(val, 1, value.x);
            LS.rawset(val, 2, value.y);
            LS.rawset(val, 3, value.z);
        }
        LS.rawset(tab, name, val);
    }
}


// The syntax used by this parser is not general enough to represent all
// possible strings.  That's OK, though, since it's just for unit testing.
void AnimStepEditor::parse(std::string_view config) {
    while (true) {
        config = sv::ltrim(config);
        if (config.empty()) break;
        eng::string name(sv::read_simple_identifier(config));
        assert(!name.empty());
        AnimValue &value = map_[name];
        bool has_equal = sv::has_prefix(config, "=");
        bool has_colon = sv::has_prefix(config, ":");
        assert(has_equal || has_colon);
        config.remove_prefix(1);
        value.persistent = has_equal;
        eng::string vstr(sv::read_to_space(config));
        parse_value(vstr, &value);
    }
}

void AnimStepEditor::clear_and_parse(std::string_view config) {
    map_.clear();
    parse(config);
}

void AnimCoreState::decode(std::string_view s) {
    plane.clear();
    xyz = 0.0;
    StreamBuffer sb(s);
    AnimValue value;
    while (!sb.empty()) {
        eng::string name = sb.read_string();
        bool persistent = sb.read_bool();
        sb.read_lua_value(&value);
        if (persistent) {
            if ((name == "xyz") && (value.type == LuaValueType::VECTOR)) xyz = util::DXYZ(value.x, value.y, value.z);
            if ((name == "plane") && (value.type == LuaValueType::STRING)) plane = value.s;
        }
    }
}

int AnimQueue::get_size_limit() const {
    if (encqueue_ == nullptr) return 0;
    StreamBuffer sb(*encqueue_);
    return sb.read_uint8();
}

int AnimQueue::get_actual_size() const {
    if (encqueue_ == nullptr) return 0;
    StreamBuffer sb(*encqueue_);
    sb.read_bytes(1);
    return sb.read_uint8();
}

int64_t AnimQueue::get_final_hash() const {
    if (encqueue_ == nullptr) return 0;
    StreamBuffer sb(*encqueue_);
    sb.read_bytes(2);
    return sb.read_uint64();
}

std::string_view AnimQueue::get_final_encstep() const {
    if (encqueue_ == nullptr) return std::string_view();
    StreamBuffer sb(*encqueue_);
    sb.read_bytes(10);
    return sb.read_string_view();
}

AnimQueue::QueueRange AnimQueue::get_range(int lo, int hi) {
    // Clamp lo and hi to the valid range (0 to actual_size).
    //
    int actual_size = get_actual_size();
    if (lo < 0) lo = 0;
    if (hi > actual_size) hi = actual_size;

    // Abort early if the range is empty.  This avoids several edge cases.
    //
    if (lo >= hi) return QueueRange(0, std::string_view());

    // Get the entries.
    //
    std::string_view queueview(*encqueue_);
    StreamBuffer sb(queueview);
    sb.read_bytes(2); // Skip over the header.
    for (int i = 0; i < lo; i++) {
        sb.read_uint64();
        sb.read_string_view();
    }
    int pos1 = sb.total_reads();
    for (int i = lo; i < hi; i++) {
        sb.read_uint64();
        sb.read_string_view();
    }
    int pos2 = sb.total_reads();
    return QueueRange(hi-lo, queueview.substr(pos1, pos2 - pos1));
}

int64_t AnimQueue::hash_encstep(const QueueRange &prev, std::string_view s) {
    int64_t prev_hash = 0;
    if (prev.size > 0) {
        StreamBuffer retsb(prev.entries);
        prev_hash = retsb.read_int64();
    }
    return ::hash_encstep(prev_hash, s);
}

void AnimQueue::update_encqueue(int limit, bool add, std::string_view add_enc, int keeplo, int keephi) {
    // Get the retained entries.
    QueueRange keeprange = get_range(keeplo, keephi);
    
    // Encode everything into a binary blob.
    StreamBuffer result;
    result.write_uint8(limit);
    result.write_uint8(keeprange.size + (add ? 1:0));
    if (add) {
        int64_t add_hash = hash_encstep(keeprange, add_enc);
        result.write_uint64(add_hash);
        result.write_string(add_enc);
    }
    result.write_bytes(keeprange.entries);

    // Replace the shared string.
    encqueue_ = std::make_shared<std::string>(result.view());
}

AnimQueue::AnimQueue() {
    update_encqueue(10, true, AnimStepEditor().encode(), 0, 0);
}

void AnimQueue::clear(const AnimStepEditor &state) {
    update_encqueue(get_size_limit(), true, state.encode(), 0, 0);
}

void AnimQueue::clear() {
    update_encqueue(get_size_limit(), true, AnimStepEditor().encode(), 0, 0);
}

void AnimQueue::set_limit(int limit) {
    assert((limit >= 2) && (limit <= 250));
    update_encqueue(limit, false, std::string_view(), 0, limit);
}

void AnimQueue::add(const AnimStepEditor &state) {
    int limit = get_size_limit();
    update_encqueue(limit, true, state.encode(), 0, limit - 1);
}

void AnimQueue::replace(const AnimStepEditor &state) {
    int limit = get_size_limit();
    update_encqueue(limit, true, state.encode(), 1, limit);
}

void AnimQueue::serialize(StreamBuffer *sb) const {
    sb->write_string(*encqueue_);
}

void AnimQueue::deserialize(StreamBuffer *sb) {
    encqueue_ = std::make_shared<std::string>(sb->read_string_view());
}

bool AnimQueue::diff(const AnimQueue &auth, StreamBuffer *sb) const {
    // Fast check for exactly equivalent.  If equivalent, skip all the work.
    if (exactly_equal_fast(auth)) {
        assert(exactly_equal(auth));
        sb->write_bool(false);
        return false;
    }

    // TODO: maybe send less data?
    sb->write_bool(true);
    sb->write_string(*auth.encqueue_);
    return true;
}

void AnimQueue::patch(StreamBuffer *sb, DebugCollector *dbc) {
    bool changed = sb->read_bool();
    if (!changed) {
        return;
    }
    DebugLine(dbc) << "AnimQueue modified";
    encqueue_ = std::make_shared<std::string>(sb->read_string_view());
}

bool AnimQueue::exactly_equal(const AnimQueue &other) const {
    if (*encqueue_ != *other.encqueue_) return false;
    return true;
}

bool AnimQueue::exactly_equal_fast(const AnimQueue &other) const {
    if (encqueue_->size() != other.encqueue_->size()) return false;
    if (encqueue_->compare(0, 10, *other.encqueue_) != 0) return false;
    return true;
}

void AnimQueue::print_debug_string(eng::ostringstream &oss, bool full) const {
    bool first = true;
    // Break out the steps.
    eng::vector<std::string_view> encsteps;
    StreamBuffer sb(*encqueue_); 
    int size_limit = sb.read_uint8();
    int actual_size = sb.read_uint8();
    if (full) {
        oss << "limit=" << size_limit;
        first = false;
    }
    for (int i = 0; i < actual_size; i++) {
        sb.read_uint64();
        encsteps.push_back(sb.read_string_view());
    }
    assert(sb.empty());
    for (int i = encsteps.size() - 1; i >= 0; i --) {
        if (!first) oss << "; ";
        AnimStepEditor state(encsteps[i]);
        state.print_debug_string(oss);
        first = false;
    }
}

eng::string AnimQueue::steps_debug_string() const {
    eng::ostringstream oss;
    print_debug_string(oss, false);
    return oss.str();
}

eng::string AnimQueue::full_debug_string() const {
    eng::ostringstream oss;
    print_debug_string(oss, true);
    return oss.str();
}

AnimCoreState AnimQueue::get_final_core_state() const {
    std::string_view encstep = get_final_encstep();
    AnimCoreState result;
    result.decode(encstep);
    return result;
}

AnimStepEditor AnimQueue::get_final_persistent() const {
    std::string_view encstep = get_final_encstep();
    AnimStepEditor result;
    result.decode_persistent(encstep);
    return result;
}

AnimStepEditor AnimQueue::get_final_everything() const {
    std::string_view encstep = get_final_encstep();
    AnimStepEditor result;
    result.decode(encstep);
    return result;
}

LuaDefine(unittests_animqueue, "", "some unit tests") {
    // Useful objects.
    AnimQueue aq, aqs;
    StreamBuffer sb;
    AnimStepEditor astate;
    eng::string enc;
    AnimCoreState core;
    
    // Debug string of a newly initialized queue
    LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; [empty]");
    
    // Test AnimStepEditor simple setters.
    astate.set_string("color", "blue");
    astate.set_dxyz("xyz", util::DXYZ(1,2,3));
    astate.set_number("half", 0.5);
    astate.set_boolean("nice", true);
    LuaAssertStrEq(L, astate.debug_string(), "color:blue half:0.5 nice:true xyz:1,2,3");

    // // Test AnimStepEditor simple getters.
    // LuaAssert(L, astate.get_string("color") == "blue");
    // LuaAssert(L, astate.get_xyz("xyz") == util::DXYZ(1,2,3));
    // LuaAssert(L, astate.get_number("half") == 0.5);
    // LuaAssert(L, astate.get_boolean("nice") == true);

    // // Test AnimStepEditor simple getters on nonexistent data.
    // LuaAssert(L, astate.get_string("q") == "");
    // LuaAssert(L, astate.get_xyz("q") == util::DXYZ(0,0,0));
    // LuaAssert(L, astate.get_number("q") == 0.0);
    // LuaAssert(L, astate.get_boolean("q") == false);

    // // Test AnimStepEditor simple getters on wrong-type data.
    // LuaAssert(L, astate.get_string("half") == "");
    // LuaAssert(L, astate.get_xyz("half") == util::DXYZ(0,0,0));
    // LuaAssert(L, astate.get_number("color") == 0.0);
    // LuaAssert(L, astate.get_boolean("color") == false);

    // Test AnimStepEditor persistence manipulation.
    astate.set_persistent("color");
    astate.set_persistent("nice");
    LuaAssertStrEq(L, astate.debug_string(), "color=blue half:0.5 nice=true xyz:1,2,3");

    // Test AnimStepEditor parser.
    astate.clear_and_parse("color:green mean=true pos=3,4,5 ok:false");
    LuaAssertStrEq(L, astate.debug_string(), "color:green mean=true ok:false pos=3,4,5");

    // Test animstate encoding and decoding.
    astate.clear_and_parse("color:green mean=true pos=3,4,5 ok:false");
    enc = astate.encode();
    astate.clear();
    astate.decode(enc);
    LuaAssertStrEq(L, astate.debug_string(), "color:green mean=true ok:false pos=3,4,5");
    astate.decode_persistent(enc);
    LuaAssertStrEq(L, astate.debug_string(), "mean=true pos=3,4,5");
    
    // Test AnimCoreState.decode
    //
    astate.clear_and_parse("color=blue xyz=1,2,3 plane=banana chicken=3");
    core.decode(astate.encode());
    LuaAssert(L, core.plane == "banana");
    LuaAssert(L, core.xyz == util::DXYZ(1,2,3));

    // Verify that a newly-constructed AnimQueue is in a reasonable default state.
    //
    LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; [empty]");

    // Clear an AnimQueue to a specified initial state.
    //
    astate.clear_and_parse("color=blue xyz=1,2,3 plane=somewhere");
    aq.clear(astate);
    LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; color=blue plane=somewhere xyz=1,2,3");

    // Add animation steps to animation queue.
    // Note: each step is independent of the previous one, no composition is being done.
    //
    astate.clear_and_parse("xyz=1,2,3 plane=earth");
    aq.clear(astate);
    LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3");
    astate.clear_and_parse("xyz=4,5,6 action:jump");
    aq.add(astate);
    LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6");
    astate.clear_and_parse("plane=moon airline:southwest");
    aq.add(astate);
    LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon");
    astate.clear_and_parse("color=blue");
    aq.add(astate);    
    LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");

    // Try reducing the animation queue size limit.
    //
    aq.set_limit(2);
    LuaAssertStrEq(L, aq.full_debug_string(), "limit=2; airline:southwest plane=moon; color=blue");

    // Test get_final_persistent, get_final_everything, get_final_core_state
    //
    astate.clear_and_parse("action:jump plane=earth xyz=1,2,3 bouncy:true");
    aq.clear(astate);
    astate = aq.get_final_persistent();
    LuaAssertStrEq(L, astate.debug_string(), "plane=earth xyz=1,2,3");
    astate = aq.get_final_everything();
    LuaAssertStrEq(L, astate.debug_string(), "action:jump bouncy:true plane=earth xyz=1,2,3");
    core = aq.get_final_core_state();
    LuaAssert(L, core.plane == "earth");
    LuaAssert(L, core.xyz == util::DXYZ(1,2,3));

    // Serialize a queue.
    //
    aq.set_limit(10);
    astate.clear_and_parse("xyz=1,2,3 plane=earth");
    aq.clear(astate);
    astate.clear_and_parse("xyz=4,5,6 action:jump");
    aq.add(astate);
    astate.clear_and_parse("plane=moon airline:southwest");
    aq.add(astate);
    astate.clear_and_parse("color=blue");
    aq.add(astate);    
    LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
    aq.serialize(&sb);
    
    // Deserialize a queue.
    //
    aqs.set_limit(7);
    aqs.clear();
    LuaAssert(L, !aqs.exactly_equal(aq));
    aqs.deserialize(&sb);
    LuaAssert(L, aqs.exactly_equal(aq));
    LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
    
    // Test diff and patch.
    //
    LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
    aqs.set_limit(7);
    aqs.clear();
    sb.clear();
    aqs.diff(aq, &sb);
    //int difflen1 = sb.fill();
    LuaAssert(L, !aqs.exactly_equal(aq));
    aqs.patch(&sb, nullptr);
    LuaAssert(L, aqs.exactly_equal(aq));
    LuaAssertStrEq(L, aqs.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
    
    // Test that diff and patch are more efficient when the two queues contain some shared steps.
    //
    LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");
    astate.clear_and_parse("xyz=4,5,6 action:jump");
    aqs.clear(astate);
    astate.clear_and_parse("plane=earth xyz=1,2,3");
    aqs.add(astate);
    astate.clear_and_parse("plane=moon airline:southwest");
    aqs.add(astate);
    LuaAssertStrEq(L, aqs.full_debug_string(), "limit=10; action:jump xyz=4,5,6; plane=earth xyz=1,2,3; airline:southwest plane=moon");
    sb.clear();
    aqs.diff(aq, &sb);
    //int difflen2 = sb.fill();
    LuaAssert(L, !aqs.exactly_equal(aq));
    aqs.patch(&sb, nullptr);
    LuaAssert(L, aqs.exactly_equal(aq));
    LuaAssertStrEq(L, aqs.full_debug_string(), "limit=10; plane=earth xyz=1,2,3; action:jump xyz=4,5,6; airline:southwest plane=moon; color=blue");

    // TODO: if we make the diff routine more efficient, this should be true.
    // LuaAssert(L, difflen2 < (difflen1 / 2));

    return 0;
}
