// SPDX-FileCopyrightText: 2025 Contributors to Chatterino <https://chatterino.com>
//
// SPDX-License-Identifier: MIT

#ifdef CHATTERINO_HAVE_PLUGINS
#    include "controllers/plugins/api/Message.hpp"

#    include "Application.hpp"
#    include "controllers/plugins/LuaUtilities.hpp"
#    include "controllers/plugins/SolTypes.hpp"
#    include "messages/Message.hpp"
#    include "messages/MessageElement.hpp"

#    include <sol/sol.hpp>

namespace {

using namespace chatterino;

QDateTime datetimeFromOffset(qint64 offset)
{
    auto dt = QDateTime::fromMSecsSinceEpoch(offset);

#    ifdef CHATTERINO_WITH_TESTS
    if (getApp()->isTest())
    {
        return dt.toUTC();
    }
#    endif

    return dt;
}

template <typename T>
T requiredGet(const sol::table &tbl, auto &&key)
{
    auto v = tbl.get<sol::optional<T>>(std::forward<decltype(key)>(key));
    if (!v)
    {
        throw std::runtime_error(std::string{"Missing required property: "} +
                                 key);
    }
    return *std::move(v);
}

std::unique_ptr<TextElement> textElementFromTable(const sol::table &tbl)
{
    return std::make_unique<TextElement>(
        requiredGet<QString>(tbl, "text"),
        tbl.get_or("flags", MessageElementFlag::Text),
        MessageColor::fromLua(tbl.get_or("color", QString{})),
        tbl.get_or("style", FontStyle::ChatMedium));
}

std::unique_ptr<SingleLineTextElement> singleLineTextElementFromTable(
    const sol::table &tbl)
{
    return std::make_unique<SingleLineTextElement>(
        requiredGet<QString>(tbl, "text"),
        tbl.get_or("flags", MessageElementFlag::Text),
        MessageColor::fromLua(tbl.get_or("color", QString{})),
        tbl.get_or("style", FontStyle::ChatMedium));
}

std::unique_ptr<MentionElement> mentionElementFromTable(const sol::table &tbl)
{
    // no flags!
    return std::make_unique<MentionElement>(
        requiredGet<QString>(tbl, "display_name"),
        requiredGet<QString>(tbl, "login_name"),
        MessageColor::fromLua(requiredGet<QString>(tbl, "fallback_color")),
        MessageColor::fromLua(requiredGet<QString>(tbl, "user_color")));
}

std::unique_ptr<TimestampElement> timestampElementFromTable(
    const sol::table &tbl)
{
    // no flags!
    auto time = tbl.get<std::optional<qint64>>("time");
    if (time)
    {
        return std::make_unique<TimestampElement>(
            datetimeFromOffset(*time).time());
    }
    return std::make_unique<TimestampElement>();
}

std::unique_ptr<TwitchModerationElement> twitchModerationElementFromTable()
{
    // no flags!
    return std::make_unique<TwitchModerationElement>();
}

std::unique_ptr<LinebreakElement> linebreakElementFromTable(
    const sol::table &tbl)
{
    return std::make_unique<LinebreakElement>(
        tbl.get_or("flags", MessageElementFlag::None));
}

std::unique_ptr<ReplyCurveElement> replyCurveElementFromTable()
{
    // no flags!
    return std::make_unique<ReplyCurveElement>();
}

std::unique_ptr<ImageElement> imageElementFromTable(const sol::table &tbl)
{
    return std::make_unique<ImageElement>(
        requiredGet<ImagePtr>(tbl, "image"),
        requiredGet<MessageElementFlag>(tbl, "flags"));
}

std::unique_ptr<CircularImageElement> circularImageElementFromTable(
    const sol::table &tbl)
{
    return std::make_unique<CircularImageElement>(
        requiredGet<ImagePtr>(tbl, "image"), requiredGet<int>(tbl, "padding"),
        QColor::fromString(requiredGet<std::string_view>(tbl, "background")),
        requiredGet<MessageElementFlag>(tbl, "flags"));
}

std::unique_ptr<ScalingImageElement> scalingImageElementFromTable(
    const sol::table &tbl)
{
    return std::make_unique<ScalingImageElement>(
        requiredGet<ImageSet>(tbl, "images"),
        requiredGet<MessageElementFlag>(tbl, "flags"));
}

void setLinkOn(MessageElement *el, const Link &link)
{
    el->setLink(link);
    QString tooltip;

    switch (link.type)
    {
        case Link::Url:
            tooltip = QString("<b>URL:</b> %1").arg(link.value);
            break;
        case Link::UserAction:
            tooltip = QString("<b>Command:</b> %1").arg(link.value);
            break;
        case Link::CopyToClipboard:
            tooltip = "<b>Copy to clipboard</b>";
            break;

        // these links should be safe to click as they don't have any immediate action associated with them
        case Link::InsertText:
        case Link::JumpToChannel:
        case Link::JumpToMessage:
        case Link::UserInfo:
        case Link::UserWhisper:
        case Link::ReplyToMessage:
            break;

        // these types are not exposed to plugins
        case Link::None:
        case Link::AutoModAllow:
        case Link::AutoModDeny:
        case Link::OpenAccountsPage:
        case Link::Reconnect:
        case Link::ViewThread:
            throw std::runtime_error("Invalid link type. How'd this happen?");
    }
    el->setTooltip(tooltip);
}

std::unique_ptr<MessageElement> elementFromTable(const sol::table &tbl)
{
    auto type = requiredGet<std::string>(tbl, "type");
    std::unique_ptr<MessageElement> el;
    bool linksAllowed = true;
    if (type == TextElement::TYPE)
    {
        el = textElementFromTable(tbl);
    }
    else if (type == SingleLineTextElement::TYPE)
    {
        el = singleLineTextElementFromTable(tbl);
    }
    else if (type == MentionElement::TYPE)
    {
        el = mentionElementFromTable(tbl);
        linksAllowed = false;
    }
    else if (type == TimestampElement::TYPE)
    {
        el = timestampElementFromTable(tbl);
    }
    else if (type == TwitchModerationElement::TYPE)
    {
        el = twitchModerationElementFromTable();
    }
    else if (type == LinebreakElement::TYPE)
    {
        el = linebreakElementFromTable(tbl);
    }
    else if (type == ReplyCurveElement::TYPE)
    {
        el = replyCurveElementFromTable();
        linksAllowed = false;
    }
    else if (type == ImageElement::TYPE)
    {
        el = imageElementFromTable(tbl);
    }
    else if (type == CircularImageElement::TYPE)
    {
        el = circularImageElementFromTable(tbl);
    }
    else if (type == ScalingImageElement::TYPE)
    {
        el = scalingImageElementFromTable(tbl);
    }
    else
    {
        throw std::runtime_error("Invalid message type");
    }
    assert(el);

    el->setTrailingSpace(tbl.get_or("trailing_space", true));

    auto link = tbl.get<sol::optional<Link>>("link");
    if (link)
    {
        if (!linksAllowed)
        {
            throw std::runtime_error("'link' not supported on type='" + type +
                                     '\'');
        }
        setLinkOn(el.get(), *link);
    }
    else
    {
        el->setTooltip(tbl.get_or("tooltip", QString{}));
    }

    return el;
}

void checkWritable(Message *msg)
{
    if (msg->frozen)
    {
        throw std::runtime_error("Message is frozen");
    }
}

template <typename T>
struct MemberPtrTraits;

template <class T, class C>
struct MemberPtrTraits<T C::*> {
    using Type = T;
    using Object = C;
};

template <auto T>
decltype(auto) memberAccessor()
{
    return sol::property(
        [](Message *msg) {
            return msg->*T;
        },
        [](Message *msg, typename MemberPtrTraits<decltype(T)>::Type v) {
            checkWritable(msg);
            msg->*T = std::forward<decltype(v)>(v);
        });
}

std::shared_ptr<Message> messageFromTable(const sol::table &tbl);

}  // namespace

namespace chatterino::lua::api::message {

struct ElementRef {
    ElementRef() = default;
    ElementRef(std::shared_ptr<Message> msg, size_t index)
        : msg(std::move(msg))
        , index(index)
    {
    }

    MessageElement *element() const
    {
        if (!this->msg || this->index >= this->msg->elements.size())
        {
            return nullptr;
        }
        checkWritable(this->msg.get());
        return this->msg->elements[this->index].get();
    }

    const MessageElement *constElement() const
    {
        if (!this->msg || this->index >= this->msg->elements.size())
        {
            return nullptr;
        }
        return this->msg->elements[this->index].get();
    }

    MessageElement &ref() const
    {
        auto *el = this->element();
        if (!el)
        {
            throw std::runtime_error("Element does not exist or expired");
        }
        return *el;
    }

    const MessageElement &cref() const
    {
        const auto *el = this->constElement();
        if (!el)
        {
            throw std::runtime_error("Element does not exist or expired");
        }
        return *el;
    }

    /// Cast this element to `T`. Otherwise nullopt is returned.
    /// Use `.map()` to access the content.
    template <typename T>
    sol::optional<T &> as() const
    {
        // using ref() to error if the reference is invalid
        auto *el = dynamic_cast<T *>(&this->ref());
        if (!el)
        {
            return sol::nullopt;
        }
        return *el;
    }

    /// Cast this element to `const T`. Otherwise nullopt is returned.
    /// Use `.map()` to access the content.
    template <typename T>
    sol::optional<const T &> asConst() const
    {
        // using cref() to error if the reference is invalid
        const auto *el = dynamic_cast<const T *>(&this->cref());
        if (!el)
        {
            return sol::nullopt;
        }
        return *el;
    }

    template <typename T>
    bool is() const
    {
        return dynamic_cast<const T *>(&this->cref()) != nullptr;
    }

    /// Visit this element by dynamic casting
    template <typename... T>
    auto visit(auto &&...cb) const
    {
        static_assert(sizeof...(T) == sizeof...(cb) && sizeof...(T) > 0);

        // infer the returned type inside the optional
        using Cb0 = std::tuple_element_t<0, std::tuple<decltype(cb)...>>;
        using T0 = std::tuple_element_t<0, std::tuple<T...>>;
        using TReturn = std::invoke_result_t<Cb0, T0 &>;

        return this->visitOne<TReturn, T...>(std::forward<decltype(cb)>(cb)...);
    }

    bool operator==(const ElementRef &rhs) const
    {
        return this->msg.get() == rhs.msg.get() && this->index == rhs.index;
    }

    std::shared_ptr<Message> msg;
    size_t index = 0;

private:
    template <bool Const>
    decltype(auto) maybeConstElement() const
    {
        if constexpr (Const)
        {
            return this->constElement();
        }
        else
        {
            return this->element();
        }
    }

    /// Run one callback
    ///
    /// This is called recursively.
    /// If the callback returns something, we return an `optional<T>` otherwise
    /// we return `void`.
    template <typename TReturn, typename T, typename... Rest>
    auto visitOne(auto &&cb, auto &&...rest) const
        -> std::conditional_t<std::is_void_v<TReturn>, void,
                              sol::optional<TReturn>>
    {
        auto *el =
            dynamic_cast<T *>(this->maybeConstElement<std::is_const_v<T>>());
        if (!el)
        {
            if constexpr (sizeof...(rest) == 0)
            {
                if constexpr (std::is_void_v<
                                  std::invoke_result_t<decltype(cb), T &>>)
                {
                    return;
                }
                else
                {
                    return sol::nullopt;
                }
            }
            else
            {
                return this->visitOne<TReturn, Rest...>(
                    std::forward<decltype(rest)>(rest)...);
            }
        }
        return std::invoke(cb, *el);
    }
};

struct ElementIterator {
    using difference_type = std::ptrdiff_t;
    using value_type = ElementRef;

    ElementIterator() = default;
    ElementIterator(std::shared_ptr<Message> msg, size_t index)
        : current(std::move(msg), index)
    {
    }

    ElementRef operator*() const
    {
        return this->current;
    }
    ElementRef operator[](size_t i) const
    {
        return {this->current.msg, this->current.index + i};
    }

    ElementIterator &operator+=(difference_type diff)
    {
        this->current.index += diff;
        return *this;
    }
    ElementIterator &operator++()
    {
        return *this += 1;
    }
    ElementIterator operator++(int)  // postfix increment
    {
        auto tmp = *this;
        ++*this;
        return tmp;
    }
    friend ElementIterator operator+(difference_type diff,
                                     const ElementIterator &it)
    {
        return {it.current.msg, it.current.index + diff};
    }
    friend ElementIterator operator+(const ElementIterator &it,
                                     difference_type diff)
    {
        return {it.current.msg, it.current.index + diff};
    }

    ElementIterator &operator-=(difference_type diff)
    {
        this->current.index -= diff;
        return *this;
    }
    ElementIterator &operator--()
    {
        return *this -= 1;
    }
    ElementIterator operator--(int)  // postfix decrement
    {
        auto tmp = *this;
        --*this;
        return tmp;
    }

    friend difference_type operator-(const ElementIterator &lhs,
                                     const ElementIterator &rhs)
    {
        return static_cast<difference_type>(lhs.current.index) -
               static_cast<difference_type>(rhs.current.index);
    }
    friend ElementIterator operator-(difference_type diff,
                                     const ElementIterator &it)
    {
        return {it.current.msg, it.current.index - diff};
    }
    friend ElementIterator operator-(const ElementIterator &it,
                                     difference_type diff)
    {
        return {it.current.msg, it.current.index - diff};
    }

    bool operator==(const ElementIterator &rhs) const
    {
        return this->current == rhs.current;
    }
    bool operator<(const ElementIterator &rhs) const
    {
        return this->current.index < rhs.current.index;
    }
    bool operator<=(const ElementIterator &rhs) const
    {
        return this->current.index <= rhs.current.index;
    }
    bool operator>(const ElementIterator &rhs) const
    {
        return this->current.index > rhs.current.index;
    }
    bool operator>=(const ElementIterator &rhs) const
    {
        return this->current.index >= rhs.current.index;
    }

    ElementRef current;
};

static_assert(std::random_access_iterator<ElementIterator>);

struct MessageElements {
    using value_type = ElementIterator::value_type;
    using iterator = ElementIterator;
    using size_type = size_t;

    explicit MessageElements(std::shared_ptr<Message> msg)
        : msg(std::move(msg))
    {
    }
    ~MessageElements() = default;

    MessageElements(const MessageElements &) = delete;
    MessageElements(MessageElements &&) = default;
    MessageElements &operator=(const MessageElements &) = delete;
    MessageElements &operator=(MessageElements &&) = default;

    ElementIterator begin() const
    {
        return {this->msg, 0};
    }
    ElementIterator end() const
    {
        return {this->msg, this->msg->elements.size()};
    }

    size_type size() const
    {
        if (!this->msg)
        {
            return 0;
        }
        return this->msg->elements.size();
    }

    // NOLINTNEXTLINE
    size_type max_size() const
    {
        return this->size();  // we can't insert
    }

    bool empty() const
    {
        if (!this->msg)
        {
            return true;
        }
        return this->msg->elements.empty();
    }

    // NOLINTNEXTLINE
    void push_back(ElementIterator::value_type /* v */) const
    {
        throw std::runtime_error("Insertion is not supported");
    }

    // NOLINTNEXTLINE
    void erase(ElementIterator it) const
    {
        if (it.current.msg != this->msg || !this->msg ||
            it.current.index >= this->msg->elements.size())
        {
            throw std::runtime_error("Can't erase here");
        }
        checkWritable(this->msg.get());
        this->msg->elements.erase(this->msg->elements.begin() +
                                  static_cast<ptrdiff_t>(it.current.index));
    }

    std::shared_ptr<Message> msg;
};

void createUserType(sol::table &c2)
{
    c2.new_usertype<ElementRef>(
        "MessageElement", sol::no_constructor,  //
        "type", sol::property([](const ElementRef &el) {
            return el.cref().type();
        }),
        "flags", sol::property([](const ElementRef &el) {
            return el.cref().getFlags().value();
        }),
        "add_flags",
        [](const ElementRef &el, MessageElementFlag flag) {
            el.ref().addFlags(flag);
        },
        "link",
        sol::property(
            [](const ElementRef &el) {
                return el.cref().getLink();
            },
            [](const ElementRef &el, const Link &link) {
                if (el.is<MentionElement>() || el.is<LinkElement>())
                {
                    throw std::runtime_error(
                        "Setting a link on this element is unsupported");
                }
                setLinkOn(&el.ref(), link);
            }),
        "tooltip",
        sol::property(
            [](const ElementRef &el) {
                return el.cref().getTooltip();
            },
            [](const ElementRef &el, const QString &tooltip) {
                el.ref().setTooltip(tooltip);
            }),
        "trailing_space",
        sol::property(
            [](const ElementRef &el) {
                return el.cref().hasTrailingSpace();
            },
            [](const ElementRef &el, bool trailingSpace) {
                el.ref().setTrailingSpace(trailingSpace);
            }),
        "padding", sol::property([](const ElementRef &el) {
            return el.asConst<CircularImageElement>().map(
                &CircularImageElement::padding);
        }),
        "background", sol::property([](const ElementRef &el) {
            return el.asConst<CircularImageElement>().map(
                [](const CircularImageElement &el) {
                    return el.background().name(QColor::HexArgb);
                });
        }),
        "images", sol::property([](const ElementRef &el) {
            return el.asConst<ScalingImageElement>().map(
                &ScalingImageElement::images);
        }),
        "image", sol::property([](const ElementRef &el) {
            return el.visit<const ImageElement, const CircularImageElement>(
                &ImageElement::image, &CircularImageElement::image);
        }),
        "words", sol::property([](const ElementRef &el) {
            return el.visit<const TextElement, const SingleLineTextElement>(
                &TextElement::words, &SingleLineTextElement::words);
        }),
        "color", sol::property([](const ElementRef &el) {
            return el.visit<const TextElement, const SingleLineTextElement>(
                [](const TextElement &el) {
                    return el.color().toLua();
                },
                [](const SingleLineTextElement &el) {
                    return el.color().toLua();
                });
        }),
        "style", sol::property([](const ElementRef &el) {
            return el.visit<const TextElement, const SingleLineTextElement>(
                &TextElement::fontStyle, &SingleLineTextElement::fontStyle);
        }),
        "lowercase", sol::property([](const ElementRef &el) {
            return el.asConst<LinkElement>().map(&LinkElement::lowercase);
        }),
        "original", sol::property([](const ElementRef &el) {
            return el.asConst<LinkElement>().map(&LinkElement::original);
        }),
        "fallback_color", sol::property([](const ElementRef &el) {
            return el.asConst<MentionElement>().map(
                [](const MentionElement &el) {
                    return el.fallbackColor().toLua();
                });
        }),
        "user_color", sol::property([](const ElementRef &el) {
            return el.asConst<MentionElement>().map(
                [](const MentionElement &el) {
                    return el.userColor().toLua();
                });
        }),
        "user_login_name", sol::property([](const ElementRef &el) {
            return el.asConst<MentionElement>().map(
                &MentionElement::userLoginName);
        }),
        "time", sol::property([](const ElementRef &el) {
            return el.asConst<TimestampElement>().map(
                [](const TimestampElement &el) {
                    return QDateTime(QDate::currentDate(), el.time())
                        .toMSecsSinceEpoch();
                });
        }));

    c2.new_usertype<Message>(
        "Message", sol::factories([](const sol::table &tbl) {
            return messageFromTable(tbl);
        }),
        "flags",
        sol::property(
            [](Message *msg) {
                return msg->flags.value();
            },
            [](Message *msg, MessageFlag f) {
                // flags are always mutable
                msg->flags = f;
            }),
        "parse_time",
        sol::property(
            [](Message *msg) {
                return QDateTime(QDate::currentDate(), msg->parseTime)
                    .toMSecsSinceEpoch();
            },
            [](Message *msg, qint64 ms) {
                checkWritable(msg);
                msg->parseTime = datetimeFromOffset(ms).time();
            }),
        "id", memberAccessor<&Message::id>(),                         //
        "search_text", memberAccessor<&Message::searchText>(),        //
        "message_text", memberAccessor<&Message::messageText>(),      //
        "login_name", memberAccessor<&Message::loginName>(),          //
        "display_name", memberAccessor<&Message::displayName>(),      //
        "localized_name", memberAccessor<&Message::localizedName>(),  //
        "user_id", memberAccessor<&Message::userID>(),                //
        "channel_name", memberAccessor<&Message::channelName>(),      //
        "username_color",
        sol::property(
            [](Message *msg) {
                return msg->usernameColor.name(QColor::HexArgb);
            },
            [](Message *msg, std::string_view sv) {
                checkWritable(msg);
                msg->usernameColor = QColor::fromString(sv);
            }),
        "server_received_time",
        sol::property(
            [](Message *msg) {
                return msg->serverReceivedTime.toMSecsSinceEpoch();
            },
            [](Message *msg, qint64 ms) {
                checkWritable(msg);
                msg->serverReceivedTime = datetimeFromOffset(ms);
            }),
        "highlight_color",
        sol::property(
            [](Message *msg) {
                if (!msg->highlightColor)
                {
                    return QString{};
                }
                return msg->highlightColor->name(QColor::HexArgb);
            },
            [](Message *msg, std::string_view sv) {
                checkWritable(msg);
                if (sv.empty())
                {
                    msg->highlightColor.reset();
                }
                else
                {
                    msg->highlightColor =
                        std::make_shared<QColor>(QColor::fromString(sv));
                }
            }),
        // must be read only (but it might be helpful for generic Lua functions)
        "frozen", sol::property([](Message *msg) {
            return msg->frozen;
        }),
        "elements",
        [](const std::shared_ptr<Message> &msg) {
            return MessageElements(msg);
        },
        "append_element",
        sol::overload(
            // Message:append_element(MessageElementInit)
            // Create a new element
            [](Message *msg, const sol::table &tbl) {
                checkWritable(msg);
                auto el = elementFromTable(tbl);
                if (el)
                {
                    msg->elements.emplace_back(std::move(el));
                }
            },
            // Message:append_element(MessageElement)
            // Create a clone of the given element and add it to the message
            [](Message *msg, ElementRef &element) {
                checkWritable(msg);
                msg->elements.emplace_back(element.constElement()->clone());
            }));
}

}  // namespace chatterino::lua::api::message

namespace {

using namespace chatterino;

std::shared_ptr<Message> messageFromTable(const sol::table &tbl)
{
    auto msg = std::make_shared<Message>();
    msg->flags = tbl.get_or("flags", MessageFlag::None);

    // This takes a UTC offset (not the milliseconds since the start of the day)
    auto parseTime = tbl.get<std::optional<qint64>>("parse_time");
    if (parseTime)
    {
        msg->parseTime = datetimeFromOffset(*parseTime).time();
    }

    msg->id = tbl.get_or("id", QString{});
    msg->searchText = tbl.get_or("search_text", QString{});
    msg->messageText = tbl.get_or("message_text", QString{});
    msg->loginName = tbl.get_or("login_name", QString{});
    msg->displayName = tbl.get_or("display_name", QString{});
    msg->localizedName = tbl.get_or("localized_name", QString{});
    msg->userID = tbl.get_or("user_id", QString{});
    // missing: timeoutUser
    msg->channelName = tbl.get_or("channel_name", QString{});

    auto usernameColor = tbl.get_or("username_color", QString{});
    if (!usernameColor.isEmpty())
    {
        msg->usernameColor = QColor(usernameColor);
    }

    auto serverReceivedTime =
        tbl.get<std::optional<qint64>>("server_received_time");
    if (serverReceivedTime)
    {
        msg->serverReceivedTime = datetimeFromOffset(*serverReceivedTime);
    }

    // missing: badges
    // missing: badgeInfos

    // we construct a color on the fly here
    auto highlightColor = tbl.get_or("highlight_color", QString{});
    if (!highlightColor.isEmpty())
    {
        msg->highlightColor = std::make_shared<QColor>(highlightColor);
    }

    // missing: replyThread
    // missing: replyParent
    // missing: count

    auto elements = tbl.get<std::optional<sol::table>>("elements");
    if (elements)
    {
        auto size = elements->size();
        for (size_t i = 1; i <= size; i++)
        {
            auto ref =
                elements->get<std::optional<lua::api::message::ElementRef>>(i);
            if (ref.has_value())
            {
                msg->elements.emplace_back(ref->constElement()->clone());
            }
            else
            {
                msg->elements.emplace_back(
                    elementFromTable(elements->get<sol::table>(i)));
            }
        }
    }

    // missing: reward
    return msg;
}

}  // namespace

#endif
