#pragma once

#include <nlohmann/json.hpp>

#include "mtx/events/messages/image.hpp"
#include "mtx/identifiers.hpp"

using json = nlohmann::json;
namespace mtx {
namespace events {

enum class EventType
{
        /// m.key.verification.cancel
        KeyVerificationCancel,
        /// m.key.verification.request
        KeyVerificationRequest,
        /// m.key.verification.start
        KeyVerificationStart,
        /// m.key.verification.accept
        KeyVerificationAccept,
        /// m.key.verification.key
        KeyVerificationKey,
        /// m.key.verification.mac
        KeyVerificationMac,
        /// m.reaction,
        Reaction,
        /// m.room_key
        RoomKey,
        /// m.room_key_request
        RoomKeyRequest,
        /// m.room.aliases
        RoomAliases,
        /// m.room.avatar
        RoomAvatar,
        /// m.room.canonical_alias
        RoomCanonicalAlias,
        /// m.room.create
        RoomCreate,
        /// m.room.encrypted.
        RoomEncrypted,
        /// m.room.encryption.
        RoomEncryption,
        /// m.room.guest_access
        RoomGuestAccess,
        /// m.room.history_visibility
        RoomHistoryVisibility,
        /// m.room.join_rules
        RoomJoinRules,
        /// m.room.member
        RoomMember,
        /// m.room.message
        RoomMessage,
        /// m.room.name
        RoomName,
        /// m.room.power_levels
        RoomPowerLevels,
        /// m.room.topic
        RoomTopic,
        /// m.room.redaction
        RoomRedaction,
        /// m.room.pinned_events
        RoomPinnedEvents,
        /// m.room.tombstone
        RoomTombstone,
        // m.sticker
        Sticker,
        // m.tag
        Tag,
        // m.presence
        Presence,
        // m.push_rules
        PushRules,
        // Unsupported event
        Unsupported,
};

std::string
to_string(EventType type);

EventType
getEventType(const std::string &type);

EventType
getEventType(const json &obj);

//! The basic set of fields all events must have.
template<class Content>
struct Event
{
        //! The fields in this object will vary depending on the type of event.
        //! When interacting with the REST API, this is the HTTP body.
        Content content;
        //! The type of event.
        //! This *should* be namespaced similar to Java package
        //! naming conventions e.g. 'com.example.subdomain.event.type'
        EventType type;
        //! Contains the fully-qualified ID of the user who sent this event.
        std::string sender;
};

template<class Content>
void
to_json(json &obj, const Event<Content> &event)
{
        obj["content"] = event.content;
        obj["sender"]  = event.sender;
        obj["type"]    = ::mtx::events::to_string(event.type);
}

template<class Content>
void
from_json(const json &obj, Event<Content> &event)
{
        event.content = obj.at("content").get<Content>();
        event.type    = getEventType(obj.at("type").get<std::string>());
        event.sender  = obj.value("sender", "");
}

//! Extension of the Event type for device events.
template<class Content>
struct DeviceEvent : public Event<Content>
{
        std::string sender;
};

template<class Content>
void
from_json(const json &obj, DeviceEvent<Content> &event)
{
        Event<Content> base_event = event;
        from_json(obj, base_event);
        event.content = base_event.content;
        event.type    = base_event.type;
        event.sender  = obj.at("sender");
}

template<class Content>
void
to_json(json &obj, const DeviceEvent<Content> &event)
{
        Event<Content> base_event = event;
        to_json(obj, base_event);

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

struct UnsignedData
{
        //! The time in milliseconds that has elapsed since the event was sent.
        //! This field is generated by the local homeserver,
        //! and may be incorrect if the local time on at least one
        //! of the two servers is out of sync, which can cause the age to
        //! either be negative or greater than it actually is.
        uint64_t age = 0;
        //! The client-supplied transaction ID, if the client
        //! being given the event is the same one which sent it.
        std::string transaction_id;
        std::string prev_sender;
        std::string replaces_state;
        //! The event ID that redacted this event.
        std::string redacted_by;
};

inline void
from_json(const json &obj, UnsignedData &data)
{
        if (obj.find("age") != obj.end())
                data.age = obj.at("age").get<uint64_t>();

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

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

        if (obj.find("replaces_state") != obj.end())
                data.replaces_state = obj.at("replaces_state").get<std::string>();

        if (obj.find("redacted_by") != obj.end())
                data.redacted_by = obj.at("redacted_by").get<std::string>();
}

inline void
to_json(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;
}

template<class Content>
struct StrippedEvent : public Event<Content>
{
        std::string state_key;
};

template<class Content>
void
from_json(const json &obj, StrippedEvent<Content> &event)
{
        Event<Content> &base = event;
        from_json(obj, base);

        event.state_key = obj.at("state_key");
}

template<class Content>
void
to_json(json &obj, const StrippedEvent<Content> &event)
{
        Event<Content> base_event = event;
        to_json(obj, base_event);

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

//! RoomEvent.
template<class Content>
struct RoomEvent : public Event<Content>
{
        //! The globally unique event identifier.
        std::string event_id;
        //! The ID of the room associated with this event.
        std::string room_id;
        //! Timestamp in milliseconds on originating homeserver
        //! when this event was sent.
        uint64_t origin_server_ts;
        // SPEC_BUG: The contents of unsigned_data are also present as top level keys.
        //! Contains optional extra information about the event.
        UnsignedData unsigned_data;
};

template<class Content>
void
from_json(const json &obj, RoomEvent<Content> &event)
{
        Event<Content> &base = event;
        from_json(obj, base);

        event.event_id         = obj.at("event_id");
        event.origin_server_ts = obj.at("origin_server_ts");

        // SPEC_BUG: Not present in the state array returned by /sync.
        if (obj.find("room_id") != obj.end())
                event.room_id = obj.at("room_id");

        if (obj.find("unsigned") != obj.end())
                event.unsigned_data = obj.at("unsigned");
}

template<class Content>
void
to_json(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;
}

//! Extension of the RoomEvent.
template<class Content>
struct StateEvent : public RoomEvent<Content>
{
        //! A unique key which defines the overwriting semantics
        //! for this piece of room state.
        std::string state_key;
};

template<class Content>
void
to_json(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>
void
from_json(const json &obj, StateEvent<Content> &event)
{
        RoomEvent<Content> &base = event;
        from_json(obj, base);

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

//! Extension of the RoomEvent.
template<class Content>
struct RedactionEvent : public RoomEvent<Content>
{
        //! The event id of the event that was redacted.
        std::string redacts;
};

template<class Content>
void
to_json(json &obj, const RedactionEvent<Content> &event)
{
        RoomEvent<Content> base_event = event;
        to_json(obj, base_event);

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

template<class Content>
void
from_json(const json &obj, RedactionEvent<Content> &event)
{
        RoomEvent<Content> &base = event;
        from_json(obj, base);

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

//! Extension of the RoomEvent.
template<class Content>
struct EncryptedEvent : public RoomEvent<Content>
{};

template<class Content>
void
to_json(json &obj, const EncryptedEvent<Content> &event)
{
        RoomEvent<Content> base_event = event;
        to_json(obj, base_event);
}

template<class Content>
void
from_json(const json &obj, EncryptedEvent<Content> &event)
{
        RoomEvent<Content> &base = event;
        from_json(obj, base);
}

enum class MessageType
{
        // m.audio
        Audio,
        // m.emote
        Emote,
        // m.file
        File,
        // m.image
        Image,
        // m.location
        Location,
        // m.notice
        Notice,
        // m.text
        Text,
        // m.video
        Video,
        // Unrecognized message type
        Unknown,
};

MessageType
getMessageType(const std::string &type);

MessageType
getMessageType(const json &obj);

struct Sticker : public RoomEvent<mtx::events::msg::StickerImage>
{};

} // namespace events
} // namespace mtx
