/*
 * Copyright (c) 2022 Intel Corporation.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/kernel.h>
#include <zephyr/init.h>
#include <errno.h>
#include <zephyr/crypto/crypto.h>
#include "crypto_intel_sha_priv.h"
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(SHA);

#define DT_DRV_COMPAT intel_adsp_sha

static struct sha_session sha_sessions[SHA_MAX_SESSIONS];

static int intel_sha_get_unused_session_idx(void)
{
	int i;

	for (i = 0; i < SHA_MAX_SESSIONS; i++) {
		if (!sha_sessions[i].in_use) {
			sha_sessions[i].in_use = true;
			return i;
		}
	}
	return -1;
}

static int intel_sha_set_ctl_enable(struct sha_container *sha, int status)
{
	/* wait until not busy when turning off */
	if (status == 0 && sha->dfsha->shactl.part.en == 1) {
		while (sha->dfsha->shasts.part.busy) {
		}
	}

	sha->dfsha->shactl.part.en = status;
	return 0;
}

static int intel_sha_set_resume_length_dw0(struct sha_container *sha, uint32_t lower_length)
{
	int err = -EINVAL;

	if (IS_ALIGNED(lower_length, SHA_REQUIRED_BLOCK_ALIGNMENT)) {
		sha->dfsha->sharldw0.full = lower_length;
		err = 0;
	}

	return err;
}

static int intel_sha_set_resume_length_dw1(struct sha_container *sha, uint32_t upper_length)
{
	sha->dfsha->sharldw1.full = upper_length;
	return 0;
}

static int intel_sha_regs_cpy(void *dst, const void *src, size_t len)
{
	uint32_t counter;
	int err = -EINVAL;

	if ((IS_ALIGNED(len, sizeof(uint32_t))) && (IS_ALIGNED(dst, sizeof(uint32_t))) &&
	    (IS_ALIGNED(src, sizeof(uint32_t)))) {
		len /= sizeof(uint32_t);
		for (counter = 0; counter != len; ++counter) {
			((uint32_t *)dst)[counter] = ((uint32_t *)src)[counter];
		}

		err = 0;
	}

	return err;
}

/* ! Perform SHA computation over requested region. */
static int intel_sha_device_run(const struct device *dev, const void *buf_in, size_t buf_in_size,
				size_t max_buff_len, uint32_t state)
{
	int err;
	struct sha_container *const self = dev->data;
	union sha_state state_u = { .full = state };
	/* align to OWORD */
	const size_t aligned_buff_size = ROUND_UP(buf_in_size, 0x10);

	err = intel_sha_set_ctl_enable(self, 0);
	if (err) {
		return err;
	}

	/* set processing element disable */
	self->dfsha->pibcs.part.peen = 0;
	/* set pib base addr */
	self->dfsha->pibba.full = (uint32_t)buf_in;

	if (max_buff_len < aligned_buff_size) {
		return -EINVAL;
	}

	self->dfsha->pibs.full = aligned_buff_size;
	/* enable interrupt */
	self->dfsha->pibcs.part.bscie = 1;
	self->dfsha->pibcs.part.teie = 0;
	/* set processing element enable */
	self->dfsha->pibcs.part.peen = 1;

	if (self->dfsha->shactl.part.en) {
		return -EINVAL; /* already enabled */
	}

	self->dfsha->shactl.part.hrsm = state_u.part.hrsm;

	/* set initial values if resuming */
	if (state_u.part.hrsm) {
		err = intel_sha_set_resume_length_dw0(self, self->dfsha->shaaldw0.full);
		if (err) {
			return err;
		}
		err = intel_sha_set_resume_length_dw1(self, self->dfsha->shaaldw1.full);
		if (err) {
			return err;
		}
		err = intel_sha_regs_cpy((void *)self->dfsha->initial_vector,
					 (void *)self->dfsha->sha_result,
					 sizeof(self->dfsha->initial_vector));
		if (err) {
			return err;
		}
	}

	/* set ctl hash first middle */
	if (self->dfsha->shactl.part.en) {
		return -EINVAL; /* already enabled */
	}

	self->dfsha->shactl.part.hfm = state_u.part.state;

	/* increment pointer */
	self->dfsha->pibfpi.full = buf_in_size;

	err = intel_sha_set_ctl_enable(self, 1);
	if (err) {
		return err;
	}

	err = intel_sha_set_ctl_enable(self, 0);

	return err;
}

static int intel_sha_copy_hash(struct sha_container *const self, void *dst, size_t len)
{
	/* NOTE: generated hash value should be read from the end */

	int err = -EINVAL;
	uint32_t counter = 0;
	uint32_t last_idx = 0;

	if ((IS_ALIGNED(len, sizeof(uint32_t))) && (IS_ALIGNED(dst, sizeof(uint32_t)))) {
		len /= sizeof(uint32_t);
		counter = 0;
		/* The index of a last element in the sha result buffer. */
		last_idx = (sizeof(self->dfsha->sha_result) / sizeof(uint32_t)) - 1;

		for (counter = 0; counter != len; counter++) {
			((uint32_t *)dst)[counter] =
				((uint32_t *)self->dfsha->sha_result)[last_idx - counter];
		}

		err = 0;
	}

	return err;
}

static int intel_sha_device_get_hash(const struct device *dev, void *buf_out, size_t buf_out_size)
{
	int err;
	struct sha_container *const self = dev->data;

	if (buf_out == NULL) {
		return -EINVAL;
	}
	/* wait until not busy */
	while (self->dfsha->shasts.part.busy) {
	}

	err = intel_sha_copy_hash(self, buf_out, buf_out_size);

	return err;
}

static int intel_sha_compute(struct hash_ctx *ctx, struct hash_pkt *pkt, bool finish)
{
	int ret;
	struct sha_container *self = (struct sha_container *const)(ctx->device)->data;
	struct sha_session *session = (struct sha_session *)ctx->drv_sessn_state;
	size_t frag_length;
	size_t output_size;
	uint32_t *hash_int_ptr = (uint32_t *)(pkt->out_buf);

	/* set algo */
	self->dfsha->shactl.full = 0x0;
	self->dfsha->shactl.part.algo = session->algo;

	/* restore ctx */
	self->dfsha->shaaldw0 = session->sha_ctx.shaaldw0;
	self->dfsha->shaaldw1 = session->sha_ctx.shaaldw1;

	ret = intel_sha_regs_cpy((void *)self->dfsha->initial_vector,
				 (void *)session->sha_ctx.initial_vector,
				 sizeof(self->dfsha->initial_vector));
	if (ret) {
		return ret;
	}

	ret = intel_sha_regs_cpy((void *)self->dfsha->sha_result,
				 (void *)session->sha_ctx.sha_result,
				 sizeof(self->dfsha->sha_result));

	if (ret) {
		return ret;
	}

	/* compute hash */
	do {
		frag_length = pkt->in_len > SHA_API_MAX_FRAG_LEN ?
					     SHA_API_MAX_FRAG_LEN :
						   pkt->in_len;

		if ((frag_length == pkt->in_len) && finish) {
			session->state.part.state = SHA_LAST;
		}

		ret = intel_sha_device_run(ctx->device, pkt->in_buf, frag_length, frag_length,
					   session->state.full);
		if (ret) {
			return ret;
		}

		/* set state for next iteration */
		session->state.part.hrsm = SHA_HRSM_ENABLE;
		session->state.part.state = SHA_MIDLE;

		pkt->in_len -= frag_length;
		pkt->in_buf += frag_length;
	} while (pkt->in_len > 0);

	if (finish) {
		switch (self->dfsha->shactl.part.algo) {
		case CRYPTO_HASH_ALGO_SHA224:
			output_size = SHA224_ALGORITHM_HASH_SIZEOF;
			break;
		case CRYPTO_HASH_ALGO_SHA256:
			output_size = SHA256_ALGORITHM_HASH_SIZEOF;
			break;
		case CRYPTO_HASH_ALGO_SHA384:
			output_size = SHA384_ALGORITHM_HASH_SIZEOF;
			break;
		case CRYPTO_HASH_ALGO_SHA512:
			output_size = SHA512_ALGORITHM_HASH_SIZEOF;
			break;
		default:
			return -ENOTSUP;
		}
		ret = intel_sha_device_get_hash(ctx->device, pkt->out_buf, output_size);

		if (ret) {
			return ret;
		}

		/* Fix byte ordering to match common hash representation. */
		for (size_t i = 0; i != output_size / sizeof(uint32_t); i++) {
			hash_int_ptr[i] = BYTE_SWAP32(hash_int_ptr[i]);
		}
	}
	return ret;
}

static int intel_sha_device_set_hash_type(const struct device *dev, struct hash_ctx *ctx,
					  enum hash_algo algo)
{
	int ctx_idx;
	struct sha_container *self = (struct sha_container *const)(dev)->data;

	ctx_idx = intel_sha_get_unused_session_idx();

	if (ctx_idx < 0) {
		LOG_ERR("All sessions in use!");
		return -ENOSPC;
	}
	ctx->drv_sessn_state = &sha_sessions[ctx_idx];

	/* set processing element enable */
	self->dfsha->pibcs.part.peen = 0;

	/* populate sha session data */
	sha_sessions[ctx_idx].state.part.state = SHA_FIRST;
	sha_sessions[ctx_idx].state.part.hrsm = SHA_HRSM_DISABLE;
	sha_sessions[ctx_idx].algo = algo;

	ctx->hash_hndlr = intel_sha_compute;
	return 0;
}

static int intel_sha_device_free(const struct device *dev, struct hash_ctx *ctx)
{
	struct sha_container *self = (struct sha_container *const)(dev)->data;
	struct sha_session *session = (struct sha_session *)ctx->drv_sessn_state;

	(void)memset((void *)self->dfsha, 0, sizeof(struct sha_hw_regs));
	(void)memset(&session->sha_ctx, 0, sizeof(struct sha_context));
	(void)memset(&session->state, 0, sizeof(union sha_state));
	session->in_use = 0;
	session->algo = 0;
	return 0;
}

static int intel_sha_device_hw_caps(const struct device *dev)
{
	return (CAP_SEPARATE_IO_BUFS | CAP_SYNC_OPS);
}

static struct crypto_driver_api hash_enc_funcs = {
	.hash_begin_session = intel_sha_device_set_hash_type,
	.hash_free_session = intel_sha_device_free,
	.hash_async_callback_set = NULL,
	.query_hw_caps = intel_sha_device_hw_caps,
};

#define INTEL_SHA_DEVICE_INIT(inst)                                               \
static struct sha_container sha_data_##inst  = {                                  \
	.dfsha = (volatile struct sha_hw_regs *)DT_INST_REG_ADDR_BY_IDX(inst, 0)  \
};                                                                                \
DEVICE_DT_INST_DEFINE(inst, NULL, NULL, &sha_data_##inst, NULL,                   \
	POST_KERNEL, CONFIG_CRYPTO_INIT_PRIORITY, (void *)&hash_enc_funcs);

DT_INST_FOREACH_STATUS_OKAY(INTEL_SHA_DEVICE_INIT)
