#include <stddef.h>
#include <stdint.h>
#include <string.h>

#include "coduo_engine_structs.h"

#define MD4_INIT_STATE_0 UINT32_C(0x67452301)
#define MD4_INIT_STATE_1 UINT32_C(0xefcdab89)
#define MD4_INIT_STATE_2 UINT32_C(0x98badcfe)
#define MD4_INIT_STATE_3 UINT32_C(0x10325476)
#define MD4_BYTE_BITS 8
#define MD4_COUNT_HIGH_SHIFT 29
#define MD4_BLOCK_MASK (CODUO_MD4_BLOCK_SIZE - 1)
#define MD4_FINAL_LENGTH_SIZE 8
#define MD4_FINAL_COUNT_TARGET 56
#define MD4_FINAL_COUNT_TARGET_NEXT_BLOCK 120
#define COM_BLOCK_CHECKSUM_KEY_BYTES 4
#define MD4_ROUND2_CONSTANT UINT32_C(0x5a827999)
#define MD4_ROUND3_CONSTANT UINT32_C(0x6ed9eba1)

typedef union coduo_md4_checksum_digest_u {
    coduo_md4_digest_t bytes;
    uint32_t words[CODUO_MD4_DIGEST_SIZE / sizeof(uint32_t)];
} coduo_md4_checksum_digest_t;

static coduo_md4_padding_t md4_padding = {0x80};

static void MD4Transform(coduo_md4_context_t *context,
                         const uint8_t block[CODUO_MD4_BLOCK_SIZE]);
static void Encode(uint8_t *output, const uint32_t *input,
                   uint32_t length);
static void Decode(uint32_t *output, const uint8_t *input,
                   uint32_t length);

void Com_Memset(void *dest, int32_t value, size_t count);

void MD4Init(coduo_md4_context_t *context)
{
    context->count[1] = 0;
    context->count[0] = 0;
    context->state[0] = MD4_INIT_STATE_0;
    context->state[1] = MD4_INIT_STATE_1;
    context->state[2] = MD4_INIT_STATE_2;
    context->state[3] = MD4_INIT_STATE_3;
}

void MD4Update(coduo_md4_context_t *context, const void *data,
               uint32_t length)
{
    const uint8_t *input = data;
    uint32_t inputIndex;
    uint32_t partLength;
    uint32_t copyIndex;
    uint32_t lengthBits;

    inputIndex = (context->count[0] >> 3) & MD4_BLOCK_MASK;
    lengthBits = length << 3;
    context->count[0] += lengthBits;
    if (context->count[0] < lengthBits) {
        ++context->count[1];
    }
    context->count[1] += length >> MD4_COUNT_HIGH_SHIFT;

    partLength = CODUO_MD4_BLOCK_SIZE - inputIndex;
    if (length < partLength) {
        copyIndex = 0;
    } else {
        memcpy(&context->buffer[inputIndex], input, partLength);
        MD4Transform(context, context->buffer);
        for (copyIndex = partLength;
             copyIndex + CODUO_MD4_BLOCK_SIZE - 1 < length;
             copyIndex += CODUO_MD4_BLOCK_SIZE) {
            MD4Transform(context, &input[copyIndex]);
        }
        inputIndex = 0;
    }

    memcpy(&context->buffer[inputIndex], &input[copyIndex],
           length - copyIndex);
}

void MD4Final(uint8_t digest[CODUO_MD4_DIGEST_SIZE],
              coduo_md4_context_t *context)
{
    uint8_t bits[MD4_FINAL_LENGTH_SIZE];
    uint32_t inputIndex;
    uint32_t padLength;

    Encode(bits, context->count, MD4_FINAL_LENGTH_SIZE);
    inputIndex = (context->count[0] >> 3) & MD4_BLOCK_MASK;
    if (inputIndex < MD4_FINAL_COUNT_TARGET) {
        padLength = MD4_FINAL_COUNT_TARGET - inputIndex;
    } else {
        padLength = MD4_FINAL_COUNT_TARGET_NEXT_BLOCK - inputIndex;
    }

    MD4Update(context, md4_padding, padLength);
    MD4Update(context, bits, MD4_FINAL_LENGTH_SIZE);
    Encode(digest, context->state, CODUO_MD4_DIGEST_SIZE);

    Com_Memset(context, 0, sizeof(*context));
}

int32_t Com_BlockChecksum(const void *buffer, int length)
{
    coduo_md4_context_t context;
    coduo_md4_checksum_digest_t digest;

    MD4Init(&context);
    MD4Update(&context, buffer, (uint32_t)length);
    MD4Final(digest.bytes, &context);

    return digest.words[1] ^ digest.words[0] ^ digest.words[2] ^
           digest.words[3];
}

int32_t Com_BlockChecksumKey(const void *buffer, int length, int key)
{
    coduo_md4_context_t context;
    coduo_md4_checksum_digest_t digest;

    MD4Init(&context);
    MD4Update(&context, &key, COM_BLOCK_CHECKSUM_KEY_BYTES);
    MD4Update(&context, buffer, (uint32_t)length);
    MD4Final(digest.bytes, &context);

    return digest.words[1] ^ digest.words[0] ^ digest.words[2] ^
           digest.words[3];
}

static void MD4Transform(
    coduo_md4_context_t *context,
    const uint8_t block[CODUO_MD4_BLOCK_SIZE])
{
    uint32_t a;
    uint32_t b;
    uint32_t c;
    uint32_t d;
    uint32_t x[CODUO_MD4_BLOCK_SIZE / sizeof(uint32_t)];

    a = context->state[0];
    b = context->state[1];
    c = context->state[2];
    d = context->state[3];

    Decode(x, block, CODUO_MD4_BLOCK_SIZE);

#define MD4_ROTATE_LEFT(value, bits) \
    (((value) << (bits)) | ((value) >> (32U - (bits))))
#define MD4_ROUND1(a_, b_, c_, d_, x_, s_) \
    do { \
        (a_) += (((~(b_)) & (d_)) | ((b_) & (c_))) + (x_); \
        (a_) = MD4_ROTATE_LEFT((a_), (s_)); \
    } while (0)
#define MD4_ROUND2(a_, b_, c_, d_, x_, s_) \
    do { \
        (a_) += (((b_) & (c_)) | (((b_) | (c_)) & (d_))) + \
                (x_) + MD4_ROUND2_CONSTANT; \
        (a_) = MD4_ROTATE_LEFT((a_), (s_)); \
    } while (0)
#define MD4_ROUND3(a_, b_, c_, d_, x_, s_) \
    do { \
        (a_) += ((b_) ^ (c_) ^ (d_)) + (x_) + MD4_ROUND3_CONSTANT; \
        (a_) = MD4_ROTATE_LEFT((a_), (s_)); \
    } while (0)

    MD4_ROUND1(a, b, c, d, x[0], 3);
    MD4_ROUND1(d, a, b, c, x[1], 7);
    MD4_ROUND1(c, d, a, b, x[2], 11);
    MD4_ROUND1(b, c, d, a, x[3], 19);
    MD4_ROUND1(a, b, c, d, x[4], 3);
    MD4_ROUND1(d, a, b, c, x[5], 7);
    MD4_ROUND1(c, d, a, b, x[6], 11);
    MD4_ROUND1(b, c, d, a, x[7], 19);
    MD4_ROUND1(a, b, c, d, x[8], 3);
    MD4_ROUND1(d, a, b, c, x[9], 7);
    MD4_ROUND1(c, d, a, b, x[10], 11);
    MD4_ROUND1(b, c, d, a, x[11], 19);
    MD4_ROUND1(a, b, c, d, x[12], 3);
    MD4_ROUND1(d, a, b, c, x[13], 7);
    MD4_ROUND1(c, d, a, b, x[14], 11);
    MD4_ROUND1(b, c, d, a, x[15], 19);

    MD4_ROUND2(a, b, c, d, x[0], 3);
    MD4_ROUND2(d, a, b, c, x[4], 5);
    MD4_ROUND2(c, d, a, b, x[8], 9);
    MD4_ROUND2(b, c, d, a, x[12], 13);
    MD4_ROUND2(a, b, c, d, x[1], 3);
    MD4_ROUND2(d, a, b, c, x[5], 5);
    MD4_ROUND2(c, d, a, b, x[9], 9);
    MD4_ROUND2(b, c, d, a, x[13], 13);
    MD4_ROUND2(a, b, c, d, x[2], 3);
    MD4_ROUND2(d, a, b, c, x[6], 5);
    MD4_ROUND2(c, d, a, b, x[10], 9);
    MD4_ROUND2(b, c, d, a, x[14], 13);
    MD4_ROUND2(a, b, c, d, x[3], 3);
    MD4_ROUND2(d, a, b, c, x[7], 5);
    MD4_ROUND2(c, d, a, b, x[11], 9);
    MD4_ROUND2(b, c, d, a, x[15], 13);

    MD4_ROUND3(a, b, c, d, x[0], 3);
    MD4_ROUND3(d, a, b, c, x[8], 9);
    MD4_ROUND3(c, d, a, b, x[4], 11);
    MD4_ROUND3(b, c, d, a, x[12], 15);
    MD4_ROUND3(a, b, c, d, x[2], 3);
    MD4_ROUND3(d, a, b, c, x[10], 9);
    MD4_ROUND3(c, d, a, b, x[6], 11);
    MD4_ROUND3(b, c, d, a, x[14], 15);
    MD4_ROUND3(a, b, c, d, x[1], 3);
    MD4_ROUND3(d, a, b, c, x[9], 9);
    MD4_ROUND3(c, d, a, b, x[5], 11);
    MD4_ROUND3(b, c, d, a, x[13], 15);
    MD4_ROUND3(a, b, c, d, x[3], 3);
    MD4_ROUND3(d, a, b, c, x[11], 9);
    MD4_ROUND3(c, d, a, b, x[7], 11);
    MD4_ROUND3(b, c, d, a, x[15], 15);

    context->state[0] += a;
    context->state[1] += b;
    context->state[2] += c;
    context->state[3] += d;

    Com_Memset(x, 0, sizeof(x));

#undef MD4_ROUND3
#undef MD4_ROUND2
#undef MD4_ROUND1
#undef MD4_ROTATE_LEFT
}

static void Encode(uint8_t *output, const uint32_t *input,
                   uint32_t length)
{
    uint32_t inputIndex;
    uint32_t outputIndex;

    inputIndex = 0;
    for (outputIndex = 0; outputIndex < length; outputIndex += 4U) {
        output[outputIndex] = (uint8_t)input[inputIndex];
        output[outputIndex + 1U] =
            (uint8_t)(input[inputIndex] >> 8U);
        output[outputIndex + 2U] =
            (uint8_t)(input[inputIndex] >> 16U);
        output[outputIndex + 3U] =
            (uint8_t)(input[inputIndex] >> 24U);
        inputIndex++;
    }
}

static void Decode(uint32_t *output, const uint8_t *input,
                   uint32_t length)
{
    uint32_t inputIndex;
    uint32_t outputIndex;

    outputIndex = 0;
    for (inputIndex = 0; inputIndex < length; inputIndex += 4U) {
        output[outputIndex] =
            ((uint32_t)input[inputIndex]) |
            ((uint32_t)input[inputIndex + 1U] << 8U) |
            ((uint32_t)input[inputIndex + 2U] << 16U) |
            ((uint32_t)input[inputIndex + 3U] << 24U);
        outputIndex++;
    }
}
