/*
 * Copyright (c) 2018-2020 Atmosphère-NX
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License
 * version 2, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
#pragma once
#include 
#include 
namespace ams::svc::ipc {
    ALWAYS_INLINE u32 *GetMessageBuffer() {
        return GetThreadLocalRegion()->message_buffer;
    }
    constexpr inline size_t MessageBufferSize = sizeof(::ams::svc::ThreadLocalRegion::message_buffer);
    class MessageBuffer {
        public:
            class MessageHeader {
                private:
                    /* Define fields for the first header word. */
                    using Tag           = util::BitPack32::Field<0,                  BITSIZEOF(u16), u16>;
                    using PointerCount  = util::BitPack32::Field;
                    using SendCount     = util::BitPack32::Field;
                    using ReceiveCount  = util::BitPack32::Field;
                    using ExchangeCount = util::BitPack32::Field;
                    static_assert(ExchangeCount::Next == BITSIZEOF(u32));
                    /* Define fields for the second header word. */
                    using RawCount          = util::BitPack32::Field<0,                       10, s32>;
                    using ReceiveListCount  = util::BitPack32::Field;
                    using Reserved0         = util::BitPack32::Field;
                    using ReceiveListOffset = util::BitPack32::Field;
                    using HasSpecialHeader  = util::BitPack32::Field;
                    static constexpr inline u64 NullTag = 0;
                    static_assert(HasSpecialHeader::Next == BITSIZEOF(u32));
                public:
                    enum ReceiveListCountType {
                        ReceiveListCountType_None            = 0,
                        ReceiveListCountType_ToMessageBuffer = 1,
                        ReceiveListCountType_ToSingleBuffer  = 2,
                        ReceiveListCountType_CountOffset = 2,
                        ReceiveListCountType_CountMax    = 13,
                    };
                private:
                    util::BitPack32 header[2];
                public:
                    constexpr ALWAYS_INLINE MessageHeader() : header{util::BitPack32{0}, util::BitPack32{0}} {
                        this->header[0].Set(NullTag);
                    }
                    constexpr ALWAYS_INLINE MessageHeader(u16 tag, bool special, s32 ptr, s32 send, s32 recv, s32 exch, s32 raw, s32 recv_list) : header{util::BitPack32{0}, util::BitPack32{0}} {
                        this->header[0].Set(tag);
                        this->header[0].Set(ptr);
                        this->header[0].Set(send);
                        this->header[0].Set(recv);
                        this->header[0].Set(exch);
                        this->header[1].Set(raw);
                        this->header[1].Set(recv_list);
                        this->header[1].Set(special);
                    }
                    ALWAYS_INLINE explicit MessageHeader(const MessageBuffer &buf) : header{util::BitPack32{0}, util::BitPack32{0}} {
                        buf.Get(0, this->header, util::size(this->header));
                    }
                    ALWAYS_INLINE explicit MessageHeader(const u32 *msg) : header{util::BitPack32{msg[0]}, util::BitPack32{msg[1]}} { /* ... */ }
                    constexpr ALWAYS_INLINE u16 GetTag() const {
                        return this->header[0].Get();
                    }
                    constexpr ALWAYS_INLINE s32 GetPointerCount() const {
                        return this->header[0].Get();
                    }
                    constexpr ALWAYS_INLINE s32 GetSendCount() const {
                        return this->header[0].Get();
                    }
                    constexpr ALWAYS_INLINE s32 GetReceiveCount() const {
                        return this->header[0].Get();
                    }
                    constexpr ALWAYS_INLINE s32 GetExchangeCount() const {
                        return this->header[0].Get();
                    }
                    constexpr ALWAYS_INLINE s32 GetMapAliasCount() const {
                        return this->GetSendCount() + this->GetReceiveCount() + this->GetExchangeCount();
                    }
                    constexpr ALWAYS_INLINE s32 GetRawCount() const {
                        return this->header[1].Get();
                    }
                    constexpr ALWAYS_INLINE s32 GetReceiveListCount() const {
                        return this->header[1].Get();
                    }
                    constexpr ALWAYS_INLINE s32 GetReceiveListOffset() const {
                        return this->header[1].Get();
                    }
                    constexpr ALWAYS_INLINE bool GetHasSpecialHeader() const {
                        return this->header[1].Get();
                    }
                    constexpr ALWAYS_INLINE void SetReceiveListCount(s32 recv_list) {
                        this->header[1].Set(recv_list);
                    }
                    constexpr ALWAYS_INLINE const util::BitPack32 *GetData() const {
                        return this->header;
                    }
                    static constexpr ALWAYS_INLINE size_t GetDataSize() {
                        return sizeof(header);
                    }
            };
            class SpecialHeader {
                private:
                    /* Define fields for the header word. */
                    using HasProcessId    = util::BitPack32::Field<0,                     1, bool>;
                    using CopyHandleCount = util::BitPack32::Field;
                    using MoveHandleCount = util::BitPack32::Field;
                private:
                    util::BitPack32 header;
                    bool has_header;
                public:
                    constexpr ALWAYS_INLINE explicit SpecialHeader(bool pid, s32 copy, s32 move) : header{0}, has_header(true) {
                        this->header.Set(pid);
                        this->header.Set(copy);
                        this->header.Set(move);
                    }
                    ALWAYS_INLINE explicit SpecialHeader(const MessageBuffer &buf, const MessageHeader &hdr) : header{0}, has_header(hdr.GetHasSpecialHeader()) {
                        if (this->has_header) {
                            buf.Get(MessageHeader::GetDataSize() / sizeof(util::BitPack32), std::addressof(this->header), sizeof(this->header) / sizeof(util::BitPack32));
                        }
                    }
                    constexpr ALWAYS_INLINE bool GetHasProcessId() const {
                        return this->header.Get();
                    }
                    constexpr ALWAYS_INLINE bool GetCopyHandleCount() const {
                        return this->header.Get();
                    }
                    constexpr ALWAYS_INLINE bool GetMoveHandleCount() const {
                        return this->header.Get();
                    }
                    constexpr ALWAYS_INLINE const util::BitPack32 *GetHeader() const {
                        return std::addressof(this->header);
                    }
                    constexpr ALWAYS_INLINE size_t GetHeaderSize() const {
                        if (this->has_header) {
                            return sizeof(this->header);
                        } else {
                            return 0;
                        }
                    }
                    constexpr ALWAYS_INLINE size_t GetDataSize() const {
                        if (this->has_header) {
                            return (this->GetHasProcessId() ? sizeof(u64) : 0)   +
                                   (this->GetCopyHandleCount() * sizeof(Handle)) +
                                   (this->GetMoveHandleCount() * sizeof(Handle));
                        } else {
                            return 0;
                        }
                    }
            };
            class MapAliasDescriptor {
                public:
                    enum Attribute {
                        Attribute_Ipc          = 0,
                        Attribute_NonSecureIpc = 1,
                        Attribute_NonDeviceIpc = 3,
                    };
                private:
                    /* Define fields for the first two words. */
                    using SizeLow     = util::BitPack32::Field<0, BITSIZEOF(u32), u32>;
                    using AddressLow  = util::BitPack32::Field<0, BITSIZEOF(u32), u32>;
                    /* Define fields for the packed descriptor word. */
                    using Attributes  = util::BitPack32::Field<0,                  2, Attribute>;
                    using AddressHigh = util::BitPack32::Field;
                    using Reserved    = util::BitPack32::Field;
                    using SizeHigh    = util::BitPack32::Field;
                    using AddressMid  = util::BitPack32::Field;
                    constexpr ALWAYS_INLINE u32 GetAddressMid(u64 address) {
                        return static_cast(address >> AddressLow::Count) & ((1u << AddressMid::Count) - 1);
                    }
                    constexpr ALWAYS_INLINE u32 GetAddressHigh(u64 address) {
                        return static_cast(address >> (AddressLow::Count + AddressMid::Count));
                    }
                private:
                    util::BitPack32 data[3];
                public:
                    constexpr ALWAYS_INLINE MapAliasDescriptor() : data{util::BitPack32{0}, util::BitPack32{0}, util::BitPack32{0}} { /* ... */ }
                    ALWAYS_INLINE MapAliasDescriptor(const void *buffer, size_t _size, Attribute attr = Attribute_Ipc) : data{util::BitPack32{0}, util::BitPack32{0}, util::BitPack32{0}} {
                        const u64 address = reinterpret_cast(buffer);
                        const u64 size    = static_cast(_size);
                        this->data[0] = { static_cast(size) };
                        this->data[1] = { static_cast(address) };
                        this->data[2].Set(attr);
                        this->data[2].Set(GetAddressMid(address));
                        this->data[2].Set(static_cast(size >> SizeLow::Count));
                        this->data[2].Set(GetAddressHigh(address));
                    }
                    ALWAYS_INLINE MapAliasDescriptor(const MessageBuffer &buf, s32 index) : data{util::BitPack32{0}, util::BitPack32{0}, util::BitPack32{0}} {
                        buf.Get(index, this->data, util::size(this->data));
                    }
                    constexpr ALWAYS_INLINE uintptr_t GetAddress() const {
                        const u64 address = (static_cast((this->data[2].Get() << AddressMid::Count) | this->data[2].Get()) << AddressLow::Count) | this->data[1].Get();
                        return address;
                    }
                    constexpr ALWAYS_INLINE uintptr_t GetSize() const {
                        const u64 size = (static_cast(this->data[2].Get()) << SizeLow::Count) | this->data[0].Get();
                        return size;
                    }
                    constexpr ALWAYS_INLINE Attribute GetAttribute() const {
                        return this->data[2].Get();
                    }
                    constexpr ALWAYS_INLINE const util::BitPack32 *GetData() const {
                        return this->data;
                    }
                    static constexpr ALWAYS_INLINE size_t GetDataSize() {
                        return sizeof(data);
                    }
            };
            class PointerDescriptor {
                private:
                    /* Define fields for the packed descriptor word. */
                    using Index       = util::BitPack32::Field<0,                  4, s32>;
                    using Reserved0   = util::BitPack32::Field;
                    using AddressHigh = util::BitPack32::Field;
                    using Reserved1   = util::BitPack32::Field;
                    using AddressMid  = util::BitPack32::Field;
                    using Size        = util::BitPack32::Field;
                    /* Define fields for the second word. */
                    using AddressLow  = util::BitPack32::Field<0, BITSIZEOF(u32), u32>;
                    constexpr ALWAYS_INLINE u32 GetAddressMid(u64 address) {
                        return static_cast(address >> AddressLow::Count) & ((1u << AddressMid::Count) - 1);
                    }
                    constexpr ALWAYS_INLINE u32 GetAddressHigh(u64 address) {
                        return static_cast(address >> (AddressLow::Count + AddressMid::Count));
                    }
                private:
                    util::BitPack32 data[2];
                public:
                    constexpr ALWAYS_INLINE PointerDescriptor() : data{util::BitPack32{0}, util::BitPack32{0}} { /* ... */ }
                    ALWAYS_INLINE PointerDescriptor(const void *buffer, size_t size, s32 index) : data{util::BitPack32{0}, util::BitPack32{0}} {
                        const u64 address = reinterpret_cast(buffer);
                        this->data[0].Set(index);
                        this->data[0].Set(GetAddressHigh(address));
                        this->data[0].Set(GetAddressMid(address));
                        this->data[0].Set(size);
                        this->data[1] = { static_cast(address) };
                    }
                    ALWAYS_INLINE PointerDescriptor(const MessageBuffer &buf, s32 index) : data{util::BitPack32{0}, util::BitPack32{0}} {
                        buf.Get(index, this->data, util::size(this->data));
                    }
                    constexpr ALWAYS_INLINE s32 GetIndex() const {
                        return this->data[0].Get();
                    }
                    constexpr ALWAYS_INLINE uintptr_t GetAddress() const {
                        const u64 address = (static_cast((this->data[0].Get() << AddressMid::Count) | this->data[0].Get()) << AddressLow::Count) | this->data[1].Get();
                        return address;
                    }
                    constexpr ALWAYS_INLINE size_t GetSize() const {
                        return this->data[0].Get();
                    }
                    constexpr ALWAYS_INLINE const util::BitPack32 *GetData() const {
                        return this->data;
                    }
                    static constexpr ALWAYS_INLINE size_t GetDataSize() {
                        return sizeof(data);
                    }
            };
            class ReceiveListEntry {
                private:
                    /* Define fields for the first word. */
                    using AddressLow  = util::BitPack32::Field<0, BITSIZEOF(u32), u32>;
                    /* Define fields for the packed descriptor word. */
                    using AddressHigh = util::BitPack32::Field<0,                  7, u32>;
                    using Reserved    = util::BitPack32::Field;
                    using Size        = util::BitPack32::Field;
                    constexpr ALWAYS_INLINE u32 GetAddressHigh(u64 address) {
                        return static_cast(address >> (AddressLow::Count));
                    }
                private:
                    util::BitPack32 data[2];
                public:
                    constexpr ALWAYS_INLINE ReceiveListEntry() : data{util::BitPack32{0}, util::BitPack32{0}} { /* ... */ }
                    ALWAYS_INLINE ReceiveListEntry(const void *buffer, size_t size) : data{util::BitPack32{0}, util::BitPack32{0}} {
                        const u64 address = reinterpret_cast(buffer);
                        this->data[0] = { static_cast(address) };
                        this->data[1].Set(GetAddressHigh(address));
                        this->data[1].Set(size);
                    }
                    ALWAYS_INLINE ReceiveListEntry(u32 a, u32 b) : data{util::BitPack32{a}, util::BitPack32{b}} { /* ... */ }
                    constexpr ALWAYS_INLINE uintptr_t GetAddress() {
                        const u64 address = (static_cast(this->data[1].Get()) << AddressLow::Count) | this->data[0].Get();
                        return address;
                    }
                    constexpr ALWAYS_INLINE size_t GetSize() const {
                        return this->data[1].Get();
                    }
                    constexpr ALWAYS_INLINE const util::BitPack32 *GetData() const {
                        return this->data;
                    }
                    static constexpr ALWAYS_INLINE size_t GetDataSize() {
                        return sizeof(data);
                    }
            };
        private:
            u32 *buffer;
            size_t size;
        public:
            constexpr MessageBuffer(u32 *b, size_t sz) : buffer(b), size(sz) { /* ... */ }
            constexpr explicit MessageBuffer(u32 *b) : buffer(b), size(sizeof(::ams::svc::ThreadLocalRegion::message_buffer)) { /* ... */ }
            constexpr ALWAYS_INLINE size_t GetBufferSize() const {
                return this->size;
            }
            ALWAYS_INLINE void Get(s32 index, util::BitPack32 *dst, size_t count) const {
                /* Ensure that this doesn't get re-ordered. */
                __asm__ __volatile__("" ::: "memory");
                /* Get the words. */
                static_assert(sizeof(*dst) == sizeof(*this->buffer));
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wclass-memaccess"
                __builtin_memcpy(dst, this->buffer + index, count * sizeof(*dst));
#pragma GCC diagnostic pop
            }
            ALWAYS_INLINE s32 Set(s32 index, const util::BitPack32 *src, size_t count) const {
                /* Ensure that this doesn't get re-ordered. */
                __asm__ __volatile__("" ::: "memory");
                /* Set the words. */
                __builtin_memcpy(this->buffer + index, src, count * sizeof(*src));
                /* Ensure that this doesn't get re-ordered. */
                __asm__ __volatile__("" ::: "memory");
                return index + count;
            }
            template
            ALWAYS_INLINE const T &GetRaw(s32 index) const {
                return *reinterpret_cast(this->buffer + index);
            }
            template
            ALWAYS_INLINE s32 SetRaw(s32 index, const T &val) {
                *reinterpret_cast(this->buffer + index) = val;
                return index + (util::AlignUp(sizeof(val), sizeof(*this->buffer)) / sizeof(*this->buffer));
            }
            ALWAYS_INLINE void GetRawArray(s32 index, void *dst, size_t len) {
                __builtin_memcpy(dst, this->buffer + index, len);
            }
            ALWAYS_INLINE void SetRawArray(s32 index, const void *src, size_t len) {
                __builtin_memcpy(this->buffer + index, src, len);
            }
            ALWAYS_INLINE void SetNull() const {
                this->Set(MessageHeader());
            }
            ALWAYS_INLINE s32 Set(const MessageHeader &hdr) const {
                __builtin_memcpy(this->buffer, hdr.GetData(), hdr.GetDataSize());
                return hdr.GetDataSize() / sizeof(*this->buffer);
            }
            ALWAYS_INLINE s32 Set(const SpecialHeader &spc) const {
                const s32 index = MessageHeader::GetDataSize() / sizeof(*this->buffer);
                __builtin_memcpy(this->buffer + index, spc.GetHeader(), spc.GetHeaderSize());
                return index + (spc.GetHeaderSize() / sizeof(*this->buffer));
            }
            ALWAYS_INLINE s32 SetHandle(s32 index, const ::ams::svc::Handle &hnd) {
                static_assert(util::IsAligned(sizeof(hnd), sizeof(*this->buffer)));
                __builtin_memcpy(this->buffer + index, std::addressof(hnd), sizeof(hnd));
                return index + (sizeof(hnd) / sizeof(*this->buffer));
            }
            ALWAYS_INLINE s32 SetProcessId(s32 index, const u64 pid) {
                static_assert(util::IsAligned(sizeof(pid), sizeof(*this->buffer)));
                __builtin_memcpy(this->buffer + index, std::addressof(pid), sizeof(pid));
                return index + (sizeof(pid) / sizeof(*this->buffer));
            }
            ALWAYS_INLINE s32 Set(s32 index, const MapAliasDescriptor &desc) {
                __builtin_memcpy(this->buffer + index, desc.GetData(), desc.GetDataSize());
                return index + (desc.GetDataSize() / sizeof(*this->buffer));
            }
            ALWAYS_INLINE s32 Set(s32 index, const PointerDescriptor &desc) {
                __builtin_memcpy(this->buffer + index, desc.GetData(), desc.GetDataSize());
                return index + (desc.GetDataSize() / sizeof(*this->buffer));
            }
            ALWAYS_INLINE s32 Set(s32 index, const ReceiveListEntry &desc) {
                __builtin_memcpy(this->buffer + index, desc.GetData(), desc.GetDataSize());
                return index + (desc.GetDataSize() / sizeof(*this->buffer));
            }
            ALWAYS_INLINE s32 Set(s32 index, const u32 val) {
                static_assert(util::IsAligned(sizeof(val), sizeof(*this->buffer)));
                __builtin_memcpy(this->buffer + index, std::addressof(val), sizeof(val));
                return index + (sizeof(val) / sizeof(*this->buffer));
            }
            ALWAYS_INLINE Result GetAsyncResult() const {
                MessageHeader hdr(this->buffer);
                MessageHeader null{};
                R_SUCCEED_IF(AMS_UNLIKELY((__builtin_memcmp(hdr.GetData(), null.GetData(), MessageHeader::GetDataSize()) != 0)));
                return this->buffer[MessageHeader::GetDataSize() / sizeof(*this->buffer)];
            }
            ALWAYS_INLINE void SetAsyncResult(Result res) const {
                const s32 index = this->Set(MessageHeader());
                const auto value = res.GetValue();
                static_assert(util::IsAligned(sizeof(value), sizeof(*this->buffer)));
                __builtin_memcpy(this->buffer + index, std::addressof(value), sizeof(value));
            }
            ALWAYS_INLINE u64 GetProcessId(s32 index) const {
                u64 pid;
                __builtin_memcpy(std::addressof(pid), this->buffer + index, sizeof(pid));
                return pid;
            }
            ALWAYS_INLINE ams::svc::Handle GetHandle(s32 index) const {
                static_assert(sizeof(ams::svc::Handle) == sizeof(*this->buffer));
                return ::ams::svc::Handle(this->buffer[index]);
            }
            static constexpr ALWAYS_INLINE s32 GetSpecialDataIndex(const MessageHeader &hdr, const SpecialHeader &spc) {
                return (MessageHeader::GetDataSize() / sizeof(util::BitPack32)) + (spc.GetHeaderSize() / sizeof(util::BitPack32));
            }
            static constexpr ALWAYS_INLINE s32 GetPointerDescriptorIndex(const MessageHeader &hdr, const SpecialHeader &spc) {
                return GetSpecialDataIndex(hdr, spc) + (spc.GetDataSize() / sizeof(util::BitPack32));
            }
            static constexpr ALWAYS_INLINE s32 GetMapAliasDescriptorIndex(const MessageHeader &hdr, const SpecialHeader &spc) {
                return GetPointerDescriptorIndex(hdr, spc) + (hdr.GetPointerCount() * PointerDescriptor::GetDataSize() / sizeof(util::BitPack32));
            }
            static constexpr ALWAYS_INLINE s32 GetRawDataIndex(const MessageHeader &hdr, const SpecialHeader &spc) {
                return GetMapAliasDescriptorIndex(hdr, spc) + (hdr.GetMapAliasCount() * MapAliasDescriptor::GetDataSize() / sizeof(util::BitPack32));
            }
            static constexpr ALWAYS_INLINE s32 GetReceiveListIndex(const MessageHeader &hdr, const SpecialHeader &spc) {
                if (const s32 recv_list_index = hdr.GetReceiveListOffset()) {
                    return recv_list_index;
                } else {
                    return GetRawDataIndex(hdr, spc) + hdr.GetRawCount();
                }
            }
            static constexpr ALWAYS_INLINE s32 GetMessageBufferSize(const MessageHeader &hdr, const SpecialHeader &spc) {
                /* Get the size of the plain message. */
                size_t msg_size = GetReceiveListIndex(hdr, spc) * sizeof(util::BitPack32);
                /* Add the size of the receive list. */
                const auto count = hdr.GetReceiveListCount();
                switch (count) {
                    case MessageHeader::ReceiveListCountType_None:
                        break;
                    case MessageHeader::ReceiveListCountType_ToMessageBuffer:
                        break;
                    case MessageHeader::ReceiveListCountType_ToSingleBuffer:
                        msg_size += ReceiveListEntry::GetDataSize();
                        break;
                    default:
                        msg_size += (count - MessageHeader::ReceiveListCountType_CountOffset) * ReceiveListEntry::GetDataSize();
                        break;
                }
                return msg_size;
            }
    };
}