//////////////////////////////////////////////////////////////
//
// PLANEMAP
//
// This module defines two classes: PlaneMap, and PlaneItem.  A
// PlaneItem is an object that has a plane (a string) and an
// XYZ position.  A PlaneMap keeps track of potentially thousands
// of PlaneItem objects, allowing you to search for PlaneItems
// by scanning a geographic region.
//
// CLASS SPRITE DERIVES FROM PLANEITEM
//
// A PlaneMap records the positions of PlaneItems.  The intent is
// that class Sprite should derive from PlaneItem.  That way, the
// PlaneMap can record the positions of sprites.
//
// When you put derived types like Sprite into a PlaneMap, the
// PlaneMap will work fine.  However, when you scan the PlaneMap,
// it will give return PlaneItem pointers, not Sprite pointers.
//
// If you're sure that the PlaneMap contains only sprites, then
// you can use static_cast to convert those PlaneItem pointers to
// Sprite pointers.  Sadly, there's no simple way to avoid the casting.
//
// THE SCANNABLE REGION IS FINITE
//
// A plane is a rectangle, with a finite size: if you stray more
// than 80,000,000 meters from the origin, then that PlaneItem
// is beyond the scannable region.
//
// There's nothing stopping you from moving a PlaneItem outside
// the scannable region.  It is not an error to do so.
// However, if you do move a PlaneItem outside the scannable
// region, then that PlaneItem will not show up for scan_radius.
//
// PLANES CANNOT BE DESTROYED BUT THEY CAN BE UNINHABITED
//
// You can't "create" or "destroy" planes.  Instead, we say
// that a plane is "uninhabited" until you put a PlaneItem
// there.  Initially, all possible planes exist, but they're
// all uninhabited until you put a PlaneItem there.  Planes
// that are uninhabited take no memory.
//
// THE NOWHERE PLANE
//
// If you use the literal "nowhere" as a plane name, a special case is
// triggered: you're "nowhere."  Radius scans can never find items
// whose plane is "nowhere."  The same also applies when you use the
// empty string as a plane name.
//
// THE Z COORDINATE IS IGNORED
//
// Class PlaneItem stores X, Y, and Z.  The Z coordinate is
// ignored for all plane-scanning operations.  In other words,
// planes are 2D.  The only reason we store a Z coordinate
// is for the consistency of storing the model's X, Y, and Z
// all in the same place.
//
// MEMORY MANAGEMENT
//
// PlaneMaps do not own PlaneItems.  This is a deliberate choice.
// We assume that sprites will be owned by the sprite ID table.
// So therefore, the PlaneMaps shouldn't own the sprites.
//
// So instead, we use this rule: a PlaneMap has a function 'track'
// that causes it to start tracking the location of a PlaneItem.
// However, the PlaneMap still doesn't own the PlaneItem.
// If you destroy a PlaneMap, all the PlaneItems
// will automatically be untracked, but they won't be deleted.
//
//////////////////////////////////////////////////////////////

#pragma once

#include "wrap-vector.hpp"
#include "wrap-map.hpp"
#include "wrap-string.hpp"
#include "wrap-bytell-hash-map.hpp"
#include "util.hpp"
#include "luastack.hpp"
#include "keywords.hpp"

#include <cstdint>
#include <memory>
#include <string_view>

class PlaneMap;
class PlaneTree;


class PlaneScan : public eng::nevernew {
public:
    friend class PlaneMap;
    friend class PlaneTree;
    enum Shape { BOX, CYLINDER, SPHERE };
private:
    // The plane to scan.
    //
    // Note: the reason this uses std::string instead of eng::string
    // is that we want plane scans to not touch the engine heap.
    //
    std::string plane_;

    // The bounding box of the scan.
    util::XYZ center_, radius_;

    // If you scan a cylinder or SPHERE, it actually
    // scans the bounding box first, then clips out
    // the parts that aren't correct.
    Shape shape_;

    // When true, the items are sorted by ID.
    // WARNING: setting this to false can create
    // nondeterminism.  Scans by lua should always be sorted.
    // When sorting and 'include_near' are both specified,
    // the near item is returned in its sorted order.
    bool sorted_;

    // The near ID is omitted from the results if include_near is false.
    // The near ID is included in the results if include_near is true.
    // The near ID is ignored if near is zero.
    int64_t near_;
    bool include_near_;
    
    // If this is true, items on the nowhere plane are not scanned.
    bool omit_nowhere_;

public:
    void clear() {
        plane_.clear();
        shape_ = BOX;
        sorted_ = true;
        near_ = 0;
        include_near_ = false;
        omit_nowhere_ = false;
        radius_ = util::XYZ();
        center_ = util::XYZ();
    }
    PlaneScan() { clear(); }
    
    // Convert a lua table into a scan configuration.
    //
    // If there is an error in the configuration, 
    // throws a lua error message.
    //
    // Caution: if this detects the configuration flag 'near',
    // then it stores the near ID.  However, it doesn't fetch
    // the center and plane.  It is the caller's responsibility
    // to check if 'near' has been set, and if so, store a center
    // and plane.  This is admittedly ugly, but it eliminates
    // a dependency on the world module.
    //
    void configure(LuaKeywordParser &kw);

    void set_center_and_radius(const util::XYZ &center, float r) {
        set_center(center);
        set_radius(r);
    }

    void set_whole_plane() {
        shape_ = BOX;
        center_ = util::XYZ();
        radius_.x = std::numeric_limits<float>::infinity();
        radius_.y = radius_.z = radius_.x;
    }

    void set_center(const util::XYZ &center) { center_ = center; }
    void set_radius(const util::XYZ &radius) { radius_ = radius; }
    void set_radius(float f) { radius_.x = radius_.y = radius_.z = f; }
    void set_plane(std::string_view p) { plane_ = p; }
    void set_shape(Shape s) { shape_ = s; }
    void set_sorted(bool s) { sorted_ = s; }
    void set_near(int64_t id, bool inc) { near_ = id; include_near_ = inc; }
    void set_omit_nowhere(bool b) { omit_nowhere_ = b; }

    int64_t near() const { return near_; }

    // Reveal the contents of the PlaneScan as a string.
    eng::string debug_string() const;
};

class PlaneItem : public eng::nevernew {
    friend class PlaneTree;
    friend class PlaneMap;    
private:
    PlaneTree *tree_;
    PlaneItem *prev_;
    PlaneItem *next_;
    int64_t id_;
    eng::string plane_;
    float x_, y_, z_;

public:
    PlaneItem();
    ~PlaneItem();

    // You may modify the ID at any time.  
    void set_id(int64_t id) { id_ = id; }

    int64_t id() const { return id_; }
    const eng::string &plane() const { return plane_; }
    const float x() const { return x_; }
    const float y() const { return y_; }
    const float z() const { return z_; }

    void track(PlaneMap *pmap);
    void untrack() { track(nullptr); }

    void set_pos(const eng::string &plane, float x, float y, float z);
    void set_xyz(float x, float y, float z);
};

class PlaneMap : public eng::nevernew  {
    friend class PlaneItem;
    friend class PlaneTree;
private:
    float default_radius_;

    // Plane Trees table.
    //
    eng::map<eng::string, std::unique_ptr<PlaneTree>, std::less<>> planes_;

public:
    // No special code is needed for construction or destruction.
    PlaneMap();
    ~PlaneMap();

    // The 'insert' and 'remove' operators are inside class
    // PlaneItem.  See PlaneItem::track and PlaneItem::untrack.
    
    // Scan the PlaneMap for items, return their IDs.
    //
    // Note: the reason this uses IdVector instead of IdVector
    // is that we want scans to not touch the engine heap.
    //
    void scan(const PlaneScan &sc, util::IdVector *into) const;
    
    // Set the default radius for all planes.
    // Maybe we'll make it adaptive some day.
    void set_default_radius(float r) { default_radius_ = r; }

    // Return a debug string for the specified plane.
    // This is for unit testing.
    eng::string tree_debug_string(const eng::string &plane);
    eng::string outliers_debug_string(const eng::string &plane);
    eng::string search_bboxes_debug_string(const PlaneScan &scan);
    eng::string scan_steps_debug_string(const PlaneScan &scan);

    // Untrack all planeitems.  This is for unit testing.
    void untrack_all();

private:
    // unit testing stuff.
    friend int lfn_unittests_planemap(lua_State *L);
};


