/*
 * Copyright (c) 2024 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <stdio.h>
#include <string.h>
#include <inttypes.h>

#include "hardware/sha256.h"
#include "pico/bootrom/lock.h"
#include "pico/sha256.h"
#include "pico/time.h"

// We add one 0x80 byte, then 8 bytes for the size
#define SHA256_PADDING_DATA_BYTES 9
#define SHA256_BLOCK_SIZE_BYTES 64

bool __weak pico_sha256_lock(pico_sha256_state_t *state) {
    if (!bootrom_try_acquire_lock(BOOTROM_LOCK_SHA_256))
        return false;
    state->locked = true;
    return true;
}

void __weak pico_sha256_unlock(pico_sha256_state_t *state) {
    assert(state->locked);
    bootrom_release_lock(BOOTROM_LOCK_SHA_256);
    state->locked = false;
}

void pico_sha256_cleanup(pico_sha256_state_t *state) {
    if (state->locked) {
        pico_sha256_unlock(state);
    }
}

int pico_sha256_try_start(pico_sha256_state_t *state, enum sha256_endianness endianness, bool use_dma) {
    memset(state, 0, sizeof(*state));
    if (!pico_sha256_lock(state)) return PICO_ERROR_RESOURCE_IN_USE;
    state->endianness = endianness;
    if (use_dma) {
        state->channel = (int8_t)dma_claim_unused_channel(false);
        if (state->channel < 0) {
            pico_sha256_unlock(state);
            return PICO_ERROR_INSUFFICIENT_RESOURCES;
        }
        state->config = dma_channel_get_default_config(state->channel);
        channel_config_set_transfer_data_size(&state->config, DMA_SIZE_8);
        channel_config_set_read_increment(&state->config, true);
        channel_config_set_write_increment(&state->config, false);
        channel_config_set_dreq(&state->config, DREQ_SHA256);
        sha256_set_dma_size(1);
    } else {
        state->channel = -1;
    }
    sha256_err_not_ready_clear();
    sha256_set_bswap(endianness == SHA256_BIG_ENDIAN);
    sha256_start();
    state->total_data_size = 0;
    return PICO_OK;
}

int pico_sha256_start_blocking_until(pico_sha256_state_t *state, enum sha256_endianness endianness, bool use_dma, absolute_time_t until) {
    int rc;
    do {
        rc = pico_sha256_try_start(state, endianness, use_dma);
        if (rc != PICO_ERROR_RESOURCE_IN_USE) break;
        if (time_reached(until)) {
            rc = PICO_ERROR_TIMEOUT;
            break;
        }
    } while (true);
    return rc;
}

static void write_to_hardware(pico_sha256_state_t *state, const uint8_t *data, size_t data_size_bytes) {
    if (state->channel >= 0) {
        dma_channel_wait_for_finish_blocking(state->channel);
        assert(!sha256_err_not_ready());
        sha256_wait_ready_blocking();
        dma_channel_configure(
            state->channel,
            &state->config,
            sha256_get_write_addr(),
            data,
            data_size_bytes,
            true
        );
    } else {
        if (!state->cache_used && !(((uintptr_t)data)&3u)) {
            GCC_Like_Pragma("GCC diagnostic ignored \"-Wcast-align\"")
            const uint32_t *data32 = (const uint32_t *)data;
            // aligned writes
            while (data_size_bytes >= 4) {
                // write a whole word
                sha256_wait_ready_blocking();
                sha256_put_word(*data32++);
                data_size_bytes -= 4;
            }
            data = (const uint8_t *)data32;
        }
        while (data_size_bytes--) {
            state->cache.bytes[state->cache_used++] = *data++;
            if (state->cache_used == 4) {
                state->cache_used = 0;
                sha256_wait_ready_blocking();
                sha256_put_word(state->cache.word);
            }
        }
    }
}

static void update_internal(pico_sha256_state_t *state, const uint8_t *data, size_t data_size_bytes) {
    assert(state->locked);
    // must finish off the last 64 byte block first or else sha256_err_not_ready will be true
    size_t bytes_left = ((state->total_data_size + (SHA256_BLOCK_SIZE_BYTES - 1)) & ~(SHA256_BLOCK_SIZE_BYTES - 1)) - state->total_data_size;
    if (bytes_left > data_size_bytes) bytes_left = data_size_bytes;
    if (bytes_left > 0) {
        write_to_hardware(state, data, bytes_left);
        state->total_data_size += bytes_left;
        data_size_bytes -= bytes_left;
        data += bytes_left;
    }
    // Write the rest of the data
    if (data_size_bytes > 0) {
        write_to_hardware(state, data, data_size_bytes);
        state->total_data_size += data_size_bytes;
    }
}

static void add_zero_bytes(pico_sha256_state_t *state, size_t data_size_bytes) {
    uint32_t zero = 0;
    // todo: can be done a bit more efficiently with dma?
    assert(data_size_bytes < INT32_MAX);
    while((int32_t)data_size_bytes > 0) {
        update_internal(state, (uint8_t *)&zero, MIN(4, data_size_bytes));
        data_size_bytes -= 4;
    }
}

void pico_sha256_update(pico_sha256_state_t *state, const uint8_t *data, size_t data_size_bytes) {
    update_internal(state, data, data_size_bytes);
}

void pico_sha256_update_blocking(pico_sha256_state_t *state, const uint8_t *data, size_t data_size_bytes) {
    update_internal(state, data, data_size_bytes);
    if (state->channel >= 0) {
        dma_channel_wait_for_finish_blocking(state->channel);
    }
}

// write the SHA-256 padding to hardware
static void write_padding(pico_sha256_state_t *state) {
    // Has to be a multiple of 64 bytes
    uint64_t size = (state->total_data_size + SHA256_PADDING_DATA_BYTES + (SHA256_BLOCK_SIZE_BYTES - 1)) & ~(SHA256_BLOCK_SIZE_BYTES - 1);
    const size_t user_data_size = state->total_data_size;
    const size_t padding_size_bytes = size - state->total_data_size;

    // append a single '1' bit
    const uint8_t one_bit = 0x80;
    update_internal(state, &one_bit, 1);

    // Zero unused padding
    add_zero_bytes(state, padding_size_bytes - SHA256_PADDING_DATA_BYTES);

    // Add size in bits, big endian
    size = __builtin_bswap64(user_data_size * 8);
    update_internal(state, (uint8_t*)&size, sizeof(uint64_t)); // last write
}

void pico_sha256_finish(pico_sha256_state_t *state, sha256_result_t *out) {
    assert(state->locked);
    // pass NULL to abandon the current hash in case of an error
    if (out) {
        write_padding(state);
        if (state->channel >= 0) {
            dma_channel_wait_for_finish_blocking(state->channel);
            assert(!sha256_err_not_ready());
        }
        sha256_wait_valid_blocking();
        sha256_get_result(out, state->endianness);
    }
    if (state->channel >= 0) {
        dma_channel_cleanup(state->channel);
        dma_channel_unclaim(state->channel);
        state->channel  = -1;
    }
    pico_sha256_unlock(state);
}
