#include "events.hpp"

#include <nlohmann/json.hpp>

#include "mtx/events/encrypted.hpp"
#include "mtx/events/unknown.hpp"

namespace mtx::events {
namespace detail {

template<typename, typename = void>
struct can_edit : std::false_type
{};

template<typename Content>
struct can_edit<Content, std::void_t<decltype(Content::relations)>>
  : std::is_same<decltype(Content::relations), mtx::common::Relations>
{};
}

template<class Content>
[[gnu::used, gnu::retain]] void
to_json(nlohmann::json &obj, const Event<Content> &event)
{
    obj["content"] = event.content;
    obj["sender"]  = event.sender;
    if constexpr (std::is_same_v<Unknown, Content>)
        obj["type"] = event.content.type;
    else
        obj["type"] = ::mtx::events::to_string(event.type);
}

template<class Content>
[[gnu::used, gnu::retain]] void
from_json(const nlohmann::json &obj, Event<Content> &event)
{
    if (!std::is_same_v<Content, mtx::events::msg::Encrypted> &&
        obj.at("content").contains("m.new_content")) {
        auto new_content = obj.at("content").at("m.new_content");

        if (obj.at("content").contains("m.relates_to"))
            new_content["m.relates_to"] = obj.at("content").at("m.relates_to");
        if (obj.at("content").at("m.new_content").contains("m.relates_to"))
            new_content["m.new_content"]["m.relates_to"] =
              obj.at("content").at("m.new_content").at("m.relates_to");
        if (obj.at("content").contains("im.nheko.relations.v1.relations"))
            new_content["im.nheko.relations.v1.relations"] =
              obj.at("content").at("im.nheko.relations.v1.relations");

        event.content = new_content.get<Content>();
    } else if (obj.at("content").is_object()) {
        event.content = obj.at("content").get<Content>();
    } else {
        event.content = {};
    }

    auto type = obj.at("type").get<std::string>();
    if (type.size() > 255) {
        throw std::out_of_range("Type exceeds 255 bytes");
    }
    event.type = getEventType(type);

    event.sender = obj.value("sender", "");

    if (event.sender.size() > 255) {
        throw std::out_of_range("Sender exceeds 255 bytes");
    }

    if constexpr (std::is_same_v<Unknown, Content>)
        event.content.type = obj.at("type").get<std::string>();
}

template<class Content>
[[gnu::used, gnu::retain]] void
from_json(const nlohmann::json &obj, DeviceEvent<Content> &event)
{
    Event<Content> &base = event;
    from_json(obj, base);

    if (event.sender.empty()) {
        throw std::out_of_range("Empty sender in to_device event.");
    }
}

template<class Content>
[[gnu::used, gnu::retain]] void
to_json(nlohmann::json &obj, const DeviceEvent<Content> &event)
{
    Event<Content> base_event = event;
    to_json(obj, base_event);
}

void
from_json(const nlohmann::json &obj, UnsignedData &data)
{
    if (auto field = obj.find("age"); field != obj.end())
        data.age = field->get<uint64_t>();

    if (auto field = obj.find("transaction_id"); field != obj.end())
        data.transaction_id = field->get<std::string>();

    if (auto field = obj.find("prev_sender"); field != obj.end())
        data.prev_sender = field->get<std::string>();

    // This field is non-standard, but otherwise we need to be able to nest arbitrary event contents
    // here, which currently doesn't work with how we type our event variants...
    if (auto field = obj.find("replaces_state"); field != obj.end())
        data.replaces_state = field->get<std::string>();

    if (auto field = obj.find("redacted_by"); field != obj.end())
        data.redacted_by = field->get<std::string>();

    if (auto field = obj.find("redacted_because"); field != obj.end())
        data.redacted_because = field->get<Event<mtx::events::msg::Redaction>>();
}

void
to_json(nlohmann::json &obj, const UnsignedData &event)
{
    if (!event.prev_sender.empty())
        obj["prev_sender"] = event.prev_sender;

    if (!event.transaction_id.empty())
        obj["transaction_id"] = event.transaction_id;

    if (!event.replaces_state.empty())
        obj["replaces_state"] = event.replaces_state;

    if (event.age != 0)
        obj["age"] = event.age;

    if (!event.redacted_by.empty())
        obj["redacted_by"] = event.redacted_by;

    if (event.redacted_because)
        obj["redacted_because"] = *event.redacted_because;
}

template<class Content>
[[gnu::used, gnu::retain]] void
from_json(const nlohmann::json &obj, StrippedEvent<Content> &event)
{
    Event<Content> &base = event;
    from_json(obj, base);

    event.state_key = obj.at("state_key").get<std::string>();

    if (event.state_key.size() > 255) {
        throw std::out_of_range("State key exceeds 255 bytes");
    }
}

template<class Content>
[[gnu::used, gnu::retain]] void
to_json(nlohmann::json &obj, const StrippedEvent<Content> &event)
{
    Event<Content> base_event = event;
    to_json(obj, base_event);

    obj["state_key"] = event.state_key;
}

template<class Content>
[[gnu::used, gnu::retain]] void
from_json(const nlohmann::json &obj, RoomEvent<Content> &event)
{
    Event<Content> &base = event;
    from_json(obj, base);

    event.event_id = obj.at("event_id").get<std::string>();

    if (event.event_id.size() > 255) {
        throw std::out_of_range("Event id exceeds 255 bytes");
    }

    event.origin_server_ts = obj.at("origin_server_ts").get<uint64_t>();

    // Not present in the state array returned by /sync.
    if (auto field = obj.find("room_id"); field != obj.end())
        event.room_id = field->get<std::string>();

    if (event.room_id.size() > 255) {
        throw std::out_of_range("Room id exceeds 255 bytes");
    }

    if (auto field = obj.find("unsigned"); field != obj.end())
        event.unsigned_data = field->get<UnsignedData>();
}

template<class Content>
[[gnu::used, gnu::retain]] void
to_json(nlohmann::json &obj, const RoomEvent<Content> &event)
{
    Event<Content> base_event = event;
    to_json(obj, base_event);

    if (!event.room_id.empty())
        obj["room_id"] = event.room_id;

    obj["event_id"]         = event.event_id;
    obj["unsigned"]         = event.unsigned_data;
    obj["origin_server_ts"] = event.origin_server_ts;
}

template<class Content>
[[gnu::used, gnu::retain]] void
to_json(nlohmann::json &obj, const StateEvent<Content> &event)
{
    RoomEvent<Content> base_event = event;
    to_json(obj, base_event);

    obj["state_key"] = event.state_key;
}

template<class Content>
[[gnu::used, gnu::retain]] void
from_json(const nlohmann::json &obj, StateEvent<Content> &event)
{
    RoomEvent<Content> &base = event;
    from_json(obj, base);

    event.state_key = obj.at("state_key").get<std::string>();

    if (event.state_key.size() > 255) {
        throw std::out_of_range("State key exceeds 255 bytes");
    }
}

template<class Content>
[[gnu::used, gnu::retain]] void
to_json(nlohmann::json &obj, const RedactionEvent<Content> &event)
{
    RoomEvent<Content> base_event = event;
    to_json(obj, base_event);

    obj["redacts"] = event.redacts;
}

template<class Content>
[[gnu::used, gnu::retain]] void
from_json(const nlohmann::json &obj, RedactionEvent<Content> &event)
{
    RoomEvent<Content> &base = event;
    from_json(obj, base);

    event.redacts = obj.at("redacts").get<std::string>();
}

template<class Content>
[[gnu::used, gnu::retain]] void
to_json(nlohmann::json &obj, const EncryptedEvent<Content> &event)
{
    RoomEvent<Content> base_event = event;
    to_json(obj, base_event);
}

template<class Content>
[[gnu::used, gnu::retain]] void
from_json(const nlohmann::json &obj, EncryptedEvent<Content> &event)
{
    RoomEvent<Content> &base = event;
    from_json(obj, base);
}

template<class Content>
[[gnu::used, gnu::retain]] void
to_json(nlohmann::json &obj, const EphemeralEvent<Content> &event)
{
    obj["content"] = event.content;
    if constexpr (std::is_same_v<Unknown, Content>)
        obj["type"] = event.content.type;
    else
        obj["type"] = ::mtx::events::to_string(event.type);

    if (!event.room_id.empty())
        obj["room_id"] = event.room_id;
}

template<class Content>
[[gnu::used, gnu::retain]] void
from_json(const nlohmann::json &obj, EphemeralEvent<Content> &event)
{
    event.content = obj.at("content").get<Content>();
    event.type    = getEventType(obj.at("type").get<std::string>());
    if constexpr (std::is_same_v<Unknown, Content>)
        event.content.type = obj.at("type").get<std::string>();

    if (obj.contains("room_id"))
        event.room_id = obj.at("room_id").get<std::string>();

    if (event.room_id.size() > 255) {
        throw std::out_of_range("Room id exceeds 255 bytes");
    }
}

}
