#include "xbox_portable.h"
#ifndef XBOX_PORTABLE_NO_DISC_WRAPPER
#include "disc.h"
#endif
#include "xbox_region.h"

#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#define XBOX_SECTOR_SIZE 2048U
#define XBOX_XDFS_MAGIC "MICROSOFT*XBOX*MEDIA"
#define XBOX_XDFS_MAGIC_SIZE 20U
#define XBOX_XDFS_STANDARD_VOLUME_LBA 32U
#define XBOX_XDFS_LEGACY_VOLUME_LBA 306112U
#define XBOX_MAX_ROOT_DIRECTORY_SECTORS 4096U
#define XBOX_XBE_CERTIFICATE_MIN_SIZE 0x0B0U

static uint16_t xbox_le16(const unsigned char *p)
{
    return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
}

static uint32_t xbox_le32(const unsigned char *p)
{
    return (uint32_t)p[0] |
           ((uint32_t)p[1] << 8) |
           ((uint32_t)p[2] << 16) |
           ((uint32_t)p[3] << 24);
}

static int xbox_ascii_case_equal(const unsigned char *value,
                                 size_t value_length,
                                 const char *expected)
{
    size_t index;
    size_t expected_length;

    if (!value || !expected)
        return 0;

    expected_length = strlen(expected);
    if (value_length != expected_length)
        return 0;

    for (index = 0; index < value_length; ++index) {
        if (tolower((unsigned char)value[index]) !=
            tolower((unsigned char)expected[index]))
            return 0;
    }

    return 1;
}

static size_t xbox_utf8_append(char *output,
                               size_t output_size,
                               size_t position,
                               uint32_t codepoint)
{
    if (!output || output_size == 0 || position >= output_size)
        return position;

    if (codepoint <= 0x7FU) {
        if (position + 1 >= output_size)
            return position;
        output[position++] = (char)codepoint;
    } else if (codepoint <= 0x7FFU) {
        if (position + 2 >= output_size)
            return position;
        output[position++] = (char)(0xC0U | (codepoint >> 6));
        output[position++] = (char)(0x80U | (codepoint & 0x3FU));
    } else {
        if (position + 3 >= output_size)
            return position;
        output[position++] = (char)(0xE0U | (codepoint >> 12));
        output[position++] = (char)(0x80U | ((codepoint >> 6) & 0x3FU));
        output[position++] = (char)(0x80U | (codepoint & 0x3FU));
    }

    output[position] = '\0';
    return position;
}

static void xbox_decode_title(const unsigned char *certificate,
                              size_t certificate_size,
                              char *output,
                              size_t output_size)
{
    size_t index;
    size_t position;

    if (!output || output_size == 0)
        return;

    output[0] = '\0';
    if (!certificate || certificate_size < 0x00CU + 80U)
        return;

    position = 0;
    for (index = 0; index < 40U; ++index) {
        uint16_t codepoint = xbox_le16(certificate + 0x00CU + (index * 2U));
        if (codepoint == 0)
            break;
        if (codepoint >= 0xD800U && codepoint <= 0xDFFFU)
            codepoint = (uint16_t)'?';
        position = xbox_utf8_append(output,
                                    output_size,
                                    position,
                                    codepoint);
    }
}

static int xbox_parse_media_id(const unsigned char *dmi,
                               size_t dmi_size,
                               char *output,
                               size_t output_size)
{
    size_t index;
    size_t position;

    if (!output || output_size == 0)
        return 0;

    output[0] = '\0';
    if (!dmi || dmi_size <= 8U)
        return 0;

    position = 0;
    for (index = 8U; index < dmi_size && index < 24U; ++index) {
        unsigned char c = dmi[index];
        if (isalnum(c)) {
            if (position + 1U >= output_size)
                break;
            output[position++] = (char)c;
        }
    }
    output[position] = '\0';

    return position >= 4U;
}

static int xbox_find_default_xbe(xbox_portable_read10_func read10,
                                 void *context,
                                 uint32_t root_lba,
                                 uint32_t root_size,
                                 uint32_t *xbe_lba)
{
    unsigned char sector[XBOX_SECTOR_SIZE];
    uint32_t sectors;
    uint32_t sector_index;

    if (!read10 || !xbe_lba || root_size == 0)
        return 0;

    sectors = (root_size + XBOX_SECTOR_SIZE - 1U) / XBOX_SECTOR_SIZE;
    if (sectors > XBOX_MAX_ROOT_DIRECTORY_SECTORS)
        sectors = XBOX_MAX_ROOT_DIRECTORY_SECTORS;

    for (sector_index = 0; sector_index < sectors; ++sector_index) {
        size_t offset = 0;
        if (read10(context,
                   root_lba + sector_index,
                   1,
                   sector,
                   sizeof(sector)) < 0)
            return 0;

        while (offset + 14U <= sizeof(sector)) {
            uint16_t left_entry;
            uint8_t name_length;
            uint32_t start_lba;
            size_t next_offset;

            left_entry = xbox_le16(sector + offset);
            name_length = sector[offset + 13U];
            if (left_entry == 0xFFFFU || name_length == 0U ||
                name_length == 0xFFU)
                break;
            if (offset + 14U + name_length > sizeof(sector))
                break;

            start_lba = xbox_le32(sector + offset + 4U);
            if (xbox_ascii_case_equal(sector + offset + 14U,
                                      name_length,
                                      "default.xbe")) {
                *xbe_lba = start_lba;
                return 1;
            }

            next_offset = (14U + (size_t)name_length + 3U) & ~((size_t)3U);
            if (next_offset == 0)
                break;
            offset += next_offset;
        }
    }

    return 0;
}

static int xbox_parse_xbe_certificate(xbox_portable_read10_func read10,
                                      void *context,
                                      uint32_t xbe_lba,
                                      xbox_portable_metadata *metadata)
{
    unsigned char header[XBOX_SECTOR_SIZE];
    unsigned char certificate[XBOX_SECTOR_SIZE * 2U];
    uint32_t base_va;
    uint32_t certificate_va;
    uint32_t file_offset;
    uint32_t certificate_lba;
    size_t inner_offset;
    uint32_t sectors_to_read;
    const unsigned char *cert;

    if (read10(context, xbe_lba, 1, header, sizeof(header)) < 0)
        return 0;
    if (xbox_le32(header) != 0x48454258U)
        return 0;

    base_va = xbox_le32(header + 0x104U);
    certificate_va = xbox_le32(header + 0x118U);
    if (certificate_va < base_va)
        return 0;

    file_offset = certificate_va - base_va;
    certificate_lba = xbe_lba + (file_offset / XBOX_SECTOR_SIZE);
    inner_offset = file_offset % XBOX_SECTOR_SIZE;
    sectors_to_read =
        (inner_offset + XBOX_XBE_CERTIFICATE_MIN_SIZE > XBOX_SECTOR_SIZE) ? 2U : 1U;

    memset(certificate, 0, sizeof(certificate));
    if (read10(context,
               certificate_lba,
               sectors_to_read,
               certificate,
               sizeof(certificate)) < 0)
        return 0;

    cert = certificate + inner_offset;
    metadata->title_id = xbox_le32(cert + 0x008U);
    metadata->allowed_media = xbox_le32(cert + 0x09CU);
    metadata->game_region = xbox_le32(cert + 0x0A0U);
    metadata->game_ratings = xbox_le32(cert + 0x0A4U);
    metadata->disc_number = xbox_le32(cert + 0x0A8U);
    metadata->version = xbox_le32(cert + 0x0ACU);
    metadata->have_game_region = 1;
    xbox_decode_title(cert,
                      XBOX_XBE_CERTIFICATE_MIN_SIZE,
                      metadata->title,
                      sizeof(metadata->title));
    xbox_region_format(metadata->game_region,
                       metadata->region,
                       sizeof(metadata->region));

    return 1;
}

void xbox_portable_metadata_init(xbox_portable_metadata *metadata)
{
    if (metadata)
        memset(metadata, 0, sizeof(*metadata));
}

int xbox_portable_read_metadata(xbox_portable_read10_func read10,
                                xbox_portable_read_structure_func read_structure,
                                void *context,
                                xbox_portable_metadata *metadata)
{
    static const uint32_t volume_candidates[] = {
        XBOX_XDFS_STANDARD_VOLUME_LBA,
        XBOX_XDFS_LEGACY_VOLUME_LBA
    };
    unsigned char volume[XBOX_SECTOR_SIZE];
    unsigned char dmi[XBOX_SECTOR_SIZE];
    uint32_t root_lba = 0;
    uint32_t root_size = 0;
    uint32_t xbe_lba = 0;
    size_t candidate_index;

    if (!read10 || !metadata)
        return 0;

    xbox_portable_metadata_init(metadata);

    for (candidate_index = 0;
         candidate_index < sizeof(volume_candidates) / sizeof(volume_candidates[0]);
         ++candidate_index) {
        memset(volume, 0, sizeof(volume));
        if (read10(context,
                   volume_candidates[candidate_index],
                   1,
                   volume,
                   sizeof(volume)) < 0)
            continue;
        if (memcmp(volume, XBOX_XDFS_MAGIC, XBOX_XDFS_MAGIC_SIZE) != 0)
            continue;

        metadata->xdfs_volume_lba = volume_candidates[candidate_index];
        root_lba = xbox_le32(volume + 0x14U);
        root_size = xbox_le32(volume + 0x18U);
        break;
    }

    if (root_lba == 0 || root_size == 0)
        return 0;
    if (!xbox_find_default_xbe(read10,
                               context,
                               root_lba,
                               root_size,
                               &xbe_lba))
        return 0;
    if (!xbox_parse_xbe_certificate(read10,
                                    context,
                                    xbe_lba,
                                    metadata))
        return 0;

    if (read_structure) {
        memset(dmi, 0, sizeof(dmi));
        if (read_structure(context,
                           0x04U,
                           0x00U,
                           dmi,
                           sizeof(dmi)) >= 0)
            xbox_parse_media_id(dmi,
                                sizeof(dmi),
                                metadata->media_id,
                                sizeof(metadata->media_id));
    }

    metadata->valid = 1;
    return 1;
}

#ifndef XBOX_PORTABLE_NO_DISC_WRAPPER
static int xbox_disc_read10(void *context,
                            uint32_t lba,
                            uint32_t sectors,
                            unsigned char *buffer,
                            size_t buffer_size)
{
    return disc_xbox_read_10((disc *)context,
                             lba,
                             sectors,
                             buffer,
                             buffer_size);
}

static int xbox_disc_read_structure(void *context,
                                    uint8_t format,
                                    uint8_t layer,
                                    unsigned char *buffer,
                                    size_t buffer_size)
{
    return disc_xbox_read_dvd_structure((disc *)context,
                                        format,
                                        layer,
                                        buffer,
                                        buffer_size);
}

int xbox_portable_read_metadata_from_disc(struct disc_s *disc_value,
                                          xbox_portable_metadata *metadata)
{
    if (!disc_value)
        return 0;

    return xbox_portable_read_metadata(xbox_disc_read10,
                                       xbox_disc_read_structure,
                                       disc_value,
                                       metadata);
}
#endif
