/* Screenmap maps strip indexes to x,y coordinates. This is used for FastLED Web
 * to map the 1D strip to a 2D grid. Note that the strip can have arbitrary
 * size. this was first motivated during the (attempted? Oct. 19th 2024) port of
 * the Chromancer project to FastLED Web.
 */

#include "fl/math/screenmap.h"

// Heavy includes moved from header to reduce compilation time
#include "fl/math/lut.h"       // Full LUT definitions needed for implementation
#include "fl/stl/json.h"      // 61.1ms - only needed for ParseJson/toJson implementations
#include "fl/stl/string.h"       // 129.4ms - only needed for string parameters in implementations
#include "fl/stl/flat_map.h"  // 12.4ms - only needed for flat_map parameters in implementations
// IWYU pragma: begin_keep
#include "fl/stl/function.h"
// IWYU pragma: end_keep  // ~5ms - only needed for function<> constructor implementation
#include "fl/stl/static_assert.h"

// Other implementation dependencies
#include "fl/math/math.h"
#include "fl/math/math.h"
#include "fl/stl/vector.h"
#include "fl/log/log.h"
#include "fl/stl/noexcept.h"


namespace fl {

// Default constructor and destructor - must be in .cpp for proper smart_ptr handling
ScreenMap::ScreenMap() = default;
ScreenMap::~ScreenMap() FL_NOEXCEPT = default;

// Helper function to extract a vector of floats from a JSON array
fl::vector<float> jsonArrayToFloatVector(const fl::json& jsonArray) {
    fl::vector<float> result;
    
    if (!jsonArray.has_value() || !jsonArray.is_array()) {
        return result;
    }
    auto begin_float =  jsonArray.begin_array<float>();
    auto end_float = jsonArray.end_array<float>();

    using T = decltype(*begin_float);
    FL_STATIC_ASSERT(fl::is_same<T, fl::parse_result<float>>::value, "Value type must be parse_result<float>");
    
    // Use explicit array iterator style as demonstrated in FEATURE.md
    // DO NOT CHANGE THIS CODE. FIX THE IMPLIMENTATION IF NECESSARY.
    for (auto it = begin_float; it != end_float; ++it) {
        // assert that the value type is parse_result<float>

        // get the name of the type
        auto parseResult = *it;
        if (!parseResult.has_error()) {
            result.push_back(parseResult.get_value());
        } else {
            FL_WARN("jsonArrayToFloatVector: parse_result<float> has error: " << parseResult.get_error().message);
        }
    }
    
    return result;
}

ScreenMap ScreenMap::Circle(int numLeds, float cm_between_leds,
                            float cm_led_diameter, float completion) {
    ScreenMap screenMap(numLeds);

    // radius from LED spacing
    float circumference = numLeds * cm_between_leds;
    float radius = circumference / (2 * FL_PI);

    // how big an arc we light vs leave dark
    float totalAngle = completion * 2 * FL_PI;
    float gapAngle = 2 * FL_PI - totalAngle;

    // shift so the dark gap is centered at the bottom (–π/2)
    float startAngle = -FL_PI / 2 + gapAngle / 2.0f;

    // if partial, land last LED exactly at startAngle+totalAngle
    float divisor =
        (completion < 1.0f && numLeds > 1) ? (numLeds - 1) : numLeds;

    for (int i = 0; i < numLeds; ++i) {
        float angle = startAngle + (i * totalAngle) / divisor;
        float x = radius * cos(angle) * 2;
        float y = radius * sin(angle) * 2;
        screenMap[i] = {x, y};
    }

    screenMap.setDiameter(cm_led_diameter);
    return screenMap;
}

ScreenMap ScreenMap::DefaultStrip(int numLeds, float cm_between_leds,
                                  float cm_led_diameter, float completion) {
    return Circle(numLeds, cm_between_leds, cm_led_diameter, completion);
}

// Helper: parse a v2 screenmap document into the flat segmentMaps.
// v2 shape (see ledmapper #92):
//   {
//     "version": 2,                                  // optional, explicit
//     "groups":   { "<name>": { "color": "...", ... } },  // optional, ignored by firmware
//     "segments": [
//       { "id": "<unique>", "pin": <int|str>, "group": "<name>",
//         "x": [...], "y": [...], "z": [...],         // z optional
//         "parent": "<id>", "offset": <int|null> }    // parent+offset on forks only
//     ]
//   }
//
// Flatten policy: each segment becomes one ScreenMap entry keyed by `id`.
// `pin`, `group`, `parent`, `offset` are not represented in the firmware-side
// flat_map; they're UI/wiring metadata the editor and video tools care about.
// `z` is dropped (firmware-side ScreenMap is 2D today).
static bool parseV2SegmentArray(const fl::json& segmentsArr,
                                fl::flat_map<string, ScreenMap> *segmentMaps,
                                string *err) FL_NOEXCEPT {
    if (!segmentsArr.has_value() || !segmentsArr.is_array()) {
        *err = "v2 'segments' is not an array";
        return false;
    }

    auto arrPtr = segmentsArr.as_array();
    if (!arrPtr) {
        *err = "v2 'segments' array could not be read";
        return false;
    }

    for (const auto& elem : *arrPtr) {
        if (!elem) {
            *err = "v2 segment is null";
            return false;
        }
        fl::json segVal(elem);
        if (!segVal.has_value() || !segVal.is_object()) {
            *err = "v2 segment is not an object";
            return false;
        }

        // Required: id
        if (!segVal.contains("id") || !segVal["id"].has_value()) {
            *err = "v2 segment missing 'id'";
            return false;
        }
        auto idOpt = segVal["id"].as_string();
        if (!idOpt) {
            *err = "v2 segment 'id' is not a string";
            return false;
        }
        string id = *idOpt;

        // Required: x
        if (!segVal.contains("x") || !segVal["x"].has_value() || !segVal["x"].is_array()) {
            *err = "v2 segment '" + id + "' missing or invalid 'x' array";
            return false;
        }
        fl::vector<float> x_array = jsonArrayToFloatVector(segVal["x"]);

        // Required: y
        if (!segVal.contains("y") || !segVal["y"].has_value() || !segVal["y"].is_array()) {
            *err = "v2 segment '" + id + "' missing or invalid 'y' array";
            return false;
        }
        fl::vector<float> y_array = jsonArrayToFloatVector(segVal["y"]);

        // Optional: diameter (not in canonical v2 but accepted as a backward-compat
        // hint when present; otherwise the per-group `diameter` could be wired here
        // in a future iteration).
        float diameter = -1.0f;
        if (segVal.contains("diameter") && segVal["diameter"].has_value()) {
            auto diameterOpt = segVal["diameter"].as_float();
            if (diameterOpt) {
                diameter = static_cast<float>(*diameterOpt);
            }
        }

        auto n = fl::min(x_array.size(), y_array.size());
        ScreenMap segment_map(n, diameter);
        for (size_t i = 0; i < n; i++) {
            segment_map.set(i, vec2f{x_array[i], y_array[i]});
        }
        (*segmentMaps)[id] = fl::move(segment_map);
    }
    return true;
}

bool ScreenMap::ParseJson(const char *jsonStrScreenMap,
                          fl::flat_map<string, ScreenMap> *segmentMaps, string *err) {

#if FASTLED_NO_JSON
    FL_UNUSED(jsonStrScreenMap);
    FL_UNUSED(segmentMaps);
    FL_UNUSED(err);
    FL_WARN("ScreenMap::ParseJson called with FASTLED_NO_JSON");
    if (err) {
        *err = "JSON is not supported in this build";
    }
    return false;
#else
    //FL_WARN_SCREENMAP("ParseJson called with JSON: " << jsonStrScreenMap);

    string _err;
    if (!err) {
        err = &_err;
    }

    auto jsonDoc = fl::json::parse(jsonStrScreenMap);
    if (!jsonDoc.has_value()) {
        *err = "Failed to parse JSON";
        FL_WARN("Failed to parse JSON");
        return false;
    }

    if (!jsonDoc.is_object()) {
        *err = "JSON root is not an object";
        FL_WARN("JSON root is not an object");
        return false;
    }

    // ── v2 dispatch ──────────────────────────────────────────────────────
    // v2 if: explicit "version": 2  OR  has top-level "segments" array.
    // v1 if: explicit "version": 1  OR  has top-level "map" object.
    bool explicitV2 = false;
    bool explicitV1 = false;
    if (jsonDoc.contains("version") && jsonDoc["version"].has_value()) {
        auto versionOpt = jsonDoc["version"].as_int();
        if (versionOpt) {
            int v = static_cast<int>(*versionOpt);
            if (v == 2) explicitV2 = true;
            else if (v == 1) explicitV1 = true;
        }
    }
    bool hasSegments = jsonDoc.contains("segments") && jsonDoc["segments"].has_value()
                       && jsonDoc["segments"].is_array();
    bool hasMap = jsonDoc.contains("map") && jsonDoc["map"].has_value()
                  && jsonDoc["map"].is_object();

    if (explicitV2 || (!explicitV1 && hasSegments && !hasMap)) {
        return parseV2SegmentArray(jsonDoc["segments"], segmentMaps, err);
    }

    // Fall through to v1 path.
    // Check if "map" key exists and is an object
    if (!jsonDoc.contains("map")) {
        *err = "Missing 'map' key in JSON";
        FL_WARN("Missing 'map' key in JSON");
        return false;
    }
    
    // Get the map object
    auto mapObj = jsonDoc["map"];
    if (!mapObj.has_value() || !mapObj.is_object()) {
        *err = "Invalid 'map' object in JSON";
        FL_WARN("Invalid 'map' object in JSON");
        return false;
    }
    
    auto jsonMapPtr = mapObj.as_object();
    if (!jsonMapPtr || jsonMapPtr->empty()) {
        *err = "Failed to parse map from JSON or map is empty";
        FL_WARN("Failed to parse map from JSON or map is empty");
        return false;
    }

    auto& jsonMap = *jsonMapPtr;

    
    for (const auto& kv : jsonMap) {
        auto name = kv.first;

        
        // Check that the value is not null before creating json object
        if (!kv.second) {
            *err = "Null value for segment " + name;
            return false;
        }
        
        // Create json object directly from shared_ptr
        fl::json val(kv.second);
        if (!val.has_value()) {
            *err = "Invalid value for segment " + name;
            return false;
        }
        
        if (!val.is_object()) {
            *err = "Segment value for " + name + " is not an object";
            return false;
        }
        
        // Check if x array exists and is actually an array
        if (!val.contains("x")) {
            *err = "Missing x array for " + name;
            return false;
        }
        
        if (!val["x"].has_value() || !val["x"].is_array()) {
            *err = "Invalid x array for " + name;
            return false;
        }
        
        // Extract x array using our helper function
        fl::vector<float> x_array = jsonArrayToFloatVector(val["x"]);
        
        // Check if y array exists and is actually an array
        if (!val.contains("y")) {
            *err = "Missing y array for " + name;
            return false;
        }
        
        if (!val["y"].has_value() || !val["y"].is_array()) {
            *err = "Invalid y array for " + name;
            return false;
        }
        
        // Extract y array using our helper function
        fl::vector<float> y_array = jsonArrayToFloatVector(val["y"]);
        
        // Get diameter (optional) with default value
        float diameter = -1.0f; // default value
        if (val.contains("diameter") && val["diameter"].has_value()) {
                            auto diameterOpt = val["diameter"].as_float();
            if (diameterOpt) {
                diameter = static_cast<float>(*diameterOpt);
            }
        }

        auto n = fl::min(x_array.size(), y_array.size());
        if (n != x_array.size() || n != y_array.size()) {
            if (n != x_array.size()) {
            }
            if (n != y_array.size()) {
            }
        }

        ScreenMap segment_map(n, diameter);
        for (size_t i = 0; i < n; i++) {
            segment_map.set(i, vec2f{x_array[i], y_array[i]});
        }
        (*segmentMaps)[name] = fl::move(segment_map);
    }
    return true;
#endif
}

bool ScreenMap::ParseJson(const char *jsonStrScreenMap,
                          const char *screenMapName, ScreenMap *screenmap,
                          string *err) {

    fl::flat_map<string, ScreenMap> segmentMaps;
    bool ok = ParseJson(jsonStrScreenMap, &segmentMaps, err);
    if (!ok) {
        return false;
    }
    if (segmentMaps.size() == 0) {
        return false;
    }
    if (segmentMaps.contains(screenMapName)) {
        *screenmap = segmentMaps[screenMapName];
        return true;
    }
    string _err = "ScreenMap not found: ";
    _err.append(screenMapName);
    if (err) {
        *err = _err;
    }
    
    return false;
}

void ScreenMap::toJson(const fl::flat_map<string, ScreenMap> &segmentMaps,
                       fl::json *doc) {

#if FASTLED_NO_JSON
    FL_WARN("ScreenMap::toJson called with FASTLED_NO_JSON");
    return;
#else
    if (!doc) {
        FL_WARN("ScreenMap::toJson called with nullptr doc");
        return;
    }

    // Emits the v2 screenmap shape (issue ledmapper#143):
    //   { "version": 2,
    //     "groups": { "<name>": { "color": "#hex" } },
    //     "segments": [ { "id": "<name>", "pin": "pin1", "group": "<name>",
    //                     "x": [...], "y": [...], "diameter": ... } ] }
    // Bilingual readers (`ScreenMap::ParseJson`, ledmapper) accept both v1
    // and v2, so any existing on-disk v1 JSON keeps loading. v1 emission
    // is no longer supported.
    *doc = fl::json::object();

    fl::json groupsObj = fl::json::object();
    fl::json segmentsArr = fl::json::array();

    // Distinct palette so each strip lights up differently in the editor
    // preview without the user having to pick a colour. Cycle on overflow.
    static const char *const kPalette[] = {
        "#3b82f6", "#10b981", "#f59e0b", "#ef4444",
        "#a855f7", "#06b6d4", "#ec4899", "#84cc16",
    };
    constexpr size_t kPaletteSize = sizeof(kPalette) / sizeof(kPalette[0]);

    size_t idx = 0;
    for (const auto& kv : segmentMaps) {
        if (kv.second.getLength() == 0) {
            FL_WARN("ScreenMap::toJson called with empty segment: " << fl::string(kv.first));
            continue;
        }

        const auto& name = kv.first;
        const auto& segment = kv.second;
        const float diameter = segment.getDiameter();

        fl::json xArray = fl::json::array();
        for (u16 i = 0; i < segment.getLength(); i++) {
            xArray.push_back(fl::json(static_cast<double>(segment[i].x)));
        }
        fl::json yArray = fl::json::array();
        for (u16 i = 0; i < segment.getLength(); i++) {
            yArray.push_back(fl::json(static_cast<double>(segment[i].y)));
        }

        fl::json groupObj = fl::json::object();
        groupObj.set("color", fl::json(fl::string(kPalette[idx % kPaletteSize])));
        groupsObj.set(name, groupObj);

        fl::json segmentObj = fl::json::object();
        segmentObj.set("id", fl::json(fl::string(name)));
        segmentObj.set("pin", fl::json(fl::string("pin1")));
        segmentObj.set("group", fl::json(fl::string(name)));
        segmentObj.set("x", xArray);
        segmentObj.set("y", yArray);
        segmentObj.set("diameter", fl::json(static_cast<double>(diameter)));
        segmentsArr.push_back(segmentObj);

        idx++;
    }

    doc->set("version", fl::json(static_cast<double>(2)));
    doc->set("groups", groupsObj);
    doc->set("segments", segmentsArr);

    fl::string debugStr = doc->to_string();
    FL_WARN("ScreenMap::toJson generated JSON: " << debugStr);
#endif
}

void ScreenMap::toJsonStr(const fl::flat_map<string, ScreenMap> &segmentMaps,
                          string *jsonBuffer) {
    fl::json doc;
    toJson(segmentMaps, &doc);
    *jsonBuffer = doc.to_string();
}

ScreenMap::ScreenMap(u32 length, float mDiameter)
    : length(length), mDiameter(mDiameter) {
    if (length > 0) {
        mLookUpTable = fl::make_shared<LUTXYFLOAT>(length);
        LUTXYFLOAT &lut = *mLookUpTable.get();
        vec2f *data = lut.getDataMutable();
        for (u32 x = 0; x < length; x++) {
            data[x] = {0, 0};
        }
    }
}

ScreenMap::ScreenMap(const vec2f *lut, u32 length, float diameter)
    : length(length), mDiameter(diameter) {
    mLookUpTable = fl::make_shared<LUTXYFLOAT>(length);
    LUTXYFLOAT &lut16xy = *mLookUpTable.get();
    vec2f *data = lut16xy.getDataMutable();
    for (u32 x = 0; x < length; x++) {
        data[x] = lut[x];
    }
}

ScreenMap::ScreenMap(int count, float diameter, fl::function<void(int, vec2f& pt_out)> func)
    : length(count), mDiameter(diameter) {
    if (count > 0) {
        mLookUpTable = fl::make_shared<LUTXYFLOAT>(count);
        LUTXYFLOAT &lut = *mLookUpTable.get();
        vec2f *data = lut.getDataMutable();
        for (int i = 0; i < count; i++) {
            func(i, data[i]);
        }
    }
}

ScreenMap::ScreenMap(const ScreenMap &other) {
    mDiameter = other.mDiameter;
    length = other.length;
    mLookUpTable = other.mLookUpTable;
    mSourceXYMap = other.mSourceXYMap;
}

ScreenMap::ScreenMap(ScreenMap&& other) {
    mDiameter = other.mDiameter;
    length = other.length;
    fl::swap(mLookUpTable, other.mLookUpTable);
    fl::swap(mSourceXYMap, other.mSourceXYMap);
    other.mLookUpTable.reset();
    other.mSourceXYMap.reset();
}

void ScreenMap::set(u16 index, const vec2f &p) {
    if (mLookUpTable) {
        LUTXYFLOAT &lut = *mLookUpTable.get();
        auto *data = lut.getDataMutable();
        data[index] = p;
    }
}

void ScreenMap::setDiameter(float diameter) { mDiameter = diameter; }

vec2f ScreenMap::mapToIndex(u32 x) const {
    if (x >= length || !mLookUpTable) {
        return {0, 0};
    }
    LUTXYFLOAT &lut = *mLookUpTable.get();
    vec2f screen_coords = lut[x];
    return screen_coords;
}

u32 ScreenMap::getLength() const { return length; }

float ScreenMap::getDiameter() const { return mDiameter; }

vec2f ScreenMap::getBounds() const {

    if (length == 0 || !mLookUpTable) {
        return {0, 0};
    }

    LUTXYFLOAT &lut = *mLookUpTable.get();

    fl::vec2f *data = lut.getDataMutable();
    // float minX = lut[0].x;
    // float maxX = lut[0].x;
    // float minY = lut[0].y;
    // float maxY = lut[0].y;
    float minX = data[0].x;
    float maxX = data[0].x;
    float minY = data[0].y;
    float maxY = data[0].y;

    for (u32 i = 1; i < length; i++) {
        const vec2f &p = lut[i];
        minX = fl::min(minX, p.x);
        maxX = fl::max(maxX, p.x);
        minY = fl::min(minY, p.y);
        maxY = fl::max(maxY, p.y);
    }

    return {maxX - minX, maxY - minY};
}

const vec2f &ScreenMap::empty() {
    static const vec2f s_empty = vec2f(0, 0); // okay static in header
    return s_empty;
}

const vec2f &ScreenMap::operator[](u32 x) const {
    if (x >= length || !mLookUpTable) {
        return empty(); // better than crashing.
    }
    LUTXYFLOAT &lut = *mLookUpTable.get();
    return lut[x];
}

vec2f &ScreenMap::operator[](u32 x) {
    if (x >= length || !mLookUpTable) {
        return const_cast<vec2f &>(empty()); // better than crashing.
    }
    LUTXYFLOAT &lut = *mLookUpTable.get();
    auto *data = lut.getDataMutable();
    return data[x];
}

ScreenMap &ScreenMap::operator=(const ScreenMap &other) FL_NOEXCEPT {
    if (this != &other) {
        mDiameter = other.mDiameter;
        length = other.length;
        mLookUpTable = other.mLookUpTable;
        mSourceXYMap = other.mSourceXYMap;
    }
    return *this;
}

ScreenMap &ScreenMap::operator=(ScreenMap &&other) FL_NOEXCEPT {
    if (this != &other) {
        mDiameter = other.mDiameter;
        length = other.length;
        mLookUpTable = fl::move(other.mLookUpTable);
        mSourceXYMap = fl::move(other.mSourceXYMap);
        other.length = 0;
        other.mDiameter = -1.0f;
    }
    return *this;
}

void ScreenMap::setSourceXYMap(const fl::shared_ptr<XYMap>& xymap) {
    mSourceXYMap = xymap;
}

const XYMapPtr& ScreenMap::getSourceXYMapPtr() const {
    return mSourceXYMap;
}

const XYMap* ScreenMap::getXYMap() const {
    return mSourceXYMap.get();
}

bool ScreenMap::hasSourceXYMap() const {
    return mSourceXYMap != nullptr;
}

void ScreenMap::addOffset(const vec2f &p) {
    vec2f *data = mLookUpTable->getDataMutable();
    for (u32 i = 0; i < length; i++) {
        vec2f &curr = data[i];
        curr.x += p.x;
        curr.y += p.y;
    }
}

ScreenMap& ScreenMap::addOffsetX(float x) { addOffset({x, 0}); return *this; }
ScreenMap& ScreenMap::addOffsetY(float y) { addOffset({0, y}); return *this; }

} // namespace fl
