#include "JsonLogic.h"

#include <cerrno>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <cstring>
#include <limits>

#define MAKE_OPERATION(operation) \
    [this](JsonArrayView values, JsonView data) { return operation(values, data); }

namespace json_logic
{
    namespace
    {
        bool parse_number(const char* text, double& result)
        {
            if (text == nullptr || *text == '\0') return false;

            char* end = nullptr;
            errno = 0;
            result = std::strtod(text, &end);

            if (errno == ERANGE || end == text) return false;
            while (*end != '\0' && std::isspace(static_cast<unsigned char>(*end))) ++end;
            return *end == '\0';
        }

        bool parse_index(const std::string& text, std::size_t& result)
        {
            if (text.empty() || text[0] == '-') return false;

            char* end = nullptr;
            errno = 0;
            const unsigned long value = std::strtoul(text.c_str(), &end, 10);
            if (errno == ERANGE || end == text.c_str() || *end != '\0') return false;

            result = static_cast<std::size_t>(value);
            return true;
        }

        enum class JsonType
        {
            Null,
            Boolean,
            Number,
            String,
            Array,
            Object
        };

        JsonType json_type(JsonView value)
        {
            if (value.isNull()) return JsonType::Null;
            if (value.is<bool>()) return JsonType::Boolean;
            if (value.is<ArduinoJson::JsonInteger>() ||
                value.is<ArduinoJson::JsonUInt>() ||
                value.is<ArduinoJson::JsonFloat>())
                return JsonType::Number;
            if (value.is<const char*>()) return JsonType::String;
            if (value.is<ArduinoJson::JsonArrayConst>()) return JsonType::Array;
            return JsonType::Object;
        }
    }

    JsonLogic::JsonLogic()
    {
        operations_.insert({"var", MAKE_OPERATION(operation_data_var)});
        operations_.insert({"missing", MAKE_OPERATION(operation_data_missing)});
        operations_.insert({"missing_some", MAKE_OPERATION(operation_data_missing_some)});

        operations_.insert({"if", MAKE_OPERATION(operation_logic_if)});
        operations_.insert({"==", MAKE_OPERATION(operation_logic_abstract_equal)});
        operations_.insert({"===", MAKE_OPERATION(operation_logic_strict_equal)});
        operations_.insert({"!=", MAKE_OPERATION(operation_logic_abstract_different)});
        operations_.insert({"!==", MAKE_OPERATION(operation_logic_strict_different)});
        operations_.insert({"!", MAKE_OPERATION(operation_logic_not)});
        operations_.insert({"!!", MAKE_OPERATION(operation_logic_truthy)});
        operations_.insert({"or", MAKE_OPERATION(operation_logic_or)});
        operations_.insert({"and", MAKE_OPERATION(operation_logic_and)});

        operations_.insert({">", MAKE_OPERATION(operation_numeric_gt)});
        operations_.insert({">=", MAKE_OPERATION(operation_numeric_gte)});
        operations_.insert({"<", MAKE_OPERATION(operation_numeric_lt)});
        operations_.insert({"<=", MAKE_OPERATION(operation_numeric_lte)});
        operations_.insert({"max", MAKE_OPERATION(operation_numeric_max)});
        operations_.insert({"min", MAKE_OPERATION(operation_numeric_min)});
        operations_.insert({"+", MAKE_OPERATION(operation_numeric_addition)});
        operations_.insert({"-", MAKE_OPERATION(operation_numeric_subtraction)});
        operations_.insert({"*", MAKE_OPERATION(operation_numeric_multiplication)});
        operations_.insert({"/", MAKE_OPERATION(operation_numeric_division)});
        operations_.insert({"%", MAKE_OPERATION(operation_numeric_modulo)});

        operations_.insert({"map", MAKE_OPERATION(operation_array_map)});
        operations_.insert({"reduce", MAKE_OPERATION(operation_array_reduce)});
        operations_.insert({"filter", MAKE_OPERATION(operation_array_filter)});
        operations_.insert({"all", MAKE_OPERATION(operation_array_all)});
        operations_.insert({"none", MAKE_OPERATION(operation_array_none)});
        operations_.insert({"some", MAKE_OPERATION(operation_array_some)});
        operations_.insert({"merge", MAKE_OPERATION(operation_array_merge)});
        operations_.insert({"in", MAKE_OPERATION(operation_array_string_in)});

        operations_.insert({"cat", MAKE_OPERATION(operation_string_cat)});
        operations_.insert({"substr", MAKE_OPERATION(operation_string_substr)});
        operations_.insert({"log", MAKE_OPERATION(operation_misc_log)});
    }

    JsonLogic* JsonLogic::GetInstance()
    {
        static JsonLogic instance;
        return &instance;
    }

    Json JsonLogic::Apply(JsonView logic) const
    {
        Json data;
        data.to<ArduinoJson::JsonObject>();
        return Apply(logic, data);
    }

    Json JsonLogic::Apply(JsonView logic, JsonView data) const
    {
        ClearError();
        return ApplyInternal(logic, data);
    }

    Json JsonLogic::ApplyInternal(JsonView logic, JsonView data) const
    {
        if (logic.is<ArduinoJson::JsonArrayConst>())
        {
            Json result;
            auto output = result.to<ArduinoJson::JsonArray>();
            for (JsonView element : logic.as<ArduinoJson::JsonArrayConst>())
            {
                const Json applied = ApplyInternal(element, data);
                if (HasError()) return Json{};
                output.add(applied);
            }
            return result;
        }

        if (!IsLogic(logic)) return Copy(logic);

        const std::string operation_name = GetOperator(logic);
        const auto operation = operations_.find(operation_name);
        if (operation == operations_.end()) return Copy(logic);

        const JsonView raw_values = GetValues(logic);
        if (raw_values.is<ArduinoJson::JsonArrayConst>())
            return operation->second(raw_values.as<ArduinoJson::JsonArrayConst>(), data);

        Json normalized_values;
        normalized_values.to<ArduinoJson::JsonArray>().add(raw_values);
        return operation->second(normalized_values.as<ArduinoJson::JsonArrayConst>(), data);
    }

    void JsonLogic::AddOperation(const std::string& name, const Operation& operation)
    {
        operations_[name] = operation;
    }

    void JsonLogic::RmOperation(const std::string& name)
    {
        operations_.erase(name);
    }

    bool JsonLogic::HasError() const
    {
        return !last_error_.empty();
    }

    const std::string& JsonLogic::GetLastError() const
    {
        return last_error_;
    }

    void JsonLogic::ClearError() const
    {
        last_error_.clear();
    }

    Json JsonLogic::Fail(const char* operation_name, const std::string& message) const
    {
        last_error_ = "[" + std::string(operation_name) + "] " + message;
#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS)) && !defined(ARDUINO)
        throw JsonLogicException(last_error_);
#else
        return Json{};
#endif
    }

    Json JsonLogic::Copy(JsonView value)
    {
        Json result;
        result.set(value);
        return result;
    }

    Json JsonLogic::MakeArray()
    {
        Json result;
        result.to<ArduinoJson::JsonArray>();
        return result;
    }

    Json JsonLogic::MakeBoolean(bool value)
    {
        Json result;
        result.set(value);
        return result;
    }

    Json JsonLogic::MakeNumber(double value)
    {
        Json result;
        result.set(value);
        return result;
    }

    Json JsonLogic::MakeInteger(ArduinoJson::JsonInteger value)
    {
        Json result;
        result.set(value);
        return result;
    }

    Json JsonLogic::MakeString(const std::string& value)
    {
        Json result;
        result.set(value);
        return result;
    }

    bool JsonLogic::IsLogic(JsonView logic)
    {
        return logic.is<ArduinoJson::JsonObjectConst>() && logic.size() == 1;
    }

    std::string JsonLogic::GetOperator(JsonView logic)
    {
        const auto object = logic.as<ArduinoJson::JsonObjectConst>();
        return object.begin()->key().c_str();
    }

    JsonView JsonLogic::GetValues(JsonView logic)
    {
        return logic.as<ArduinoJson::JsonObjectConst>().begin()->value();
    }

    bool JsonLogic::IsNumber(JsonView value)
    {
        return value.is<ArduinoJson::JsonInteger>() ||
               value.is<ArduinoJson::JsonUInt>() ||
               value.is<ArduinoJson::JsonFloat>();
    }

    bool JsonLogic::IsInteger(JsonView value)
    {
        return value.is<ArduinoJson::JsonInteger>() || value.is<ArduinoJson::JsonUInt>();
    }

    double JsonLogic::ToDouble(JsonView value)
    {
        return value.as<double>();
    }

    bool JsonLogic::JsonEqual(JsonView lhs, JsonView rhs)
    {
        return lhs == rhs;
    }

    bool JsonLogic::StrictEqual(JsonView lhs, JsonView rhs)
    {
        if (json_type(lhs) != json_type(rhs)) return false;
        if (IsNumber(lhs)) return ToDouble(lhs) == ToDouble(rhs);
        return JsonEqual(lhs, rhs);
    }

    bool JsonLogic::AbstractEqual(JsonView lhs, JsonView rhs)
    {
        if (StrictEqual(lhs, rhs)) return true;

        if (IsNumber(lhs))
        {
            const double number = ToDouble(lhs);

            if (rhs.is<bool>()) return number == (rhs.as<bool>() ? 1.0 : 0.0);

            if (rhs.is<const char*>())
            {
                double parsed = 0;
                return parse_number(rhs.as<const char*>(), parsed) && number == parsed;
            }

            if (rhs.is<ArduinoJson::JsonArrayConst>())
            {
                const auto array = rhs.as<ArduinoJson::JsonArrayConst>();
                if (array.size() == 0) return number == 0.0;
                if (array.size() == 1) return AbstractEqual(lhs, array[0]);
            }
        }

        if (!IsNumber(lhs) && IsNumber(rhs)) return AbstractEqual(rhs, lhs);
        return false;
    }

    bool JsonLogic::Truthy(JsonView value)
    {
        if (value.is<bool>()) return value.as<bool>();
        if (IsNumber(value)) return ToDouble(value) != 0.0;
        if (value.is<const char*>()) return std::strlen(value.as<const char*>()) != 0;
        if (value.is<ArduinoJson::JsonArrayConst>()) return value.size() != 0;
        return false;
    }

    bool JsonLogic::TryResolveVariable(JsonView key, JsonView data, Json& result)
    {
        if (!key.is<const char*>()) return false;

        const std::string path = key.as<const char*>();
        JsonView current = data;
        if (path.empty())
        {
            result.set(current);
            return true;
        }

        std::size_t token_start = 0;
        while (token_start <= path.size())
        {
            const std::size_t separator = path.find('.', token_start);
            const std::string token = path.substr(
                token_start,
                separator == std::string::npos ? std::string::npos : separator - token_start);

            if (token.empty())
            {
                result.set(current);
                return true;
            }

            std::size_t index = 0;
            if (parse_index(token, index))
            {
                if (!current.is<ArduinoJson::JsonArrayConst>()) return false;
                const auto array = current.as<ArduinoJson::JsonArrayConst>();
                if (index >= array.size()) return false;
                current = array[index];
            }
            else
            {
                if (!current.is<ArduinoJson::JsonObjectConst>()) return false;
                const JsonView member = current[token];
                if (member.isUnbound()) return false;
                current = member;
            }

            if (separator == std::string::npos) break;
            token_start = separator + 1;
        }

        result.set(current);
        return true;
    }

    Json JsonLogic::CastToNumber(JsonView value) const
    {
        if (IsNumber(value)) return Copy(value);
        if (value.is<bool>()) return MakeInteger(value.as<bool>() ? 1 : 0);
        if (value.is<ArduinoJson::JsonArrayConst>()) return MakeInteger(value.size() == 0 ? 0 : 1);

        if (value.is<const char*>())
        {
            double parsed = 0;
            if (parse_number(value.as<const char*>(), parsed)) return MakeNumber(parsed);
        }

        std::string serialized;
        ArduinoJson::serializeJson(value, serialized);
        return Fail(__FUNCTION__, "Invalid cast to number: " + serialized);
    }
}

#undef MAKE_OPERATION
