/**
 * hdr_encoding.c
 * Written by Michael Barker and released to the public domain,
 * as explained at http://creativecommons.org/publicdomain/zero/1.0/
 */

#include <errno.h>
#include <stddef.h>
#include <math.h>

#include "hdr_encoding.h"
#include "hdr_tests.h"

int zig_zag_encode_i64(uint8_t* buffer, int64_t signed_value)
{
    int bytesWritten;
    int64_t value = signed_value;

    value = (value << 1) ^ (value >> 63);
    if (value >> 7 == 0)
    {
        buffer[0] = (uint8_t) value;
        bytesWritten = 1;
    }
    else
    {
        buffer[0] = (uint8_t) ((value & 0x7F) | 0x80);
        if (value >> 14 == 0)
        {
            buffer[1] = (uint8_t) (value >> 7);
            bytesWritten = 2;
        }
        else
        {
            buffer[1] = (uint8_t) ((value >> 7 | 0x80));
            if (value >> 21 == 0)
            {
                buffer[2] = (uint8_t) (value >> 14);
                bytesWritten = 3;
            }
            else
            {
                buffer[2] = (uint8_t) (value >> 14 | 0x80);
                if (value >> 28 == 0)
                {
                    buffer[3] = (uint8_t) (value >> 21);
                    bytesWritten = 4;
                }
                else
                {
                    buffer[3] = (uint8_t) (value >> 21 | 0x80);
                    if (value >> 35 == 0)
                    {
                        buffer[4] = (uint8_t) (value >> 28);
                        bytesWritten = 5;
                    }
                    else
                    {
                        buffer[4] = (uint8_t) (value >> 28 | 0x80);
                        if (value >> 42 == 0)
                        {
                            buffer[5] = (uint8_t) (value >> 35);
                            bytesWritten = 6;
                        }
                        else
                        {
                            buffer[5] = (uint8_t) (value >> 35 | 0x80);
                            if (value >> 49 == 0)
                            {
                                buffer[6] = (uint8_t) (value >> 42);
                                bytesWritten = 7;
                            }
                            else
                            {
                                buffer[6] = (uint8_t) (value >> 42 | 0x80);
                                if (value >> 56 == 0)
                                {
                                    buffer[7] = (uint8_t) (value >> 49);
                                    bytesWritten = 8;
                                }
                                else
                                {
                                    buffer[7] = (uint8_t) (value >> 49 | 0x80);
                                    buffer[8] = (uint8_t) (value >> 56);
                                    bytesWritten = 9;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    return bytesWritten;
}

int zig_zag_decode_i64(const uint8_t* buffer, int64_t* retVal)
{
    uint64_t v = buffer[0];
    uint64_t value = v & 0x7F;
    int bytesRead = 1;
    if ((v & 0x80) != 0)
    {
        bytesRead = 2;
        v = buffer[1];
        value |= (v & 0x7F) << 7;
        if ((v & 0x80) != 0)
        {
            bytesRead = 3;
            v = buffer[2];
            value |= (v & 0x7F) << 14;
            if ((v & 0x80) != 0)
            {
                bytesRead = 4;
                v = buffer[3];
                value |= (v & 0x7F) << 21;
                if ((v & 0x80) != 0)
                {
                    bytesRead = 5;
                    v = buffer[4];
                    value |= (v & 0x7F) << 28;
                    if ((v & 0x80) != 0)
                    {
                        bytesRead = 6;
                        v = buffer[5];
                        value |= (v & 0x7F) << 35;
                        if ((v & 0x80) != 0)
                        {
                            bytesRead = 7;
                            v = buffer[6];
                            value |= (v & 0x7F) << 42;
                            if ((v & 0x80) != 0)
                            {
                                bytesRead = 8;
                                v = buffer[7];
                                value |= (v & 0x7F) << 49;
                                if ((v & 0x80) != 0)
                                {
                                    bytesRead = 9;
                                    v = buffer[8];
                                    value |= v << 56;
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    value = (value >> 1) ^ (-(value & 1));
    *retVal = (int64_t) value;

    return bytesRead;
}

static const char base64_table[] =
    {
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
        'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
        'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/', '\0'
    };

static char get_base_64(uint32_t _24_bit_value, int shift)
{
    uint32_t _6_bit_value = 0x3F & (_24_bit_value >> shift);
    return base64_table[_6_bit_value];
}

static int from_base_64(int c)
{
    if ('A' <= c && c <= 'Z')
    {
        return c - 'A';
    }
    else if ('a' <= c && c <= 'z')
    {
        return (c - 'a') + 26;
    }
    else if ('0' <= c && c <= '9')
    {
        return (c - '0') + 52;
    }
    else if ('+' == c)
    {
        return 62;
    }
    else if ('/' == c)
    {
        return 63;
    }
    else if ('=' == c)
    {
        return 0;
    }

    return EINVAL;
}

size_t hdr_base64_encoded_len(size_t decoded_size)
{
    return (size_t) (ceil(decoded_size / 3.0) * 4.0);
}

size_t hdr_base64_decoded_len(size_t encoded_size)
{
    return (encoded_size / 4) * 3;
}

static void hdr_base64_encode_block_pad(const uint8_t* input, char* output, size_t pad)
{
    uint32_t _24_bit_value = 0;

    switch (pad)
    {
        case 2:
            _24_bit_value = (input[0] << 16) + (input[1] << 8);

            output[0] = get_base_64(_24_bit_value, 18);
            output[1] = get_base_64(_24_bit_value, 12);
            output[2] = get_base_64(_24_bit_value,  6);
            output[3] = '=';

            break;

        case 1:
            _24_bit_value = (input[0] << 16);

            output[0] = get_base_64(_24_bit_value, 18);
            output[1] = get_base_64(_24_bit_value, 12);
            output[2] = '=';
            output[3] = '=';

            break;

        default:
            /* No-op */
            break;
    }
}

/**
 * Assumes that there is 3 input bytes and 4 output chars.
 */
void hdr_base64_encode_block(const uint8_t* input, char* output)
{
    uint32_t _24_bit_value = (input[0] << 16) + (input[1] << 8) + (input[2]);

    output[0] = get_base_64(_24_bit_value, 18);
    output[1] = get_base_64(_24_bit_value, 12);
    output[2] = get_base_64(_24_bit_value,  6);
    output[3] = get_base_64(_24_bit_value,  0);
}

int hdr_base64_encode(
    const uint8_t* input, size_t input_len, char* output, size_t output_len)
{
    size_t i, j, remaining;

    if (hdr_base64_encoded_len(input_len) != output_len)
    {
        return EINVAL;
    }

    for (i = 0, j = 0; input_len - i >= 3 && j < output_len; i += 3, j += 4)
    {
        hdr_base64_encode_block(&input[i], &output[j]);
    }

    remaining = input_len - i;

    hdr_base64_encode_block_pad(&input[i], &output[j], remaining);

    return 0;
}

/**
 * Assumes that there is 4 input chars available and 3 output chars.
 */
void hdr_base64_decode_block(const char* input, uint8_t* output)
{
    uint32_t _24_bit_value = 0;

    _24_bit_value |= from_base_64(input[0]) << 18;
    _24_bit_value |= from_base_64(input[1]) << 12;
    _24_bit_value |= from_base_64(input[2]) << 6;
    _24_bit_value |= from_base_64(input[3]);

    output[0] = (uint8_t) ((_24_bit_value >> 16) & 0xFF);
    output[1] = (uint8_t) ((_24_bit_value >> 8) & 0xFF);
    output[2] = (uint8_t) ((_24_bit_value) & 0xFF);
}

int hdr_base64_decode(
    const char* input, size_t input_len, uint8_t* output, size_t output_len)
{
    size_t i, j;

    if (input_len < 4 ||
        (input_len & 3) != 0 ||
        (input_len / 4) * 3 != output_len)
    {
        return EINVAL;
    }

    for (i = 0, j = 0; i < input_len; i += 4, j += 3)
    {
        hdr_base64_decode_block(&input[i], &output[j]);
    }

    return 0;
}
