#pragma once

#include <concepts>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>

namespace moza::internal {

template <typename T>
struct Reflect;

template <typename T>
concept Reflectable = requires {
    Reflect<T>::visit(std::declval<T&>(), [](auto&) {});
    Reflect<T>::visit(std::declval<const T&>(), [](const auto&) {});
};

template <typename T>
struct ValueCodec;

template <typename T>
concept CodecSupported = requires(const T& value, const Value& encoded) {
    { ValueCodec<T>::encode(value) } -> std::same_as<Value>;
    { ValueCodec<T>::decode(encoded) } -> std::same_as<T>;
};

template <typename T>
    requires std::is_integral_v<T> && !std::is_same_v<T, bool>
struct ValueCodec<T>
{
    static Value encode(T value)
    {
        if constexpr (std::is_signed_v<T>)
        {
            return Value(static_cast<int64_t>(value));
        }
        else
        {
            return Value(static_cast<uint64_t>(value));
        }
    }

    static T decode(const Value& value)
    {
        if constexpr (std::is_signed_v<T>)
        {
            return static_cast<T>(value.asInt());
        }
        else
        {
            return static_cast<T>(value.asUInt());
        }
    }
};

template <typename T>
    requires std::is_floating_point_v<T>
struct ValueCodec<T>
{
    static Value encode(T value)
    {
        return Value(static_cast<double>(value));
    }

    static T decode(const Value& value)
    {
        return static_cast<T>(value.asDouble());
    }
};

template <>
struct ValueCodec<bool>
{
    static Value encode(bool value)
    {
        return Value(value);
    }

    static bool decode(const Value& value)
    {
        return value.asBool();
    }
};

template <>
struct ValueCodec<std::string>
{
    static Value encode(const std::string& value)
    {
        return Value(value);
    }

    static std::string decode(const Value& value)
    {
        return value.asString();
    }
};

template <>
struct ValueCodec<std::vector<std::uint8_t>>
{
    static Value encode(const std::vector<std::uint8_t>& value)
    {
        return Value(value);
    }

    static std::vector<std::uint8_t> decode(const Value& value)
    {
        return value.asBinary();
    }
};

template <typename T>
    requires std::is_enum_v<T>
struct ValueCodec<T>
{
    static Value encode(T value)
    {
        return ValueCodec<std::underlying_type_t<T>>::encode(static_cast<std::underlying_type_t<T>>(value));
    }

    static T decode(const Value& value)
    {
        return static_cast<T>(ValueCodec<std::underlying_type_t<T>>::decode(value));
    }
};

template <typename T>
    requires CodecSupported<T> && (!std::is_same_v<T, std::vector<std::uint8_t>>)
struct ValueCodec<std::vector<T>>
{
    static Value encode(const std::vector<T>& value)
    {
        std::vector<Value> items;
        items.reserve(value.size());
        for (const auto& item : value)
        {
            items.push_back(ValueCodec<T>::encode(item));
        }
        return Value(items);
    }

    static std::vector<T> decode(const Value& value)
    {
        std::vector<T> result;
        const auto     items = value.asList();
        result.reserve(items.size());
        for (const auto& item : items)
        {
            result.push_back(ValueCodec<T>::decode(item));
        }
        return result;
    }
};

template <typename T>
    requires Reflectable<T>
struct ValueCodec<T>
{
    static Value encode(const T& value)
    {
        std::vector<Value> items;
        Reflect<T>::visit(value, [&]<typename U>(const U& fieldValue) {
            using FieldType = std::remove_cvref_t<U>;
            items.push_back(ValueCodec<FieldType>::encode(fieldValue));
        });
        return Value(items);
    }

    static T decode(const Value& value)
    {
        T           result{};
        const auto  items = value.asList();
        std::size_t index = 0;
        Reflect<T>::visit(result, [&]<typename U>(U& fieldValue) {
            using FieldType = std::remove_cvref_t<U>;
            if (index < items.size())
            {
                fieldValue = ValueCodec<FieldType>::decode(items[index]);
            }
            ++index;
        });
        return result;
    }
};

} // namespace moza::internal

namespace moza {

struct NoPayload {};

class ParameterBase
{
public:
    ParameterBase() = default;
    explicit ParameterBase(RawParameter parameter) : m_parameter(std::move(parameter)) {}

    bool         isValid() const { return m_parameter.isValid(); }
    DeviceHandle deviceHandle() const { return m_parameter.deviceHandle(); }
    ParameterId  id() const { return m_parameter.id(); }

    Errno getInfo(ParameterInfo* info) const
    {
        return m_parameter.getInfo(info);
    }

    std::pair<Errno, ParameterInfo> getInfo() const
    {
        return m_parameter.getInfo();
    }

    const RawParameter& raw() const { return m_parameter; }
    RawParameter&       raw() { return m_parameter; }

    explicit operator bool() const { return isValid(); }
    operator RawParameter() const { return m_parameter; }

protected:
    const RawParameter& parameter() const { return m_parameter; }

private:
    RawParameter m_parameter;
};

template <typename TRead, typename TWrite = TRead>
class Parameter : public ParameterBase
{
public:
    Parameter() = default;
    explicit Parameter(RawParameter parameter) : ParameterBase(std::move(parameter)) {}

    Errno fetch() const
    {
        return this->parameter().fetch();
    }

    std::pair<Errno, TRead> fetchSync(int32_t timeoutMs) const
        requires internal::CodecSupported<TRead>
    {
        auto [err, value] = this->parameter().fetchSync(timeoutMs);
        if (err != MOZA_ERR_NO_ERROR)
        {
            return {err, TRead{}};
        }
        return {err, internal::ValueCodec<TRead>::decode(value)};
    }

    std::pair<Errno, TRead> get() const
        requires internal::CodecSupported<TRead>
    {
        auto [err, value] = this->parameter().getValue();
        if (err != MOZA_ERR_NO_ERROR)
        {
            return {err, TRead{}};
        }
        return {err, internal::ValueCodec<TRead>::decode(value)};
    }

    std::pair<Errno, TRead> getSync(int32_t timeoutMs) const
        requires internal::CodecSupported<TRead>
    {
        auto [err, value] = this->parameter().getValueSync(timeoutMs);
        if (err != MOZA_ERR_NO_ERROR)
        {
            return {err, TRead{}};
        }
        return {err, internal::ValueCodec<TRead>::decode(value)};
    }

    Errno set(const TWrite& value) const
        requires internal::CodecSupported<TWrite>
    {
        return this->parameter().setValue(internal::ValueCodec<TWrite>::encode(value));
    }

    std::pair<Errno, TRead> setSync(const TWrite& value, int32_t timeoutMs) const
        requires internal::CodecSupported<TRead> && internal::CodecSupported<TWrite>
    {
        auto [err, resultValue] = this->parameter().setValueSync(internal::ValueCodec<TWrite>::encode(value), timeoutMs);
        if (err != MOZA_ERR_NO_ERROR)
        {
            return {err, TRead{}};
        }
        return {err, internal::ValueCodec<TRead>::decode(resultValue)};
    }
};

template <typename TRead>
class Parameter<TRead, void> : public ParameterBase
{
public:
    Parameter() = default;
    explicit Parameter(RawParameter parameter) : ParameterBase(std::move(parameter)) {}

    Errno fetch() const
    {
        return this->parameter().fetch();
    }

    std::pair<Errno, TRead> fetchSync(int32_t timeoutMs) const
        requires internal::CodecSupported<TRead>
    {
        auto [err, value] = this->parameter().fetchSync(timeoutMs);
        if (err != MOZA_ERR_NO_ERROR)
        {
            return {err, TRead{}};
        }
        return {err, internal::ValueCodec<TRead>::decode(value)};
    }

    std::pair<Errno, TRead> get() const
        requires internal::CodecSupported<TRead>
    {
        auto [err, value] = this->parameter().getValue();
        if (err != MOZA_ERR_NO_ERROR)
        {
            return {err, TRead{}};
        }
        return {err, internal::ValueCodec<TRead>::decode(value)};
    }

    std::pair<Errno, TRead> getSync(int32_t timeoutMs) const
        requires internal::CodecSupported<TRead>
    {
        auto [err, value] = this->parameter().getValueSync(timeoutMs);
        if (err != MOZA_ERR_NO_ERROR)
        {
            return {err, TRead{}};
        }
        return {err, internal::ValueCodec<TRead>::decode(value)};
    }
};

template <typename TWrite>
class Parameter<void, TWrite> : public ParameterBase
{
public:
    Parameter() = default;
    explicit Parameter(RawParameter parameter) : ParameterBase(std::move(parameter)) {}

    Errno set(const TWrite& value) const
        requires internal::CodecSupported<TWrite>
    {
        return this->parameter().setValue(internal::ValueCodec<TWrite>::encode(value));
    }

    Errno setSync(const TWrite& value, int32_t timeoutMs) const
        requires internal::CodecSupported<TWrite>
    {
        return this->parameter().setValueSync(internal::ValueCodec<TWrite>::encode(value), timeoutMs).first;
    }
};

template <typename T>
    requires (!std::is_void_v<T>)
using ReadOnlyParameter = Parameter<T, void>;

template <typename T>
    requires (!std::is_void_v<T>)
using WriteOnlyParameter = Parameter<void, T>;

template <typename TRequest = NoPayload, typename TResponse = NoPayload>
class Command
{
public:
    Command() = default;
    explicit Command(RawCommand command) : m_command(std::move(command)) {}

    bool         isValid() const { return m_command.isValid(); }
    DeviceHandle deviceHandle() const { return m_command.deviceHandle(); }
    CommandId    id() const { return m_command.id(); }

    Errno getInfo(CommandInfo* info) const
    {
        return m_command.getInfo(info);
    }

    std::pair<Errno, CommandInfo> getInfo() const
    {
        return m_command.getInfo();
    }

    Errno send() const
        requires std::is_same_v<TRequest, NoPayload>
    {
        return m_command.send();
    }

    template <typename U = TRequest>
        requires (!std::is_same_v<U, NoPayload>) && internal::CodecSupported<U>
    Errno send(const U& request) const
    {
        return m_command.send(internal::ValueCodec<U>::encode(request));
    }

    Errno sendSync(int32_t timeoutMs) const
        requires std::is_same_v<TRequest, NoPayload> && std::is_same_v<TResponse, NoPayload>
    {
        return m_command.sendSync(timeoutMs).first;
    }

    template <typename U = TRequest>
        requires (!std::is_same_v<U, NoPayload>) && internal::CodecSupported<U> && std::is_same_v<TResponse, NoPayload>
    Errno sendSync(const U& request, int32_t timeoutMs) const
    {
        return m_command.sendSync(internal::ValueCodec<U>::encode(request), timeoutMs).first;
    }

    template <typename U = TResponse>
        requires std::is_same_v<TRequest, NoPayload> && (!std::is_same_v<U, NoPayload>) && internal::CodecSupported<U>
    std::pair<Errno, U> sendSync(int32_t timeoutMs) const
    {
        auto [err, value] = m_command.sendSync(timeoutMs);
        if (err != MOZA_ERR_NO_ERROR)
        {
            return {err, U{}};
        }
        return {err, internal::ValueCodec<U>::decode(value)};
    }

    template <typename URequest = TRequest, typename UResponse = TResponse>
        requires (!std::is_same_v<URequest, NoPayload>) && internal::CodecSupported<URequest>
        && (!std::is_same_v<UResponse, NoPayload>) && internal::CodecSupported<UResponse>
    std::pair<Errno, UResponse> sendSync(const URequest& request, int32_t timeoutMs) const
    {
        auto [err, value] = m_command.sendSync(internal::ValueCodec<URequest>::encode(request), timeoutMs);
        if (err != MOZA_ERR_NO_ERROR)
        {
            return {err, UResponse{}};
        }
        return {err, internal::ValueCodec<UResponse>::decode(value)};
    }

    const RawCommand& raw() const { return m_command; }
    RawCommand&       raw() { return m_command; }

    explicit operator bool() const { return isValid(); }
    operator RawCommand() const { return m_command; }

private:
    RawCommand m_command;
};

#define MOZA_DECLARE_DEVICE_PARAM(Type, Name, Id)                 \
    [[nodiscard]] Type Name() const                              \
    {                                                            \
        return Type(parameter(Id));                              \
    }

#define MOZA_DECLARE_DEVICE_CMD(Type, Name, Id)                  \
    [[nodiscard]] Type Name() const                              \
    {                                                            \
        return Type(command(Id));                                \
    }

#define MOZA_DECLARE_GENERATED_PARAM(Name, ConstantName, Id, ...)   \
    static constexpr ParameterId ConstantName = Id;                \
    [[nodiscard]] __VA_ARGS__ Name() const                         \
    {                                                              \
        return __VA_ARGS__(parameter(ConstantName));               \
    }

#define MOZA_DECLARE_GENERATED_CMD(Name, ConstantName, Id, ...)     \
    static constexpr CommandId ConstantName = Id;                  \
    [[nodiscard]] __VA_ARGS__ Name() const                         \
    {                                                              \
        return __VA_ARGS__(command(ConstantName));                 \
    }

#define MOZA_DECLARE_REFLECT(Type, ...)                             \
    template <>                                                     \
    struct Reflect<Type>                                            \
    {                                                               \
        using ReflectedType = Type;                                 \
        template <typename Fn>                                      \
        static void visit(ReflectedType& value, Fn&& fn)            \
        {                                                           \
            visitImpl(value, std::forward<Fn>(fn));                 \
        }                                                           \
        template <typename Fn>                                      \
        static void visit(const ReflectedType& value, Fn&& fn)      \
        {                                                           \
            visitImpl(value, std::forward<Fn>(fn));                 \
        }                                                           \
                                                                       \
    private:                                                        \
        template <typename TSelf, typename Fn>                      \
        static void visitImpl(TSelf& value, Fn&& fn)                \
        {                                                           \
            __VA_ARGS__                                             \
        }                                                           \
    };

} // namespace moza
