
#include "luastack.hpp"
#include "util.hpp"
#include "planemap.hpp"

#include <algorithm>
#include <cmath>


// Cell X, Y coordinates are packed such that they have 24 bits for X and Y.
// A cell is 10 Meters square.
// Cell ID zero is used to represent an invalid position.
//
#define CELL_LIMIT 0x7FFFFF
#define CELL_SCALE 10.0

// Round a float and return as integer.  Clamp result to the specified range.
static int round_and_clamp(float x, int lo, int hi) {
    x = round(x);
    if (x < lo) return lo;
    if (x > hi) return hi;
    return int(x);
}

// A cell range is inclusive.
struct CellRange {
    int xlo;
    int ylo;
    int xhi;
    int yhi;

    bool equal(int xl, int yl, int xh, int yh) {
        return ((xlo==xl)&&(ylo==yl)&&(xh==xhi)&&(yh==yhi));
    }
};

// Get the range of cells that includes everything in the rectangle.
//
// Gracefully handles the case that some or all of the rectangle is
// beyond the maximum cell range.  In that case, it clamps to the edge
// of the cell range.
//
static CellRange rect_cell_range(float x1, float y1, float x2, float y2) {
    CellRange result;
    result.xlo = round_and_clamp(x1 / CELL_SCALE, -CELL_LIMIT, CELL_LIMIT);
    result.ylo = round_and_clamp(y1 / CELL_SCALE, -CELL_LIMIT, CELL_LIMIT);
    result.xhi = round_and_clamp(x2 / CELL_SCALE, -CELL_LIMIT, CELL_LIMIT);
    result.yhi = round_and_clamp(y2 / CELL_SCALE, -CELL_LIMIT, CELL_LIMIT);
    return result;
}

static int64_t cell_id(int64_t cellx, int64_t celly) {
    int64_t icellx = cellx & 0xFFFFFF;
    int64_t icelly = celly & 0xFFFFFF;
    return 0x0001000000000000 | (icellx << 24) | (icelly << 0);
}

// Get the cell ID of the specified point
static int64_t point_cell_id(float x, float y) {
    float cellx = round_and_clamp(x / CELL_SCALE, -CELL_LIMIT, CELL_LIMIT);
    float celly = round_and_clamp(y / CELL_SCALE, -CELL_LIMIT, CELL_LIMIT);
    return cell_id(int64_t(cellx), int64_t(celly));
}

void PlaneMap::remove(const eng::string &plane, int64_t cellid, PlaneItem *client) {
    auto piter = planes_.find(plane);
    if (piter == planes_.end()) {
        return;
    }
    Plane &p = piter->second;
    auto liter = p.find(cellid);
    if (liter == p.end()) {
        return;
    }
    EltVec &l = liter->second;
    l.erase(std::remove(l.begin(), l.end(), client), l.end());
    if (l.empty()) {
        p.erase(liter);
    }
    if (p.empty()) {
        planes_.erase(piter);
    }
}

void PlaneMap::insert(const eng::string &plane, int64_t cellid, PlaneItem *client) {
    Plane &p = planes_[plane];
    EltVec &l = p[cellid];
    l.push_back(client);
}

void PlaneItem::set_pos(const eng::string &plane, float x, float y, float z) {
    int64_t old_cell = point_cell_id(x_, y_);
    int64_t new_cell = point_cell_id(x, y);

    // Update the grid.
    if (pmap_ != 0) {
        if ((plane_ != plane) || (old_cell != new_cell)) {
            pmap_->remove(plane_, old_cell, this);
            pmap_->insert(plane, new_cell, this);
        }
    }

    // Update the client position.
    plane_ = plane;
    x_ = x;
    y_ = y;
    z_ = z;    
}

void PlaneItem::untrack() {
    if (pmap_ != 0) {
        pmap_->remove(plane_, point_cell_id(x_, y_), this);
        pmap_ = 0;
    }
}

void PlaneItem::track(PlaneMap *pmap) {
    if (pmap_ != pmap) {
        untrack();
        pmap->insert(plane_, point_cell_id(x_, y_), this);
        pmap_ = pmap;
    }
}

PlaneItem::PlaneItem() : pmap_(NULL), x_(0.0), y_(0.0), z_(0.0) {
}

PlaneMap::PlaneMap() {
}

PlaneItem::~PlaneItem() {
    untrack();
}

PlaneMap::~PlaneMap() {
    for (const auto &p : planes_) {
        for (const auto &l : p.second) {
            for (PlaneItem *i : l.second) {
                i->pmap_ = NULL;
            }
        }
    }
}

PlaneMap::EltVec PlaneMap::get_cell(const eng::string &plane, int64_t cellid) const {
    PlaneMap::EltVec result;
    auto piter = planes_.find(plane);
    if (piter != planes_.end()) {
        const Plane &p = piter->second;
        auto liter = p.find(cellid);
        if (liter != p.end()) {
            result = liter->second;
        }
    }
    return result;
}

int PlaneMap::total_cells() const {
    int total = 0;
    for (const auto &p : planes_) {
        total += p.second.size();
    }
    return total;
}

PlaneMap::IdVector PlaneMap::scan_radius_unsorted(const eng::string &plane, float x, float y, float radius, bool exclude_nowhere, int64_t special, bool omit) const {
    PlaneMap::IdVector result;
    if ((special != 0)&&(!omit)) {
        result.push_back(special);
    }
    if (exclude_nowhere && (plane == "nowhere")) {
        return result;
    }
    auto piter = planes_.find(plane);
    if (piter != planes_.end()) {
        const Plane &p = piter->second;
        CellRange range = rect_cell_range(x - radius, y - radius, x + radius, y + radius);
        float radsq = radius*radius;
        for (int cy = range.ylo; cy <= range.yhi; cy++) {
            for (int cx = range.xlo; cx <= range.xhi; cx++) {
                auto liter = p.find(cell_id(cx, cy));
                if (liter != p.end()) {
                    for (PlaneItem *client : liter->second) {
                        if (util::distance_squared(client->x(), client->y(), x, y) <= radsq) {
                            if (client->id() != special) {
                                result.push_back(client->id());
                            }
                        }
                    }
                }
            }
        }
    }
    return result;
}

PlaneMap::IdVector PlaneMap::scan_radius(const eng::string &plane, float x, float y, float radius, bool exclude_nowhere, int64_t special, bool omit) const {
    PlaneMap::IdVector result = scan_radius_unsorted(plane, x, y, radius, exclude_nowhere, special, omit);
    if ((special != 0)&&(!omit)) {
        std::sort(result.begin() + 1, result.end());
    } else {
        std::sort(result.begin(), result.end());
    }
    return result;
}

LuaDefine(unittests_planemap, "", "some unit tests") {
    float SC = CELL_SCALE;
    float E = CELL_SCALE * 0.4;
    int LO = -CELL_LIMIT;
    int HI = CELL_LIMIT;
    PlaneMap pm;
    PlaneItem pia, pib;
    PlaneMap::EltVec elts;
    PlaneMap::IdVector ids;

    // Simple test.
    LuaAssert(L, rect_cell_range(-7*SC, -15*SC, 87*SC, 21*SC).equal(-7, -15, 87, 21));

    // Adding an epsilon doesn't change result, if epsilon is less than half of cell scale.
    LuaAssert(L, rect_cell_range(-7*SC+E, -15*SC+E, 87*SC-E, 21*SC-E).equal(-7, -15, 87, 21));
    
    // Rectangle that crosses the high end of the range.
    LuaAssert(L, rect_cell_range((HI-7)*SC, (HI-5)*SC, (HI+3)*SC, (HI+6)*SC).equal(HI-7, HI-5, HI, HI));

    // Rectangle that exceeds the high end of the range.
    LuaAssert(L, rect_cell_range((HI+7)*SC, (HI+5)*SC, (HI+15)*SC, (HI+12)*SC).equal(HI, HI, HI, HI));
    
    // Rectangle that crosses the low end of the range.
    LuaAssert(L, rect_cell_range((LO-7)*SC, (LO-5)*SC, (LO+3)*SC, (LO+4)*SC).equal(LO, LO, LO+3, LO+4));
    
    // Rectangle that exceeds the low end of the range.
    LuaAssert(L, rect_cell_range((LO-15)*SC, (LO-17)*SC, (LO-7)*SC, (LO-5)*SC).equal(LO, LO, LO, LO));

    // Simple test.
    LuaAssert(L, point_cell_id(-7*SC, 15*SC) == cell_id(-7, 15));

    // Adding epsilon doesn't change the result if less than half cell scale.
    LuaAssert(L, point_cell_id(-7*SC+E, 15*SC+E) == cell_id(-7, 15));

    // Right at the top edge of the range.
    LuaAssert(L, point_cell_id(HI*SC, HI*SC) == cell_id(HI, HI));

    // Right at the bottom edge of the range.
    LuaAssert(L, point_cell_id(LO*SC, LO*SC) == cell_id(LO, LO));

    // Beyond various edges.
    LuaAssert(L, point_cell_id((LO-1)*SC, 0) == point_cell_id(LO*SC, 0));
    LuaAssert(L, point_cell_id((HI+1)*SC, 0) == point_cell_id(HI*SC, 0));
    LuaAssert(L, point_cell_id(0, (LO-1)*SC) == point_cell_id(0, LO*SC));
    LuaAssert(L, point_cell_id(0, (HI+1)*SC) == point_cell_id(0, HI*SC));

    // Test using the insert function.
    pm.clear();
    LuaAssert(L, pm.total_cells() == 0);
    pm.insert("foo", 12345, &pia);
    LuaAssert(L, pm.total_cells() == 1);
    pm.insert("foo", 12345, &pib);
    LuaAssert(L, pm.total_cells() == 1);
    elts = pm.get_cell("foo", 12345);
    LuaAssert(L, elts.size() == 2);
    LuaAssert(L, elts[0] == &pia);
    LuaAssert(L, elts[1] == &pib);

    // Test the remove function.
    pm.remove("foo", 12345, &pia);
    LuaAssert(L, pm.total_cells() == 1);
    elts = pm.get_cell("foo", 12345);
    LuaAssert(L, elts.size() == 1);
    LuaAssert(L, elts[0] == &pib);
    pm.remove("foo", 12345, &pib);
    LuaAssert(L, pm.total_cells() == 0);
    
    // Try moving a plane item around without it being tracked to a grid.
    pia.set_pos("foo", 3, 4, 5);
    LuaAssert(L, pia.plane() == "foo");
    LuaAssert(L, pia.x() == 3.0);
    LuaAssert(L, pia.y() == 4.0);
    LuaAssert(L, pia.z() == 5.0);

    // Attach pia to the grid.  This should record it.
    pm.clear();
    pia.track(&pm);
    elts = pm.get_cell("foo", point_cell_id(3.0, 4.0));
    LuaAssert(L, elts.size() == 1);
    LuaAssert(L, elts[0] == &pia);

    // Unattach pia from the grid.  This should unrecord it.
    pia.untrack();
    LuaAssert(L, pm.total_cells() == 0);
    
    // Reattach pia to the grid, then move it.
    pia.track(&pm);
    LuaAssert(L, pm.total_cells() == 1);
    pia.set_pos("bar", 1000.0, 1000.0, 0.0);
    LuaAssert(L, pm.total_cells() == 1);
    elts = pm.get_cell("bar", point_cell_id(1000.0, 1000.0));
    LuaAssert(L, elts.size() == 1);
    LuaAssert(L, elts[0] == &pia);

    // Insert the four elements, then test the scan function.
    pib.track(&pm);
    pia.set_id(123);
    pib.set_id(456);
    pib.set_pos("bar", 1100.0, 1000.0, 0.0);
    ids = pm.scan_radius("bar", 1000.0, 1000.0, 1.0, false, 0, false);
    LuaAssert(L, ids.size() == 1);
    LuaAssert(L, ids[0] == 123);
    ids = pm.scan_radius("bar", 1000.0, 1000.0, 99.9, false, 0, false);
    LuaAssert(L, ids.size() == 1);
    LuaAssert(L, ids[0] == 123);
    ids = pm.scan_radius("bar", 1000.0, 1000.0, 100.0, false, 0, false);
    LuaAssert(L, ids.size() == 2);
    LuaAssert(L, ids[0] == 123);
    LuaAssert(L, ids[1] == 456);

    return 0;
}
