/*
 * Copyright 2019-2022 Arm Limited and/or its affiliates <open-source-office@arm.com>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include "inference_process.hpp"

#include <tensorflow/lite/micro/all_ops_resolver.h>
#include <tensorflow/lite/micro/cortex_m_generic/debug_log_callback.h>
#include <tensorflow/lite/micro/micro_error_reporter.h>
#include <tensorflow/lite/micro/micro_interpreter.h>
#include <tensorflow/lite/micro/micro_profiler.h>
#include <tensorflow/lite/schema/schema_generated.h>

#include <cmsis_compiler.h>
#include <inttypes.h>
#include <zephyr/kernel.h>

using namespace std;

namespace
{
bool copyOutput(const TfLiteTensor &src, InferenceProcess::DataPtr &dst)
{
	if (dst.data == nullptr) {
		return false;
	}

	if (src.bytes > dst.size) {
		printk("Tensor size mismatch (bytes): actual=%d, expected%d.\n", src.bytes,
		       dst.size);
		return true;
	}

	copy(src.data.uint8, src.data.uint8 + src.bytes, static_cast<uint8_t *>(dst.data));
	dst.size = src.bytes;

	return false;
}

} /* namespace */

namespace InferenceProcess
{
DataPtr::DataPtr(void *_data, size_t _size) : data(_data), size(_size)
{
}

void DataPtr::invalidate()
{
#if defined(__DCACHE_PRESENT) && (__DCACHE_PRESENT == 1U)
	SCB_InvalidateDCache_by_Addr(reinterpret_cast<uint32_t *>(data), size);
#endif
}

void DataPtr::clean()
{
#if defined(__DCACHE_PRESENT) && (__DCACHE_PRESENT == 1U)
	SCB_CleanDCache_by_Addr(reinterpret_cast<uint32_t *>(data), size);
#endif
}

InferenceJob::InferenceJob()
{
}

InferenceJob::InferenceJob(const string &_name, const DataPtr &_networkModel,
			   const vector<DataPtr> &_input, const vector<DataPtr> &_output,
			   const vector<DataPtr> &_expectedOutput)
	: name(_name), networkModel(_networkModel), input(_input), output(_output),
	  expectedOutput(_expectedOutput)
{
}

void InferenceJob::invalidate()
{
	networkModel.invalidate();

	for (auto &it : input) {
		it.invalidate();
	}

	for (auto &it : output) {
		it.invalidate();
	}

	for (auto &it : expectedOutput) {
		it.invalidate();
	}
}

void InferenceJob::clean()
{
	networkModel.clean();

	for (auto &it : input) {
		it.clean();
	}

	for (auto &it : output) {
		it.clean();
	}

	for (auto &it : expectedOutput) {
		it.clean();
	}
}

bool InferenceProcess::runJob(InferenceJob &job)
{
	/* Get model handle and verify that the version is correct */
	const tflite::Model *model = ::tflite::GetModel(job.networkModel.data);
	if (model->version() != TFLITE_SCHEMA_VERSION) {
		printk("Model schema version unsupported: version=%" PRIu32 ", supported=%d.\n",
		       model->version(), TFLITE_SCHEMA_VERSION);
		return true;
	}

	/* Create the TFL micro interpreter */
	tflite::AllOpsResolver resolver;
	tflite::MicroErrorReporter errorReporter;

	tflite::MicroInterpreter interpreter(model, resolver, tensorArena, tensorArenaSize,
					     &errorReporter);

	/* Allocate tensors */
	TfLiteStatus allocate_status = interpreter.AllocateTensors();
	if (allocate_status != kTfLiteOk) {
		printk("Failed to allocate tensors for inference. job=%p\n", &job);
		return true;
	}

	if (job.input.size() != interpreter.inputs_size()) {
		printk("Number of job and network inputs do not match. input=%zu, network=%zu\n",
		       job.input.size(), interpreter.inputs_size());
		return true;
	}

	/* Copy input data */
	for (size_t i = 0; i < interpreter.inputs_size(); ++i) {
		const DataPtr &input = job.input[i];
		const TfLiteTensor *tensor = interpreter.input(i);

		if (input.size != tensor->bytes) {
			printk("Input tensor size mismatch. index=%zu, input=%zu, network=%u\n", i,
			       input.size, tensor->bytes);
			return true;
		}

		copy(static_cast<char *>(input.data), static_cast<char *>(input.data) + input.size,
		     tensor->data.uint8);
	}

	/* Run the inference */
	TfLiteStatus invoke_status = interpreter.Invoke();
	if (invoke_status != kTfLiteOk) {
		printk("Invoke failed for inference. job=%s\n", job.name.c_str());
		return true;
	}

	/* Copy output data */
	if (job.output.size() > 0) {
		if (interpreter.outputs_size() != job.output.size()) {
			printk("Number of job and network outputs do not match. job=%zu, network=%u\n",
			       job.output.size(), interpreter.outputs_size());
			return true;
		}

		for (unsigned i = 0; i < interpreter.outputs_size(); ++i) {
			if (copyOutput(*interpreter.output(i), job.output[i])) {
				return true;
			}
		}
	}

	if (job.expectedOutput.size() > 0) {
		if (job.expectedOutput.size() != interpreter.outputs_size()) {
			printk("Number of job and network expected outputs do not match. job=%zu, network=%zu\n",
			       job.expectedOutput.size(), interpreter.outputs_size());
			return true;
		}

		for (unsigned int i = 0; i < interpreter.outputs_size(); i++) {
			const DataPtr &expected = job.expectedOutput[i];
			const TfLiteTensor *output = interpreter.output(i);

			if (expected.size != output->bytes) {
				printk("Expected output tensor size mismatch. index=%u, expected=%zu, network=%zu\n",
				       i, expected.size, output->bytes);
				return true;
			}

			for (unsigned int j = 0; j < output->bytes; ++j) {
				if (output->data.uint8[j] !=
				    static_cast<uint8_t *>(expected.data)[j]) {
					printk("Expected output tensor data mismatch. index=%u, offset=%u, expected=%02x, network=%02x\n",
					       i, j, static_cast<uint8_t *>(expected.data)[j],
					       output->data.uint8[j]);
					return true;
				}
			}
		}
	}

	return false;
}

} /* namespace InferenceProcess */
