#include "Library/Yaml/ByamlHeader.h"

#include <byteswap.h>
#include <prim/seadEndian.h>
#include <stream/seadStream.h>

#include "Library/Yaml/ByamlData.h"

#define BYAML_LE_TAG 0x5942  // 'YB'
#define BYAML_BE_TAG 0x4259  // 'BY'

namespace al {
u16 ByamlHeader::getTag() const {
    return isInvertOrder() ? bswap_16(mTag) : mTag;
}

bool ByamlHeader::isInvertOrder() const {
    return mTag == BYAML_LE_TAG;
}

u16 ByamlHeader::getVersion() const {
    if ((_0 & 0xFFFF) == BYAML_LE_TAG)  // isInvertOrder()
        return bswap_16(_0 >> 16);
    return _0 >> 16;
}

u32 ByamlHeader::getHashKeyTableOffset() const {
    return isInvertOrder() ? __bswap_32(mHashKeyOffset) : mHashKeyOffset;
}

u32 ByamlHeader::getStringTableOffset() const {
    return isInvertOrder() ? __bswap_32(mStringTableOffset) : mStringTableOffset;
}

u32 ByamlHeader::getDataOffset() const {
    return isInvertOrder() ? __bswap_32(mDataOffset) : mDataOffset;
}

ByamlStringTableIter::ByamlStringTableIter() = default;

ByamlStringTableIter::ByamlStringTableIter(const u8* data, bool isRev)
    : mData(data), mIsRev(isRev) {}

s32 ByamlStringTableIter::getSize() const {
    u32 type_and_size = *reinterpret_cast<const u32*>(mData);
    return mIsRev ? bswap_24(type_and_size >> 8) : type_and_size >> 8;
}

const u32* ByamlStringTableIter::getAddressTable() const {
    // mData is an integer pointer, so getting to the table is just increasing the pointer by 1
    // (which is + 4)
    return reinterpret_cast<const u32*>(mData + 4);
}

u32 ByamlStringTableIter::getStringAddress(s32 index) const {
    if (mIsRev)
        return bswap_32(getAddressTable()[index]);

    return getAddressTable()[index];
}

// NON_MATCHING: regalloc (https://decomp.me/scratch/AdRVH)
u32 ByamlStringTableIter::getEndAddress() const {
    u32 val = getAddressTable()[getSize()];
    return mIsRev ? bswap_32(val) : val;
}

const char* ByamlStringTableIter::getString(s32 index) const {
    return reinterpret_cast<const char*>(&mData[getStringAddress(index)]);
}

s32 ByamlStringTableIter::getStringSize(s32 index) const {
    return getStringAddress(index + 1) - getStringAddress(index) - 1;
}

s32 ByamlStringTableIter::findStringIndex(const char* str) const {
    s32 lowerBound = 0;
    s32 upperBound = getSize();
    while (lowerBound < upperBound) {
        s32 avg = (lowerBound + upperBound) / 2;
        s32 result = strcmp(str, getString(avg));
        if (result == 0)
            return avg;

        if (result > 0)
            lowerBound = avg + 1;
        else
            upperBound = avg;
    }
    return -1;
}

bool ByamlStringTableIter::isValidate() const {
    return mData != nullptr;
}

}  // namespace al

namespace alByamlLocalUtil {

const char* getDataTypeString(s32 type) {
    switch (type) {
    case (u8)al::ByamlDataType::None:
        return "None";
    case (u8)al::ByamlDataType::String:
        return "String";
    case (u8)al::ByamlDataType::Array:
        return "Array";
    case (u8)al::ByamlDataType::Hash:
        return "Hash";
    case (u8)al::ByamlDataType::StringTable:
        return "StringTable";
    case (u8)al::ByamlDataType::Bool:
        return "Bool";
    case (u8)al::ByamlDataType::Int:
        return "Int";
    case (u8)al::ByamlDataType::Float:
        return "Float";
    case (u8)al::ByamlDataType::UInt:
        return "UInt";
    case (u8)al::ByamlDataType::Int64:
        return "Int64";
    case (u8)al::ByamlDataType::UInt64:
        return "UInt64";
    case (u8)al::ByamlDataType::Double:
        return "Double";
    case (u8)al::ByamlDataType::Null:
        return "NULL";
    case (u8)al::ByamlDataType::Binary:
    default:
        return "Unknown";
    };
}

al::ByamlStringTableIter getHashKeyTable(const u8* data) {
    const al::ByamlHeader* header = reinterpret_cast<const al::ByamlHeader*>(data);
    s32 off = header->getHashKeyTableOffset();
    if (off == 0)
        return {};
    return {&data[off], header->isInvertOrder()};
}

al::ByamlStringTableIter getStringTable(const u8* data) {
    const al::ByamlHeader* header = reinterpret_cast<const al::ByamlHeader*>(data);
    s32 off = header->getStringTableOffset();
    if (off == 0)
        return {};
    return {&data[off], header->isInvertOrder()};
}

u64 getData64Bit(const u8* data, u32 off, bool isRev) {
    u64 val = *reinterpret_cast<const u64*>(&data[off]);
    return isRev ? bswap_32_64(val) : val;
}

void writeU24(sead::WriteStream* stream, s32 val) {
    if (sead::Endian::getHostEndian() == sead::Endian::cBig) {
        stream->writeU8(val >> 16);
        stream->writeU8(val >> 8);
        stream->writeU8(val);
    } else {
        stream->writeU8(val);
        stream->writeU8(val >> 8);
        stream->writeU8(val >> 16);
    }
}

// NON_MATCHING: inlined verifiHeader, splitted loads for unswappedAfterOffset and diff in final
// logic (https://decomp.me/scratch/5xOvy)
bool verifiByaml(const u8* data) {
    if (!verifiByamlHeader(data))
        return false;

    bool isRev = *((const u16*)data) == BYAML_LE_TAG;
    const u32* biggerData = (const u32*)data;

    u32 afterHashOffset = 0;
    u32 hashOffset = isRev ? bswap_32(biggerData[1]) : biggerData[1];
    if (hashOffset) {
        const u8* hashData = &data[hashOffset];
        if (!verifiByamlStringTable(hashData, isRev))
            return false;
        u32 type_and_size = *reinterpret_cast<const u32*>(hashData);
        s32 hash_size = isRev ? bswap_24(type_and_size >> 8) : type_and_size >> 8;
        s32 unswappedAfterOffset = *(s32*)&hashData[(hash_size * 4) + 4];
        u32 swappedAfterOffset = bswap_32(unswappedAfterOffset);
        afterHashOffset = isRev ? swappedAfterOffset : unswappedAfterOffset;
    }

    u32 afterStringOffset = 0;
    u32 stringOffset = isRev ? bswap_32(biggerData[2]) : biggerData[2];
    if (stringOffset) {
        const u8* stringData = &data[stringOffset];
        if (!verifiByamlStringTable(stringData, isRev))
            return false;
        u32 type_and_size = *reinterpret_cast<const u32*>(stringData);
        s32 string_size = isRev ? bswap_24(type_and_size >> 8) : type_and_size >> 8;
        s32 unswappedAfterOffset = *(s32*)&stringData[4 * string_size + 4];
        u32 swappedAfterOffset = bswap_32(unswappedAfterOffset);
        afterStringOffset = isRev ? swappedAfterOffset : unswappedAfterOffset;
    }

    u32 rootOffset = isRev ? bswap_32(biggerData[3]) : biggerData[3];

    return (((!hashOffset && !stringOffset) || rootOffset) &&
            (!hashOffset || ((!stringOffset || afterHashOffset <= stringOffset) &&
                             (!rootOffset || afterHashOffset <= rootOffset)))) &&
           (afterStringOffset <= rootOffset || !stringOffset || !rootOffset);
}

// NON_MATCHING: missing & 0xFFFF (https://decomp.me/scratch/dRP3R)
bool verifiByamlHeader(const u8* data) {
    const al::ByamlHeader* header = reinterpret_cast<const al::ByamlHeader*>(data);
    return header->getTag() == BYAML_BE_TAG && (u32)(header->getVersion() - 1) < 3;
}

bool verifiByamlStringTable(const u8* data, bool isRev) {
    const s32* address_table = reinterpret_cast<const s32*>(data + 4);

    u32 type_and_size = *reinterpret_cast<const u32*>(data);
    if ((al::ByamlDataType)type_and_size != al::ByamlDataType::StringTable)
        return false;
    s32 size = isRev ? bswap_24(type_and_size >> 8) : type_and_size >> 8;
    if (size < 1)
        return false;

    // check that strings are null-terminated
    for (s32 i = 1; i <= size; i++) {
        s32 off = isRev ? bswap_32(address_table[i]) : address_table[i];
        if (data[off - 1])
            return false;
    }

    for (s32 i = 0; i < size; i++) {
        s32 val1 = isRev ? bswap_32(address_table[i]) : address_table[i];
        s32 val2 = isRev ? bswap_32(address_table[i + 1]) : address_table[i + 1];
        if (val1 >= val2)
            return false;
    }

    // check for correct length of address table
    s32 calcFirstStringPos = 4 * size + 8;
    s32 dataFirstStringPos = isRev ? bswap_32(address_table[0]) : address_table[0];
    if (dataFirstStringPos != calcFirstStringPos)
        return false;

    // TODO: improve this. Matching, but ugly
    if (isRev) {
        for (s32 i = 0; i < size - 1; i++) {
            const char* c1 =
                (const char*)&data[isRev ? bswap_32(address_table[i]) : address_table[i]];
            const char* c2 =
                (const char*)&data[isRev ? bswap_32(address_table[i + 1]) : address_table[i + 1]];
            if (strcmp(c1, c2) > 0)
                return false;
        }
    } else {
        for (s32 i = 0; i < size - 1; i++) {
            const char* c1 =
                (const char*)&data[isRev ? bswap_32(address_table[i]) : address_table[i]];
            const char* c2 =
                (const char*)&data[false ? bswap_32(address_table[i + 1]) : address_table[i + 1]];
            if (strcmp(c1, c2) > 0)
                return false;
        }
    }
    return true;
}

}  // namespace alByamlLocalUtil
