#include <string.h>

#include "blake2/blake2.h"
#include "blake2/blake2-impl.h"

#include "core.h"

static const uint64_t blake2b_IV[8] = {
    UINT64_C(0x6a09e667f3bcc908), UINT64_C(0xbb67ae8584caa73b),
    UINT64_C(0x3c6ef372fe94f82b), UINT64_C(0xa54ff53a5f1d36f1),
    UINT64_C(0x510e527fade682d1), UINT64_C(0x9b05688c2b3e6c1f),
    UINT64_C(0x1f83d9abfb41bd6b), UINT64_C(0x5be0cd19137e2179)
};

#define rotr64(x, n) (((x) >> (n)) | ((x) << (64 - (n))))

static const unsigned int blake2b_sigma[12][16] = {
    {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
    {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3},
    {11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4},
    {7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8},
    {9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13},
    {2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9},
    {12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11},
    {13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10},
    {6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5},
    {10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0},
    {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
    {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3},
};

#define G(m, r, i, a, b, c, d) \
    do { \
        a = a + b + m[blake2b_sigma[r][2 * i + 0]]; \
        d = rotr64(d ^ a, 32); \
        c = c + d; \
        b = rotr64(b ^ c, 24); \
        a = a + b + m[blake2b_sigma[r][2 * i + 1]]; \
        d = rotr64(d ^ a, 16); \
        c = c + d; \
        b = rotr64(b ^ c, 63); \
    } while ((void)0, 0)

#define ROUND(m, v, r) \
    do { \
        G(m, r, 0, v[0], v[4], v[ 8], v[12]); \
        G(m, r, 1, v[1], v[5], v[ 9], v[13]); \
        G(m, r, 2, v[2], v[6], v[10], v[14]); \
        G(m, r, 3, v[3], v[7], v[11], v[15]); \
        G(m, r, 4, v[0], v[5], v[10], v[15]); \
        G(m, r, 5, v[1], v[6], v[11], v[12]); \
        G(m, r, 6, v[2], v[7], v[ 8], v[13]); \
        G(m, r, 7, v[3], v[4], v[ 9], v[14]); \
    } while ((void)0, 0)

void blake2b_compress(blake2b_state *S, const void *block, uint64_t f0)
{
    uint64_t m[16];
    uint64_t v[16];

    m[ 0] = load64((const uint64_t *)block +  0);
    m[ 1] = load64((const uint64_t *)block +  1);
    m[ 2] = load64((const uint64_t *)block +  2);
    m[ 3] = load64((const uint64_t *)block +  3);
    m[ 4] = load64((const uint64_t *)block +  4);
    m[ 5] = load64((const uint64_t *)block +  5);
    m[ 6] = load64((const uint64_t *)block +  6);
    m[ 7] = load64((const uint64_t *)block +  7);
    m[ 8] = load64((const uint64_t *)block +  8);
    m[ 9] = load64((const uint64_t *)block +  9);
    m[10] = load64((const uint64_t *)block + 10);
    m[11] = load64((const uint64_t *)block + 11);
    m[12] = load64((const uint64_t *)block + 12);
    m[13] = load64((const uint64_t *)block + 13);
    m[14] = load64((const uint64_t *)block + 14);
    m[15] = load64((const uint64_t *)block + 15);

    v[ 0] = S->h[0];
    v[ 1] = S->h[1];
    v[ 2] = S->h[2];
    v[ 3] = S->h[3];
    v[ 4] = S->h[4];
    v[ 5] = S->h[5];
    v[ 6] = S->h[6];
    v[ 7] = S->h[7];
    v[ 8] = blake2b_IV[0];
    v[ 9] = blake2b_IV[1];
    v[10] = blake2b_IV[2];
    v[11] = blake2b_IV[3];
    v[12] = blake2b_IV[4] ^ S->t[0];
    v[13] = blake2b_IV[5] ^ S->t[1];
    v[14] = blake2b_IV[6] ^ f0;
    v[15] = blake2b_IV[7];

    ROUND(m, v, 0);
    ROUND(m, v, 1);
    ROUND(m, v, 2);
    ROUND(m, v, 3);
    ROUND(m, v, 4);
    ROUND(m, v, 5);
    ROUND(m, v, 6);
    ROUND(m, v, 7);
    ROUND(m, v, 8);
    ROUND(m, v, 9);
    ROUND(m, v, 10);
    ROUND(m, v, 11);

    S->h[0] ^= v[0] ^ v[ 8];
    S->h[1] ^= v[1] ^ v[ 9];
    S->h[2] ^= v[2] ^ v[10];
    S->h[3] ^= v[3] ^ v[11];
    S->h[4] ^= v[4] ^ v[12];
    S->h[5] ^= v[5] ^ v[13];
    S->h[6] ^= v[6] ^ v[14];
    S->h[7] ^= v[7] ^ v[15];
}

static void blake2b_increment_counter(blake2b_state *S, uint64_t inc)
{
    S->t[0] += inc;
    S->t[1] += (S->t[0] < inc);
}

static void blake2b_init_state(blake2b_state *S)
{
    memcpy(S->h, blake2b_IV, sizeof(S->h));
    S->t[1] = S->t[0] = 0;
    S->buflen = 0;
}

void xmrig_ar2_blake2b_init(blake2b_state *S, size_t outlen)
{
    blake2b_init_state(S);
    /* XOR initial state with param block: */
    S->h[0] ^= (uint64_t)outlen | (UINT64_C(1) << 16) | (UINT64_C(1) << 24);
}

void xmrig_ar2_blake2b_update(blake2b_state *S, const void *in, size_t inlen)
{
    const uint8_t *pin = (const uint8_t *)in;

    if (S->buflen + inlen > BLAKE2B_BLOCKBYTES) {
        size_t left = S->buflen;
        size_t fill = BLAKE2B_BLOCKBYTES - left;
        memcpy(&S->buf[left], pin, fill);
        blake2b_increment_counter(S, BLAKE2B_BLOCKBYTES);
        blake2b_compress(S, S->buf, 0);
        S->buflen = 0;
        inlen -= fill;
        pin += fill;
        /* Avoid buffer copies when possible */
        while (inlen > BLAKE2B_BLOCKBYTES) {
            blake2b_increment_counter(S, BLAKE2B_BLOCKBYTES);
            blake2b_compress(S, pin, 0);
            inlen -= BLAKE2B_BLOCKBYTES;
            pin += BLAKE2B_BLOCKBYTES;
        }
    }
    memcpy(&S->buf[S->buflen], pin, inlen);
    S->buflen += inlen;
}

void xmrig_ar2_blake2b_final(blake2b_state *S, void *out, size_t outlen)
{
    uint8_t buffer[BLAKE2B_OUTBYTES] = {0};
    unsigned int i;

    blake2b_increment_counter(S, S->buflen);
    memset(&S->buf[S->buflen], 0, BLAKE2B_BLOCKBYTES - S->buflen); /* Padding */
    blake2b_compress(S, S->buf, UINT64_C(0xFFFFFFFFFFFFFFFF));

    for (i = 0; i < 8; ++i) { /* Output full hash to temp buffer */
        store64(buffer + i * sizeof(uint64_t), S->h[i]);
    }

    memcpy(out, buffer, outlen);
    xmrig_ar2_clear_internal_memory(buffer, sizeof(buffer));
    xmrig_ar2_clear_internal_memory(S->buf, sizeof(S->buf));
    xmrig_ar2_clear_internal_memory(S->h, sizeof(S->h));
}

void xmrig_ar2_blake2b_long(void *out, size_t outlen, const void *in, size_t inlen)
{
    uint8_t *pout = (uint8_t *)out;
    blake2b_state blake_state;
    uint8_t outlen_bytes[sizeof(uint32_t)] = {0};

    store32(outlen_bytes, (uint32_t)outlen);
    if (outlen <= BLAKE2B_OUTBYTES) {
        xmrig_ar2_blake2b_init(&blake_state, outlen);
        xmrig_ar2_blake2b_update(&blake_state, outlen_bytes, sizeof(outlen_bytes));
        xmrig_ar2_blake2b_update(&blake_state, in, inlen);
        xmrig_ar2_blake2b_final(&blake_state, pout, outlen);
    } else {
        uint32_t toproduce;
        uint8_t out_buffer[BLAKE2B_OUTBYTES];

        xmrig_ar2_blake2b_init(&blake_state, BLAKE2B_OUTBYTES);
        xmrig_ar2_blake2b_update(&blake_state, outlen_bytes, sizeof(outlen_bytes));
        xmrig_ar2_blake2b_update(&blake_state, in, inlen);
        xmrig_ar2_blake2b_final(&blake_state, out_buffer, BLAKE2B_OUTBYTES);

        memcpy(pout, out_buffer, BLAKE2B_OUTBYTES / 2);
        pout += BLAKE2B_OUTBYTES / 2;
        toproduce = (uint32_t)outlen - BLAKE2B_OUTBYTES / 2;

        while (toproduce > BLAKE2B_OUTBYTES) {
            xmrig_ar2_blake2b_init(&blake_state, BLAKE2B_OUTBYTES);
            xmrig_ar2_blake2b_update(&blake_state, out_buffer, BLAKE2B_OUTBYTES);
            xmrig_ar2_blake2b_final(&blake_state, out_buffer, BLAKE2B_OUTBYTES);

            memcpy(pout, out_buffer, BLAKE2B_OUTBYTES / 2);
            pout += BLAKE2B_OUTBYTES / 2;
            toproduce -= BLAKE2B_OUTBYTES / 2;
        }

        xmrig_ar2_blake2b_init(&blake_state, toproduce);
        xmrig_ar2_blake2b_update(&blake_state, out_buffer, BLAKE2B_OUTBYTES);
        xmrig_ar2_blake2b_final(&blake_state, out_buffer, toproduce);

        memcpy(pout, out_buffer, toproduce);

        xmrig_ar2_clear_internal_memory(out_buffer, sizeof(out_buffer));
    }
}
