/*
 hap.c
 
 Copyright (c) 2011-2013, Tom Butterworth and Vidvox LLC. All rights reserved.
 
 Redistribution and use in source and binary forms, with or without
 modification, are permitted provided that the following conditions are met:
 
 * Redistributions of source code must retain the above copyright
 notice, this list of conditions and the following disclaimer.
 
 * Redistributions in binary form must reproduce the above copyright
 notice, this list of conditions and the following disclaimer in the
 documentation and/or other materials provided with the distribution.
 
 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "hap.h"
#include <stdlib.h>
#include <stdint.h>
#include <string.h> // For memcpy for uncompressed frames
#include "snappy-c.h"

#define kHapUInt24Max 0x00FFFFFF

/*
 Hap Constants
 First four bits represent the compressor
 Second four bits represent the format
 */
#define kHapCompressorNone 0xA
#define kHapCompressorSnappy 0xB
#define kHapCompressorComplex 0xC

#define kHapFormatRGBDXT1 0xB
#define kHapFormatRGBADXT5 0xE
#define kHapFormatYCoCgDXT5 0xF
#define kHapFormatARGTC1 0x1
#define kHapFormatRGBABPTC 0xC
#define kHapFormatRGBBPTCUF 0x2
#define kHapFormatRGBBPTCSF 0x3

/*
 Packed byte values for Hap
 
 Format                     Compressor      Byte Code
 ----------------------------------------------------
 RGB_DXT1                   None            0xAB
 RGB_DXT1                   Snappy          0xBB
 RGB_DXT1                   Complex         0xCB
 RGBA_DXT5                  None            0xAE
 RGBA_DXT5                  Snappy          0xBE
 RGBA_DXT5                  Complex         0xCE
 YCoCg_DXT5                 None            0xAF
 YCoCg_DXT5                 Snappy          0xBF
 YCoCg_DXT5                 Complex         0xCF
 A_RGTC1                    None            0xA1
 A_RGTC1                    Snappy          0xB1
 A_RGTC1                    Complex         0xC1
 RGBA_BPTC_UNORM            None            0xAC
 RGBA_BPTC_UNORM            Snappy          0xBC
 RGBA_BPTC_UNORM            Complex         0xCC
 RGB_BPTC_UNSIGNED_FLOAT    None            0xA2
 RGB_BPTC_UNSIGNED_FLOAT    Snappy          0xB2
 RGB_BPTC_UNSIGNED_FLOAT    Complex         0xC2
 RGB_BPTC_SIGNED_FLOAT      None            0xA3
 RGB_BPTC_SIGNED_FLOAT      Snappy          0xB3
 RGB_BPTC_SIGNED_FLOAT      Complex         0xC3
 */

/*
 Hap Frame Section Types
 */
#define kHapSectionMultipleImages 0x0D
#define kHapSectionDecodeInstructionsContainer 0x01
#define kHapSectionChunkSecondStageCompressorTable 0x02
#define kHapSectionChunkSizeTable 0x03
#define kHapSectionChunkOffsetTable 0x04

/*
 To decode we use a struct to store details of each chunk
 */
typedef struct HapChunkDecodeInfo {
    unsigned int result;
    unsigned int compressor;
    const char *compressed_chunk_data;
    size_t compressed_chunk_size;
    char *uncompressed_chunk_data;
    size_t uncompressed_chunk_size;
} HapChunkDecodeInfo;

// TODO: rename the defines we use for codes used in stored frames
// to better differentiate them from the enums used for the API

// These read and write little-endian values on big or little-endian architectures
static unsigned int hap_read_3_byte_uint(const void *buffer)
{
    return (*(uint8_t *)buffer) + ((*(((uint8_t *)buffer) + 1)) << 8) + ((*(((uint8_t *)buffer) + 2)) << 16);
}

static void hap_write_3_byte_uint(void *buffer, unsigned int value)
{
    *(uint8_t *)buffer = value & 0xFF;
    *(((uint8_t *)buffer) + 1) = (value >> 8) & 0xFF;
    *(((uint8_t *)buffer) + 2) = (value >> 16) & 0xFF;
}

static unsigned int hap_read_4_byte_uint(const void *buffer)
{
    return (*(uint8_t *)buffer) + ((*(((uint8_t *)buffer) + 1)) << 8) + ((*(((uint8_t *)buffer) + 2)) << 16) + ((*(((uint8_t *)buffer) + 3)) << 24);
}

static void hap_write_4_byte_uint(const void *buffer, unsigned int value)
{
    *(uint8_t *)buffer = value & 0xFF;
    *(((uint8_t *)buffer) + 1) = (value >> 8) & 0xFF;
    *(((uint8_t *)buffer) + 2) = (value >> 16) & 0xFF;
    *(((uint8_t *)buffer) + 3) = (value >> 24) & 0xFF;
}

#define hap_top_4_bits(x) (((x) & 0xF0) >> 4)

#define hap_bottom_4_bits(x) ((x) & 0x0F)

#define hap_4_bit_packed_byte(top_bits, bottom_bits) (((top_bits) << 4) | ((bottom_bits) & 0x0F))

static int hap_read_section_header(const void *buffer, uint32_t buffer_length, uint32_t *out_header_length, uint32_t *out_section_length, unsigned int *out_section_type)
{
    /*
     Verify buffer is big enough to contain a four-byte header
     */
    if (buffer_length < 4U)
    {
        return HapResult_Bad_Frame;
    }

    /*
     The first three bytes are the length of the section (not including the header) or zero
     if the length is stored in the last four bytes of an eight-byte header
     */
    *out_section_length = hap_read_3_byte_uint(buffer);

    /*
     If the first three bytes are zero, the size is in the following four bytes
     */
    if (*out_section_length == 0U)
    {
        /*
         Verify buffer is big enough to contain an eight-byte header
         */
        if (buffer_length < 8U)
        {
            return HapResult_Bad_Frame;
        }
        *out_section_length = hap_read_4_byte_uint(((uint8_t *)buffer) + 4U);
        *out_header_length = 8U;
    }
    else
    {
        *out_header_length = 4U;
    }

    /*
     The fourth byte stores the section type
     */
    *out_section_type = *(((uint8_t *)buffer) + 3U);
    
    /*
     Verify the section does not extend beyond the buffer
     */
    if (*out_header_length + *out_section_length > buffer_length)
    {
        return HapResult_Bad_Frame;
    }

    return HapResult_No_Error;
}

static void hap_write_section_header(void *buffer, size_t header_length, uint32_t section_length, unsigned int section_type)
{
    /*
     The first three bytes are the length of the section (not including the header) or zero
     if using an eight-byte header
     */
    if (header_length == 4U)
    {
        hap_write_3_byte_uint(buffer, (unsigned int)section_length);
    }
    else
    {
        /*
         For an eight-byte header, the length is in the last four bytes
         */
        hap_write_3_byte_uint(buffer, 0U);
        hap_write_4_byte_uint(((uint8_t *)buffer) + 4U, section_length);
    }
    
    /*
     The fourth byte stores the section type
     */
    *(((uint8_t *)buffer) + 3) = section_type;
}

// Returns an API texture format constant or 0 if not recognised
static unsigned int hap_texture_format_constant_for_format_identifier(unsigned int identifier)
{
    switch (identifier)
    {
        case kHapFormatRGBDXT1:
            return HapTextureFormat_RGB_DXT1;
        case kHapFormatRGBADXT5:
            return HapTextureFormat_RGBA_DXT5;
        case kHapFormatYCoCgDXT5:
            return HapTextureFormat_YCoCg_DXT5;
        case kHapFormatARGTC1:
            return HapTextureFormat_A_RGTC1;
        case kHapFormatRGBABPTC:
            return HapTextureFormat_RGBA_BPTC_UNORM;
        case kHapFormatRGBBPTCUF:
            return HapTextureFormat_RGB_BPTC_UNSIGNED_FLOAT;
        case kHapFormatRGBBPTCSF:
            return HapTextureFormat_RGB_BPTC_SIGNED_FLOAT;
        default:
            return 0;
            
    }
}

// Returns a frame identifier or 0 if not recognised
static unsigned int hap_texture_format_identifier_for_format_constant(unsigned int constant)
{
    switch (constant)
    {
        case HapTextureFormat_RGB_DXT1:
            return kHapFormatRGBDXT1;
        case HapTextureFormat_RGBA_DXT5:
            return kHapFormatRGBADXT5;
        case HapTextureFormat_YCoCg_DXT5:
            return kHapFormatYCoCgDXT5;
        case HapTextureFormat_A_RGTC1:
            return kHapFormatARGTC1;
        case HapTextureFormat_RGBA_BPTC_UNORM:
            return kHapFormatRGBABPTC;
        case HapTextureFormat_RGB_BPTC_UNSIGNED_FLOAT:
            return kHapFormatRGBBPTCUF;
        case HapTextureFormat_RGB_BPTC_SIGNED_FLOAT:
            return kHapFormatRGBBPTCSF;
        default:
            return 0;
    }
}

// Returns the length of a decode instructions container of chunk_count chunks
// not including the section header
static size_t hap_decode_instructions_length(unsigned int chunk_count)
{
    /*
     Calculate the size of our Decode Instructions Section
     = Second-Stage Compressor Table + Chunk Size Table + headers for both sections
     = chunk_count + (4 * chunk_count) + 4 + 4
     */
    size_t length = (5 * chunk_count) + 8;

    return length;
}

static unsigned int hap_limited_chunk_count_for_frame(size_t input_bytes, unsigned int texture_format, unsigned int chunk_count)
{
    // This is a hard limit due to the 4-byte headers we use for the decode instruction container
    // (0xFFFFFF == count + (4 x count) + 20)
    if (chunk_count > 3355431)
    {
        chunk_count = 3355431;
    }
    // Divide frame equally on DXT block boundries (8 or 16 bytes)
    unsigned long dxt_block_count;
    switch (texture_format) {
        case HapTextureFormat_RGB_DXT1:
        case HapTextureFormat_A_RGTC1:
            dxt_block_count = input_bytes / 8;
            break;
        default:
            dxt_block_count = input_bytes / 16;
    }
    while (dxt_block_count % chunk_count != 0) {
        chunk_count--;
    }

    return chunk_count;
}

static size_t hap_max_encoded_length(size_t input_bytes, unsigned int texture_format, unsigned int compressor, unsigned int chunk_count)
{
    size_t decode_instructions_length, max_compressed_length;

    chunk_count = hap_limited_chunk_count_for_frame(input_bytes, texture_format, chunk_count);

    decode_instructions_length = hap_decode_instructions_length(chunk_count);

    if (compressor == HapCompressorSnappy)
    {
        size_t chunk_size = input_bytes / chunk_count;
        max_compressed_length = snappy_max_compressed_length(chunk_size) * chunk_count;
    }
    else
    {
        max_compressed_length = input_bytes;
    }

    // top section header + decode instructions section header + decode instructions + compressed data
    return max_compressed_length + 8U + decode_instructions_length + 4U;
}

unsigned long HapMaxEncodedLength(unsigned int count,
                                  unsigned long *inputBytes,
                                  unsigned int *textureFormats,
                                  unsigned int *chunkCounts)
{
    // Start with the length of a multiple-image section header
    unsigned long total_length = 8;

    // Return 0 for bad arguments
    if (count == 0 || count > 2
        || inputBytes == NULL
        || textureFormats == NULL
        || chunkCounts == NULL)
    {
        return 0;
    }

    for (int i = 0; i < count; i++)
    {
        if (chunkCounts[i] == 0)
        {
            return 0;
        }

        // Assume snappy, the worst case
        total_length += hap_max_encoded_length(inputBytes[i], textureFormats[i], HapCompressorSnappy, chunkCounts[i]);
    }

    return total_length;
}

static unsigned int hap_encode_texture(const void *inputBuffer, unsigned long inputBufferBytes, unsigned int textureFormat,
                                       unsigned int compressor, unsigned int chunkCount, void *outputBuffer,
                                       unsigned long outputBufferBytes, unsigned long *outputBufferBytesUsed)
{
    size_t top_section_header_length;
    size_t top_section_length;
    unsigned int storedCompressor;
    unsigned int storedFormat;

    /*
     Check arguments
     */
    if (inputBuffer == NULL
        || inputBufferBytes == 0
        || (textureFormat != HapTextureFormat_RGB_DXT1
            && textureFormat != HapTextureFormat_RGBA_DXT5
            && textureFormat != HapTextureFormat_YCoCg_DXT5
            && textureFormat != HapTextureFormat_A_RGTC1
            && textureFormat != HapTextureFormat_RGBA_BPTC_UNORM
            && textureFormat != HapTextureFormat_RGB_BPTC_UNSIGNED_FLOAT
            && textureFormat != HapTextureFormat_RGB_BPTC_SIGNED_FLOAT
            )
        || (compressor != HapCompressorNone
            && compressor != HapCompressorSnappy
            )
        || outputBuffer == NULL
        || outputBufferBytesUsed == NULL
        )
    {
        return HapResult_Bad_Arguments;
    }
    else if (outputBufferBytes < hap_max_encoded_length(inputBufferBytes, textureFormat, compressor, chunkCount))
    {
        return HapResult_Buffer_Too_Small;
    }
    
    /*
     To store frames of length greater than can be expressed in three bytes, we use an eight byte header (the last four bytes are the
     frame size). We don't know the compressed size until we have performed compression, but we know the worst-case size
     (the uncompressed size), so choose header-length based on that.
     
     A simpler encoder could always use the eight-byte header variation.
     */
    if (inputBufferBytes > kHapUInt24Max)
    {
        top_section_header_length = 8U;
    }
    else
    {
        top_section_header_length = 4U;
    }

    if (compressor == HapCompressorSnappy)
    {
        /*
         We attempt to chunk as requested, and if resulting frame is larger than it is uncompressed then
         store frame uncompressed
         */

        size_t decode_instructions_length;
        size_t chunk_size, compress_buffer_remaining;
        uint8_t *second_stage_compressor_table;
        void *chunk_size_table;
        char *compressed_data;
        unsigned int i;

        chunkCount = hap_limited_chunk_count_for_frame(inputBufferBytes, textureFormat, chunkCount);
        decode_instructions_length = hap_decode_instructions_length(chunkCount);

        // Check we have space for the Decode Instructions Container
        if ((inputBufferBytes + decode_instructions_length + 4) > kHapUInt24Max)
        {
            top_section_header_length = 8U;
        }

        second_stage_compressor_table = ((uint8_t *)outputBuffer) + top_section_header_length + 4 + 4;
        chunk_size_table = ((uint8_t *)outputBuffer) + top_section_header_length + 4 + 4 + chunkCount + 4;

        chunk_size = inputBufferBytes / chunkCount;

        // write the Decode Instructions section header
        hap_write_section_header(((uint8_t *)outputBuffer) + top_section_header_length, 4U, decode_instructions_length, kHapSectionDecodeInstructionsContainer);
        // write the Second Stage Compressor Table section header
        hap_write_section_header(((uint8_t *)outputBuffer) + top_section_header_length + 4U, 4U, chunkCount, kHapSectionChunkSecondStageCompressorTable);
        // write the Chunk Size Table section header
        hap_write_section_header(((uint8_t *)outputBuffer) + top_section_header_length + 4U + 4U + chunkCount, 4U, chunkCount * 4U, kHapSectionChunkSizeTable);

        compressed_data = (char *)(((uint8_t *)outputBuffer) + top_section_header_length + 4 + decode_instructions_length);

        compress_buffer_remaining = outputBufferBytes - top_section_header_length - 4 - decode_instructions_length;

        top_section_length = 4 + decode_instructions_length;

        for (i = 0; i < chunkCount; i++) {
            size_t chunk_packed_length = compress_buffer_remaining;
            const char *chunk_input_start = (const char *)(((uint8_t *)inputBuffer) + (chunk_size * i));
            if (compressor == HapCompressorSnappy)
            {
                snappy_status result = snappy_compress(chunk_input_start, chunk_size, (char *)compressed_data, &chunk_packed_length);
                if (result != SNAPPY_OK)
                {
                    return HapResult_Internal_Error;
                }
            }

            if (compressor == HapCompressorNone || chunk_packed_length >= chunk_size)
            {
                // store the chunk uncompressed
                memcpy(compressed_data, chunk_input_start, chunk_size);
                chunk_packed_length = chunk_size;
                second_stage_compressor_table[i] = kHapCompressorNone;
            }
            else
            {
                // ie we used snappy and saved some space
                second_stage_compressor_table[i] = kHapCompressorSnappy;
            }
            hap_write_4_byte_uint(((uint8_t *)chunk_size_table) + (i * 4), chunk_packed_length);
            compressed_data += chunk_packed_length;
            top_section_length += chunk_packed_length;
            compress_buffer_remaining -= chunk_packed_length;
        }

        if (top_section_length < inputBufferBytes + top_section_header_length)
        {
            // use the complex storage because snappy compression saved space
            storedCompressor = kHapCompressorComplex;
        }
        else
        {
            // Signal to store the frame uncompressed
            compressor = HapCompressorNone;
        }
    }

    if (compressor == HapCompressorNone)
    {
        memcpy(((uint8_t *)outputBuffer) + top_section_header_length, inputBuffer, inputBufferBytes);
        top_section_length = inputBufferBytes;
        storedCompressor = kHapCompressorNone;
    }
    
    storedFormat = hap_texture_format_identifier_for_format_constant(textureFormat);
    
    hap_write_section_header(outputBuffer, top_section_header_length, top_section_length, hap_4_bit_packed_byte(storedCompressor, storedFormat));

    *outputBufferBytesUsed = top_section_length + top_section_header_length;

    return HapResult_No_Error;
}

unsigned int HapEncode(unsigned int count,
                       const void **inputBuffers, unsigned long *inputBuffersBytes,
                       unsigned int *textureFormats,
                       unsigned int *compressors,
                       unsigned int *chunkCounts,
                       void *outputBuffer, unsigned long outputBufferBytes,
                       unsigned long *outputBufferBytesUsed)
{
    size_t top_section_header_length;
    size_t top_section_length;
    unsigned long section_length;

    if (count == 0 || count > 2 // A frame must contain one or two textures
        || inputBuffers == NULL
        || inputBuffersBytes == NULL
        || textureFormats == NULL
        || compressors == NULL
        || chunkCounts == NULL
        || outputBuffer == NULL
        || outputBufferBytes == 0
        || outputBufferBytesUsed == NULL)
    {
        return HapResult_Bad_Arguments;
    }

    for (int i = 0; i < count; i++)
    {
        if (chunkCounts[i] == 0)
        {
            return HapResult_Bad_Arguments;
        }
    }

    if (count == 1)
    {
        // Encode without the multi-image layout
        return hap_encode_texture(inputBuffers[0],
                                  inputBuffersBytes[0],
                                  textureFormats[0],
                                  compressors[0],
                                  chunkCounts[0],
                                  outputBuffer,
                                  outputBufferBytes,
                                  outputBufferBytesUsed);
    }
    else if ((textureFormats[0] != HapTextureFormat_YCoCg_DXT5 && textureFormats[1] != HapTextureFormat_YCoCg_DXT5)
             && (textureFormats[0] != HapTextureFormat_A_RGTC1 && textureFormats[1] != HapTextureFormat_A_RGTC1))
    {
        /*
         Permitted combinations:
         HapTextureFormat_YCoCg_DXT5 + HapTextureFormat_A_RGTC1
         */
        return HapResult_Bad_Arguments;
    }
    else
    {
        // Calculate the worst-case size for the top section and choose a header-length based on that
        top_section_length = 0;
        for (int i = 0; i < count; i++)
        {
            top_section_length += inputBuffersBytes[i] + hap_decode_instructions_length(chunkCounts[i]) + 4;
        }

        if (top_section_length > kHapUInt24Max)
        {
            top_section_header_length = 8U;
        }
        else
        {
            top_section_header_length = 4U;
        }

        // Encode each texture
        top_section_length = 0;
        for (int i = 0; i < count; i++)
        {
            void *section = ((uint8_t *)outputBuffer) + top_section_header_length + top_section_length;
            unsigned int result = hap_encode_texture(inputBuffers[i],
                                                     inputBuffersBytes[i],
                                                     textureFormats[i],
                                                     compressors[i],
                                                     chunkCounts[i],
                                                     section,
                                                     outputBufferBytes - (top_section_header_length + top_section_length),
                                                     &section_length);
            if (result != HapResult_No_Error)
            {
                return result;
            }
            top_section_length += section_length;
        }

        hap_write_section_header(outputBuffer, top_section_header_length, top_section_length, kHapSectionMultipleImages);

        *outputBufferBytesUsed = top_section_length + top_section_header_length;

        return HapResult_No_Error;
    }
}

static void hap_decode_chunk(HapChunkDecodeInfo chunks[], unsigned int index)
{
    if (chunks)
    {
        if (chunks[index].compressor == kHapCompressorSnappy)
        {
            snappy_status snappy_result = snappy_uncompress(chunks[index].compressed_chunk_data,
                                                            chunks[index].compressed_chunk_size,
                                                            chunks[index].uncompressed_chunk_data,
                                                            &chunks[index].uncompressed_chunk_size);

            switch (snappy_result)
            {
                case SNAPPY_INVALID_INPUT:
                    chunks[index].result = HapResult_Bad_Frame;
                    break;
                case SNAPPY_OK:
                    chunks[index].result = HapResult_No_Error;
                    break;
                default:
                    chunks[index].result = HapResult_Internal_Error;
                    break;
            }
        }
        else if (chunks[index].compressor == kHapCompressorNone)
        {
            memcpy(chunks[index].uncompressed_chunk_data,
                   chunks[index].compressed_chunk_data,
                   chunks[index].compressed_chunk_size);
            chunks[index].result = HapResult_No_Error;
        }
        else
        {
            chunks[index].result = HapResult_Bad_Frame;
        }
    }
}

static unsigned int hap_decode_header_complex_instructions(const void *texture_section, uint32_t texture_section_length, int * chunk_count,
                                                   const void **compressors, const void **chunk_sizes, const void **chunk_offsets, const char **frame_data){
    int result = HapResult_No_Error;
    const void *section_start;
    uint32_t section_header_length;
    uint32_t section_length;
    unsigned int section_type;
    size_t bytes_remaining = 0;

    *compressors = NULL;
    *chunk_sizes = NULL;
    *chunk_offsets = NULL;

    result = hap_read_section_header(texture_section, texture_section_length, &section_header_length, &section_length, &section_type);

    if (result == HapResult_No_Error && section_type != kHapSectionDecodeInstructionsContainer)
    {
        result = HapResult_Bad_Frame;
    }

    if (result != HapResult_No_Error)
    {
        return result;
    }

    /*
     Frame data follows immediately after the Decode Instructions Container
     */
    *frame_data = ((const char *)texture_section) + section_header_length + section_length;

    /*
     Step through the sections inside the Decode Instructions Container
     */
    section_start = ((uint8_t *)texture_section) + section_header_length;
    bytes_remaining = section_length;

    while (bytes_remaining > 0) {
        unsigned int section_chunk_count = 0;
        result = hap_read_section_header(section_start, bytes_remaining, &section_header_length, &section_length, &section_type);
        if (result != HapResult_No_Error)
        {
            return result;
        }
        section_start = ((uint8_t *)section_start) + section_header_length;
        switch (section_type) {
            case kHapSectionChunkSecondStageCompressorTable:
                *compressors = section_start;
                section_chunk_count = section_length;
                break;
            case kHapSectionChunkSizeTable:
                *chunk_sizes = section_start;
                section_chunk_count = section_length / 4;
                break;
            case kHapSectionChunkOffsetTable:
                *chunk_offsets = section_start;
                section_chunk_count = section_length / 4;
                break;
            default:
                // Ignore unrecognized sections
                break;
        }

        /*
         If we calculated a chunk count and already have one, make sure they match
         */
        if (section_chunk_count != 0)
        {
            if ((*chunk_count) != 0 && section_chunk_count != (*chunk_count))
            {
                return HapResult_Bad_Frame;
            }
            *chunk_count = section_chunk_count;
        }

        section_start = ((uint8_t *)section_start) + section_length;
        bytes_remaining -= section_header_length + section_length;
    }

    /*
     The Chunk Second-Stage Compressor Table and Chunk Size Table are required
     */
    if (*compressors == NULL || *chunk_sizes == NULL)
    {
        return HapResult_Bad_Frame;
    }
    return result;
}

unsigned int hap_decode_single_texture(const void *texture_section, uint32_t texture_section_length,
                                       unsigned int texture_section_type,
                                       HapDecodeCallback callback, void *info,
                                       void *outputBuffer, unsigned long outputBufferBytes,
                                       unsigned long *outputBufferBytesUsed,
                                       unsigned int *outputBufferTextureFormat)
{
    int result = HapResult_No_Error;
    unsigned int textureFormat;
    unsigned int compressor;
    size_t bytesUsed = 0;

    /*
     One top-level section type describes texture-format and second-stage compression
     Hap compressor/format constants can be unpacked by reading the top and bottom four bits.
     */
    compressor = hap_top_4_bits(texture_section_type);
    textureFormat = hap_bottom_4_bits(texture_section_type);

    /*
     Pass the texture format out
     */
    *outputBufferTextureFormat = hap_texture_format_constant_for_format_identifier(textureFormat);
    if (*outputBufferTextureFormat == 0)
    {
        return HapResult_Bad_Frame;
    }

    if (compressor == kHapCompressorComplex)
    {
        /*
         The top-level section should contain a Decode Instructions Container followed by frame data
         */
        int chunk_count = 0;
        const void *compressors = NULL;
        const void *chunk_sizes = NULL;
        const void *chunk_offsets = NULL;
        const char *frame_data = NULL;

        result = hap_decode_header_complex_instructions(texture_section, texture_section_length, &chunk_count, &compressors, &chunk_sizes, &chunk_offsets, &frame_data);

        if (result != HapResult_No_Error)
        {
            return result;
        }

        if (chunk_count > 0)
        {
            /*
             Step through the chunks, storing information for their decompression
             */
            HapChunkDecodeInfo *chunk_info = (HapChunkDecodeInfo *)malloc(sizeof(HapChunkDecodeInfo) * chunk_count);

            size_t running_compressed_chunk_size = 0;
            size_t running_uncompressed_chunk_size = 0;
            int i;

            if (chunk_info == NULL)
            {
                return HapResult_Internal_Error;
            }

            for (i = 0; i < chunk_count; i++) {

                chunk_info[i].compressor = *(((uint8_t *)compressors) + i);

                chunk_info[i].compressed_chunk_size = hap_read_4_byte_uint(((uint8_t *)chunk_sizes) + (i * 4));

                if (chunk_offsets)
                {
                    chunk_info[i].compressed_chunk_data = frame_data + hap_read_4_byte_uint(((uint8_t *)chunk_offsets) + (i * 4));
                }
                else
                {
                    chunk_info[i].compressed_chunk_data = frame_data + running_compressed_chunk_size;
                }

                running_compressed_chunk_size += chunk_info[i].compressed_chunk_size;

                if (chunk_info[i].compressor == kHapCompressorSnappy)
                {
                    snappy_status snappy_result = snappy_uncompressed_length(chunk_info[i].compressed_chunk_data,
                        chunk_info[i].compressed_chunk_size,
                        &(chunk_info[i].uncompressed_chunk_size));

                    if (snappy_result != SNAPPY_OK)
                    {
                        switch (snappy_result)
                        {
                        case SNAPPY_INVALID_INPUT:
                            result = HapResult_Bad_Frame;
                            break;
                        default:
                            result = HapResult_Internal_Error;
                            break;
                        }
                        break;
                    }
                }
                else
                {
                    chunk_info[i].uncompressed_chunk_size = chunk_info[i].compressed_chunk_size;
                }

                chunk_info[i].uncompressed_chunk_data = (char *)(((uint8_t *)outputBuffer) + running_uncompressed_chunk_size);
                running_uncompressed_chunk_size += chunk_info[i].uncompressed_chunk_size;
            }

            if (result == HapResult_No_Error && running_uncompressed_chunk_size > outputBufferBytes)
            {
                result = HapResult_Buffer_Too_Small;
            }

            if (result == HapResult_No_Error)
            {
                /*
                 Perform decompression
                 */
                bytesUsed = running_uncompressed_chunk_size;

                if (chunk_count == 1)
                {
                    /*
                     We don't invoke the callback for one chunk, just decode it directly
                     */
                    hap_decode_chunk(chunk_info, 0);
                }
                else
                {
                    callback((HapDecodeWorkFunction)hap_decode_chunk, chunk_info, chunk_count, info);
                }

                /*
                 Check to see if we encountered any errors and report one of them
                 */
                for (i = 0; i < chunk_count; i++)
                {
                    if (chunk_info[i].result != HapResult_No_Error)
                    {
                        result = chunk_info[i].result;
                        break;
                    }
                }
            }

            free(chunk_info);

            if (result != HapResult_No_Error)
            {
                return result;
            }
        }
    }
    else if (compressor == kHapCompressorSnappy)
    {
        /*
         Only one section is present containing a single block of snappy-compressed texture data
         */
        snappy_status snappy_result = snappy_uncompressed_length((const char *)texture_section, texture_section_length, &bytesUsed);
        if (snappy_result != SNAPPY_OK)
        {
            return HapResult_Internal_Error;
        }
        if (bytesUsed > outputBufferBytes)
        {
            return HapResult_Buffer_Too_Small;
        }
        snappy_result = snappy_uncompress((const char *)texture_section, texture_section_length, (char *)outputBuffer, &bytesUsed);
        if (snappy_result != SNAPPY_OK)
        {
            return HapResult_Internal_Error;
        }
    }
    else if (compressor == kHapCompressorNone)
    {
        /*
         Only one section is present containing a single block of uncompressed texture data
         */
        bytesUsed = texture_section_length;
        if (texture_section_length > outputBufferBytes)
        {
            return HapResult_Buffer_Too_Small;
        }
        memcpy(outputBuffer, texture_section, texture_section_length);
    }
    else
    {
        return HapResult_Bad_Frame;
    }
    /*
     Fill out the remaining return value
     */
    if (outputBufferBytesUsed != NULL)
    {
        *outputBufferBytesUsed = bytesUsed;
    }
    
    return HapResult_No_Error;
}

int hap_get_section_at_index(const void *input_buffer, uint32_t input_buffer_bytes,
                             unsigned int index,
                             const void **section, uint32_t *section_length, unsigned int *section_type)
{
    int result;
    uint32_t section_header_length;

    result = hap_read_section_header(input_buffer, input_buffer_bytes, &section_header_length, section_length, section_type);

    if (result != HapResult_No_Error)
    {
        return result;
    }

    if (*section_type == kHapSectionMultipleImages)
    {
        /*
         Step through until we find the section at index
         */
        size_t offset = 0;
        size_t top_section_length = *section_length;
        input_buffer = ((uint8_t *)input_buffer) + section_header_length;
        section_header_length = 0;
        *section_length = 0;
        for (int i = 0; i <= index; i++) {
            offset += section_header_length + *section_length;
            if (offset >= top_section_length)
            {
                return HapResult_Bad_Arguments;
            }
            result = hap_read_section_header(((uint8_t *)input_buffer) + offset,
                                             top_section_length - offset,
                                             &section_header_length,
                                             section_length,
                                             section_type);
            if (result != HapResult_No_Error)
            {
                return result;
            }
        }
        offset += section_header_length;
        *section = ((uint8_t *)input_buffer) + offset;
        return HapResult_No_Error;
    }
    else if (index == 0)
    {
        /*
         A single-texture frame with the texture as the top section.
         */
        *section = ((uint8_t *)input_buffer) + section_header_length;
        return HapResult_No_Error;
    }
    else
    {
        *section = NULL;
        *section_length = 0;
        *section_type = 0;
        return HapResult_Bad_Arguments;
    }
}

unsigned int HapDecode(const void *inputBuffer, unsigned long inputBufferBytes,
                       unsigned int index,
                       HapDecodeCallback callback, void *info,
                       void *outputBuffer, unsigned long outputBufferBytes,
                       unsigned long *outputBufferBytesUsed,
                       unsigned int *outputBufferTextureFormat)
{
    int result = HapResult_No_Error;
    const void *section;
    uint32_t section_length;
    unsigned int section_type;

    /*
     Check arguments
     */
    if (inputBuffer == NULL
        || index > 1
        || callback == NULL
        || outputBuffer == NULL
        || outputBufferTextureFormat == NULL
        )
    {
        return HapResult_Bad_Arguments;
    }

    /*
     Locate the section at the given index, which will either be the top-level section in a single texture image, or one of the
     sections inside a multi-image top-level section.
     */
    result = hap_get_section_at_index(inputBuffer, inputBufferBytes, index, &section, &section_length, &section_type);

    if (result == HapResult_No_Error)
    {
        /*
         Decode the located texture
         */
        result = hap_decode_single_texture(section,
                                           section_length,
                                           section_type,
                                           callback, info,
                                           outputBuffer,
                                           outputBufferBytes,
                                           outputBufferBytesUsed,
                                           outputBufferTextureFormat);
    }

    return result;
}

unsigned int HapGetFrameTextureCount(const void *inputBuffer, unsigned long inputBufferBytes, unsigned int *outputTextureCount)
{
    int result;
    uint32_t section_header_length;
    uint32_t section_length;
    unsigned int section_type;

    result = hap_read_section_header(inputBuffer, inputBufferBytes, &section_header_length, &section_length, &section_type);

    if (result != HapResult_No_Error)
    {
        return result;
    }

    if (section_type == kHapSectionMultipleImages)
    {
        /*
         Step through, counting sections
         */
        uint32_t offset = section_header_length;
        uint32_t top_section_length = section_length;
        *outputTextureCount = 0;
        while (offset < top_section_length) {
            result = hap_read_section_header(((uint8_t *)inputBuffer) + offset,
                                             inputBufferBytes - offset,
                                             &section_header_length,
                                             &section_length,
                                             &section_type);
            if (result != HapResult_No_Error)
            {
                return result;
            }
            offset += section_header_length + section_length;
            *outputTextureCount += 1;
        }
        return HapResult_No_Error;
    }
    else
    {
        /*
         A single-texture frame with the texture as the top section.
         */
        *outputTextureCount = 1;
        return HapResult_No_Error;
    }
}

unsigned int HapGetFrameTextureFormat(const void *inputBuffer, unsigned long inputBufferBytes, unsigned int index, unsigned int *outputBufferTextureFormat)
{
    unsigned int result = HapResult_No_Error;
    const void *section;
    uint32_t section_length;
    unsigned int section_type;
    /*
     Check arguments
     */
    if (inputBuffer == NULL
        || index > 1
        || outputBufferTextureFormat == NULL
        )
    {
        return HapResult_Bad_Arguments;
    }
    /*
     Locate the section at the given index, which will either be the top-level section in a single texture image, or one of the
     sections inside a multi-image top-level section.
     */
    result = hap_get_section_at_index(inputBuffer, inputBufferBytes, index, &section, &section_length, &section_type);

    if (result == HapResult_No_Error)
    {
        /*
         Pass the API enum value to match the constant out
         */
        *outputBufferTextureFormat = hap_texture_format_constant_for_format_identifier(hap_bottom_4_bits(section_type));
        /*
         Check a valid format was present
         */
        if (*outputBufferTextureFormat == 0)
        {
            result = HapResult_Bad_Frame;
        }
    }
    return result;
}

unsigned int HapGetFrameTextureChunkCount(const void *inputBuffer, unsigned long inputBufferBytes, unsigned int index, int *chunk_count)
{
    unsigned int result = HapResult_No_Error;
    const void *section;
    uint32_t section_length;
    unsigned int section_type;
    *chunk_count = 0;

    /*
     Check arguments
     */
    if (inputBuffer == NULL
        || index > 1
        )
    {
        return HapResult_Bad_Arguments;
    }
    /*
     Locate the section at the given index, which will either be the top-level section in a single texture image, or one of the
     sections inside a multi-image top-level section.
     */
    result = hap_get_section_at_index(inputBuffer, inputBufferBytes, index, &section, &section_length, &section_type);

    if (result == HapResult_No_Error)
    {
        unsigned int compressor;

        /*
         One top-level section type describes texture-format and second-stage compression
         Hap compressor/format constants can be unpacked by reading the top and bottom four bits.
         */
        compressor = hap_top_4_bits(section_type);

        if (compressor == kHapCompressorComplex)
        {
            /*
             The top-level section should contain a Decode Instructions Container followed by frame data
             */
            const void *compressors = NULL;
            const void *chunk_sizes = NULL;
            const void *chunk_offsets = NULL;
            const char *frame_data = NULL;

            result = hap_decode_header_complex_instructions(section, section_length, chunk_count, &compressors, &chunk_sizes, &chunk_offsets, &frame_data);

            if (result != HapResult_No_Error)
            {
                return result;
            }
        }
        else if ((compressor == kHapCompressorSnappy)||(compressor == kHapCompressorNone))
        {
            *chunk_count = 1;
        }
        else
        {
            return HapResult_Bad_Frame;
        }
    }
    return result;
}
