// Copyright (c) 2024- PPSSPP Project.

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 2.0 or later versions.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License 2.0 for more details.

// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/

// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.

#include "Core/MIPS/MIPSTracer.h"

#include <cstring> // for std::memcpy
#include "Core/MIPS/MIPSTables.h" // for MIPSDisAsm
#include "Core/MemMap.h" // for Memory::GetPointerUnchecked
#include "Common/File/FileUtil.h" // for the File::OpenCFile


bool TraceBlockStorage::save_block(const u32* instructions, u32 size) {
	// 'size' is measured in bytes
	const auto indexes_count = size / 4;

	if (cur_index + 1 + indexes_count >= raw_instructions.size()) {
		return false;
	}

	// Save the size first
	*cur_data_ptr = size;
	++cur_data_ptr;

	// Now save the MIPS instructions
	std::memcpy(cur_data_ptr, instructions, size);
	cur_data_ptr += indexes_count;

	cur_index += 1 + indexes_count;
	return true;
}

void TraceBlockStorage::initialize(u32 capacity) {
	raw_instructions.resize(capacity);
	cur_index = 0;
	cur_data_ptr = raw_instructions.data();
	INFO_LOG(Log::JIT, "TraceBlockStorage initialized: capacity=0x%x", capacity);
}

void TraceBlockStorage::clear() {
	raw_instructions.clear();
	cur_index = 0;
	cur_data_ptr = nullptr;
	INFO_LOG(Log::JIT, "TraceBlockStorage cleared");
}

void MIPSTracer::prepare_block(const MIPSComp::IRBlock* block, MIPSComp::IRBlockCache& blocks) {
	u32 virt_addr, size;
	block->GetRange(&virt_addr, &size);

	u64 hash = block->GetHash();
	auto it = hash_to_storage_index.find(hash);

	u32 storage_index;
	if (it != hash_to_storage_index.end()) {
		// We've seen this one before => it's saved in our storage
		storage_index = it->second;
	}
	else {
		// We haven't seen a block like that before, let's save it
		auto mips_instructions_ptr = (const u32*)Memory::GetPointerUnchecked(virt_addr);

		storage_index = storage.cur_index;
		if (!storage.save_block(mips_instructions_ptr, size)) {
			// We ran out of storage!
			WARN_LOG(Log::JIT, "The MIPSTracer ran out of storage for the blocks, cannot proceed!");
			stop_tracing();
			return;
		}
		// Successfully inserted the block at index 'storage_index'!

		hash_to_storage_index.emplace(hash, storage_index);
	}

	// NB!
	// If for some reason the blocks get invalidated while tracing, PPSSPP will be forced to recompile
	// the same code again => the 'trace_info' will be filled with duplicates, because we can't detect that...
	// If we store the TraceBlockInfo instances in an unordered_map, we won't be able to reference the entries
	// by using the 4 byte IRInst field 'constant' (the iterators won't fit there).
	// And, of course, doing a linear search in the vector is not worth the conserved space.
	trace_info.push_back({ virt_addr, storage_index });


	u32 index = (u32)(trace_info.size() - 1);
	auto ir_ptr = (IRInst*)blocks.GetBlockInstructionPtr(*block);
	ir_ptr[1].constant = index;
}

bool MIPSTracer::flush_to_file() {
	if (logging_path.empty()) {
		WARN_LOG(Log::JIT, "The path is empty, cannot flush the trace!");
		return false;
	}

	INFO_LOG(Log::JIT, "Flushing the trace to a file...");
	output = File::OpenCFile(logging_path, "w");
	
	if (!output) {
		WARN_LOG(Log::JIT, "MIPSTracer failed to open the file '%s'", logging_path.c_str());
		return false;
	}
	auto trace = executed_blocks.get_content();
	for (auto index : trace) {
		auto& block_info = trace_info[index];
		flush_block_to_file(block_info);
	}

	INFO_LOG(Log::JIT, "Trace flushed, closing the file...");
	fclose(output);

	clear();
	return true;
}

void MIPSTracer::flush_block_to_file(const TraceBlockInfo& block_info) {
	char buffer[512];

	// The log format is '{prefix}{disassembled line}', where 'prefix' is '0x{8 hex digits of the address}: '
	const auto prefix_size = 2 + 8 + 2;

	u32 addr = block_info.virt_address;
	u32 index = block_info.storage_index;

	u32 size = storage[index];
	++index;

	u32 end_addr = addr + size;
	

	for (; addr < end_addr; addr += 4, ++index) {
		snprintf(buffer, sizeof(buffer), "0x%08x: ", addr);
		MIPSDisAsm(storage.read_asm(index), addr, buffer + prefix_size, sizeof(buffer) - prefix_size, true);

		fprintf(output, "%s\n", buffer);
	}
}

void MIPSTracer::start_tracing() {
	if (!tracing_enabled) {
		INFO_LOG(Log::JIT, "MIPSTracer enabled");
		tracing_enabled = true;
	}
}

void MIPSTracer::stop_tracing() {
	if (tracing_enabled) {
		INFO_LOG(Log::JIT, "MIPSTracer disabled");
		tracing_enabled = false;

#ifdef _DEBUG
		print_stats();
#endif
	}
}

inline void MIPSTracer::print_stats() const {
	// First, the storage
	INFO_LOG(Log::JIT, "=============== MIPSTracer storage ===============");
	INFO_LOG(Log::JIT, "Current index = %d, storage size = %d", storage.cur_index, (int)storage.raw_instructions.size());

	// Then the cyclic buffer
	if (executed_blocks.overflow) {
		INFO_LOG(Log::JIT, "=============== MIPSTracer cyclic buffer (overflow) ===============");
		INFO_LOG(Log::JIT, "Trace size = %d, starts from index %d", (int)executed_blocks.buffer.size(), executed_blocks.current_index);
	}
	else {
		INFO_LOG(Log::JIT, "=============== MIPSTracer cyclic buffer (no overflow) ===============");
		INFO_LOG(Log::JIT, "Trace size = %d, starts from index 0", executed_blocks.current_index);
	}
	// Next, the hash-to-index mapping
	INFO_LOG(Log::JIT, "=============== MIPSTracer hashes ===============");
	INFO_LOG(Log::JIT, "Number of unique hashes = %d", (int)hash_to_storage_index.size());

	// Finally, the basic block list
	INFO_LOG(Log::JIT, "=============== MIPSTracer basic block list ===============");
	INFO_LOG(Log::JIT, "Number of processed basic blocks = %d", (int)trace_info.size());

	INFO_LOG(Log::JIT, "=============== MIPSTracer stats end ===============");
}

void MIPSTracer::initialize(u32 storage_capacity, u32 max_trace_size) {
	executed_blocks.resize(max_trace_size);
	hash_to_storage_index.reserve(max_trace_size);
	storage.initialize(storage_capacity);
	trace_info.reserve(max_trace_size);
	INFO_LOG(Log::JIT, "MIPSTracer initialized: storage_capacity=0x%x, max_trace_size=%d", storage_capacity, max_trace_size);
}

void MIPSTracer::clear() {
	executed_blocks.clear();
	hash_to_storage_index.clear();
	storage.clear();
	trace_info.clear();
	INFO_LOG(Log::JIT, "MIPSTracer cleared");
}

MIPSTracer mipsTracer;


