// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+

#include "Counters.h"
#include "MTGS.h"
#include "SaveState.h"

bool SaveStateBase::InputRecordingFreeze()
{
	// NOTE - BE CAREFUL
	// CHANGING THIS WILL BREAK BACKWARDS COMPATIBILITY ON SAVESTATES
	if (!FreezeTag("InputRecording"))
		return false;

	Freeze(g_FrameCount);
	return IsOkay();
}

#include "InputRecording.h"

#include "InputRecordingControls.h"
#include "Utilities/InputRecordingLogger.h"

#include "common/FileSystem.h"
#include "common/StringUtil.h"
#include "Counters.h"
#include "SaveState.h"
#include "VMManager.h"
#include "Host.h"
#include "ImGui/ImGuiOverlays.h"
#include "DebugTools/Debug.h"
#include "GameDatabase.h"
#include "fmt/format.h"
#include "GS.h"
#include "Host.h"

InputRecording g_InputRecording;

bool InputRecording::create(const std::string& fileName, const bool fromSaveState, const std::string& authorName)
{
	if (!m_file.openNew(fileName, fromSaveState))
	{
		return false;
	}

	m_controls.setRecordMode();
	if (fromSaveState)
	{
		std::string savestatePath = fmt::format("{}_SaveState.p2s", fileName);
		if (FileSystem::FileExists(savestatePath.c_str()))
		{
			FileSystem::CopyFilePath(savestatePath.c_str(), fmt::format("{}.bak", savestatePath).c_str(), true);
		}
		m_type = Type::FROM_SAVESTATE;
		m_is_active = true;
		m_initial_load_complete = true;
		m_watching_for_rerecords = true;
		setStartingFrame(g_FrameCount);
		VMManager::SaveState(savestatePath.c_str(), true, false, [](const std::string& error) {
			SaveState_ReportSaveErrorOSD(error, std::nullopt);
		});
	}
	else
	{
		m_starting_frame = 0;
		m_type = Type::POWER_ON;
		m_initial_load_complete = false;
		m_is_active = true;
		// TODO - should this be an explicit [full] boot instead of a reset?
		VMManager::Reset();
	}

	m_file.setEmulatorVersion();
	m_file.setAuthor(authorName);
	m_file.setGameName(VMManager::GetTitle(false));
	m_file.writeHeader();
	initializeState();
	InputRec::log(TRANSLATE_STR("InputRecording", "Started new input recording"), Host::OSD_INFO_DURATION);
	InputRec::consoleLog(fmt::format("Filename {}", m_file.getFilename()));
	return true;
}

bool InputRecording::play(const std::string& filename)
{
	if (!m_file.openExisting(filename))
	{
		return false;
	}

	// Either load the savestate, or restart the game
	if (m_file.fromSaveState())
	{
		std::string savestatePath = fmt::format("{}_SaveState.p2s", m_file.getFilename());
		if (!FileSystem::FileExists(savestatePath.c_str()))
		{
			InputRec::consoleLog(fmt::format("Could not locate savestate file at location - {}", savestatePath));
			InputRec::log(TRANSLATE_STR("InputRecording", "Failed to load state for input recording"), Host::OSD_ERROR_DURATION);
			m_file.close();
			return false;
		}
		m_type = Type::FROM_SAVESTATE;
		m_initial_load_complete = false;
		m_is_active = true;
		const auto loaded = VMManager::LoadState(savestatePath.c_str());
		if (!loaded)
		{
			InputRec::log(TRANSLATE_STR("InputRecording", "Failed to load state for input recording, unsupported version?"), Host::OSD_ERROR_DURATION);
			m_file.close();
			m_is_active = false;
			return false;
		}
	}
	else
	{
		m_starting_frame = 0;
		m_type = Type::POWER_ON;
		m_initial_load_complete = false;
		m_is_active = true;
		// TODO - should this be an explicit [full] boot instead of a reset?
		VMManager::Reset();
	}
	m_controls.setReplayMode();
	initializeState();
	InputRec::log(TRANSLATE_STR("InputRecording", "Replaying input recording"), Host::OSD_INFO_DURATION);
	m_file.logRecordingMetadata();
	if (VMManager::GetTitle(false) != m_file.getGameName())
	{
		InputRec::consoleLog(fmt::format("Input recording was possibly constructed for a different game. Expected: {}, Actual: {}", m_file.getGameName(), VMManager::GetTitle(false)));
	}
	return true;
}

void InputRecording::closeActiveFile()
{
	if (!m_is_active)
	{
		return;
	}
	if (m_file.close())
	{
		m_is_active = false;
		InputRec::log(TRANSLATE_STR("InputRecording", "Input recording stopped"), Host::OSD_ERROR_DURATION);
		MTGS::PresentCurrentFrame();
	}
	else
	{
		InputRec::log(TRANSLATE_STR("InputRecording", "Unable to stop input recording"), Host::OSD_ERROR_DURATION);
	}
}

void InputRecording::stop()
{
	if (VMManager::GetState() == VMState::Paused)
	{
		closeActiveFile();
	}
	else
	{
		// Don't stop immediately, close the file after the current frame completes
		m_recordingQueue.push([&]() {
			closeActiveFile();
		});
	}
}

void InputRecording::handleControllerDataUpdate()
{
	// TODO - multi-tap support with new file format, for now just controller 0 and 1
	for (int i = 0; i < 2; i++)
	{
		// Fetch the current frame's data
		PadData frameData(i, 0);
		if (m_is_active)
		{
			if (m_controls.isRecording())
			{
				saveControllerData(frameData, i, 0);
			}
			else if (m_controls.isReplaying())
			{
				const auto& modifiedFrameData = updateControllerData(i, 0);
				if (modifiedFrameData)
				{
					frameData = modifiedFrameData.value();
				}
			}
		}
		// Log the data we have gathered, useful for debugging our use-case
		frameData.LogPadData();
	}
}

void InputRecording::saveControllerData(const PadData& data, const int port, const int slot)
{
	// Save the frame's data to the file
	if (!m_file.writePadData(m_frame_counter, data))
	{
		InputRec::consoleLog(fmt::format("Failed to write input data at [{}:{}:{}]", m_frame_counter, port, slot));
	}
}
std::optional<PadData> InputRecording::updateControllerData(const int port, const int slot)
{
	// Get the PadData from the file
	const auto frameData = m_file.readPadData(m_frame_counter, port, slot);
	if (frameData)
	{
		// Update the g_key_status appropriately
		frameData->OverrideActualController();
	}
	else
	{
		InputRec::consoleLog(fmt::format("Failed to read input data at [{}:{}:{}]", m_frame_counter, port, slot));
	}
	return frameData;
}

void InputRecording::processRecordQueue()
{
	while (!m_recordingQueue.empty())
	{
		m_recordingQueue.front()();
		m_recordingQueue.pop();
	}
}

void InputRecording::incFrameCounter()
{
	if (!m_is_active)
	{
		return;
	}

	if (m_frame_counter == std::numeric_limits<u32>::max())
	{
		InputRec::log(TRANSLATE_STR("InputRecording", "Congratulations, you've been playing for far too long and thus have reached the limit of input recording! Stopping recording now..."), Host::OSD_CRITICAL_ERROR_DURATION);
		stop();
		return;
	}
	m_frame_counter++;

	if (m_controls.isReplaying())
	{
		InformGSThread();
		// If we've reached the end of the recording while replaying, pause
		if (m_frame_counter == m_file.getTotalFrames())
		{
			VMManager::SetPaused(true);
			// Can also stop watching for re-records, they've watched to the end of the recording
			m_watching_for_rerecords = false;
		}
	}
	if (m_controls.isRecording())
	{
		m_frame_counter_stateless++;
		m_file.setTotalFrames(m_frame_counter);
		// If we've been in record mode and moved to the next frame, we've overrote something
		// if this was following a save-state loading, this is considered a re-record, a.k.a an undo
		if (m_watching_for_rerecords)
		{
			m_file.incrementUndoCount();
			m_watching_for_rerecords = false;
		}
		InformGSThread();
	}
}

u32 InputRecording::getFrameCounter() const
{
	return m_frame_counter;
}

u32 InputRecording::getFrameCounterStateless() const
{
	return m_frame_counter_stateless;
}

bool InputRecording::isActive() const
{
	return m_is_active;
}

void InputRecording::handleExceededFrameCounter()
{
	// if we go past the end, switch to recording mode so nothing is lost
	if (m_frame_counter >= m_file.getTotalFrames() && m_controls.isReplaying())
	{
		m_controls.setRecordMode(false);
	}
}

void InputRecording::handleReset()
{
	if (m_initial_load_complete)
	{
		adjustFrameCounterOnReRecord(0);
	}
	m_initial_load_complete = true;
}

void InputRecording::handleLoadingSavestate()
{
	// We need to keep track of the starting internal frame of the recording
	// - For a power-on recording this should already be done - it starts at 0
	// - For save state recordings, this is stored inside the initial save-state
	//
	// Why?
	// - When you re-record you load another save-state which has it's own frame counter
	//   stored within, we use this to adjust the frame we are replaying/recording to
	if (isTypeSavestate() && !m_initial_load_complete)
	{
		setStartingFrame(g_FrameCount);
		m_initial_load_complete = true;
	}
	else
	{
		adjustFrameCounterOnReRecord(g_FrameCount);
		m_watching_for_rerecords = true;
	}
}

bool InputRecording::isTypeSavestate() const
{
	return m_type == Type::FROM_SAVESTATE;
}

void InputRecording::setStartingFrame(u32 startingFrame)
{
	if (m_type == Type::POWER_ON)
	{
		return;
	}
	InputRec::consoleLog(fmt::format("Internal Starting Frame: {}", startingFrame));
	m_starting_frame = startingFrame;
	InformGSThread();
}

u32 InputRecording::getStartingFrame()
{
	return m_starting_frame;
}

void InputRecording::adjustFrameCounterOnReRecord(u32 newFrameCounter)
{
	if (newFrameCounter > m_starting_frame + m_file.getTotalFrames())
	{
		InputRec::consoleLog("Warning, you've loaded PCSX2 emulation to a point after the end of the original recording. This should be avoided.");
		InputRec::consoleLog("Save state's framecount has been ignored, using the max length of the recording instead.");
		m_frame_counter = m_file.getTotalFrames();
		if (getControls().isReplaying())
		{
			getControls().setRecordMode();
		}
		return;
	}
	if (newFrameCounter < m_starting_frame)
	{
		InputRec::consoleLog("Warning, you've loaded PCSX2 emulation to a point before the start of the original recording. This should be avoided.");
		InputRec::consoleLog("Save state's framecount has been ignored, starting from the beginning in replay mode.");
		m_frame_counter = 0;
		if (getControls().isRecording())
		{
			getControls().setReplayMode();
		}
		return;
	}
	else if (newFrameCounter == 0 && getControls().isRecording())
	{
		getControls().setReplayMode();
	}
	m_frame_counter = newFrameCounter - m_starting_frame;
	m_frame_counter_stateless--;
	m_file.setTotalFrames(m_frame_counter);
	InformGSThread();
}

InputRecordingControls& InputRecording::getControls()
{
	return m_controls;
}

const InputRecordingFile& InputRecording::getData() const
{
	return m_file;
}

void InputRecording::initializeState()
{
	m_frame_counter = 0;
	m_watching_for_rerecords = false;
	InformGSThread();
}

void InputRecording::InformGSThread()
{
	TinyString recording_active_message = TinyString::from_format(TRANSLATE_FS("InputRecording", "Input Recording Active: {}"), g_InputRecording.getData().getFilename());
	TinyString frame_data_message = TinyString::from_format(TRANSLATE_FS("InputRecording", "Frame: {}/{} ({})"), g_InputRecording.getFrameCounter(), g_InputRecording.getData().getTotalFrames(), g_InputRecording.getFrameCounterStateless());
	TinyString undo_count_message = TinyString::from_format(TRANSLATE_FS("InputRecording", "Undo Count: {}"), g_InputRecording.getData().getUndoCount());

	MTGS::RunOnGSThread([recording_active_message, frame_data_message, undo_count_message](bool is_recording = g_InputRecording.getControls().isRecording()) {
		g_InputRecordingData.is_recording = is_recording;
		g_InputRecordingData.recording_active_message = recording_active_message;
		g_InputRecordingData.frame_data_message = frame_data_message;
		g_InputRecordingData.undo_count_message = undo_count_message;
	});
}
