// Copyright (c) 2021- 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 <algorithm>
#include "Core/MIPS/MIPS.h"
#include "Core/MIPS/MIPSDebugInterface.h"
#include "Core/Debugger/MemBlockInfo.h"
#include "Core/Debugger/WebSocket/MemoryInfoSubscriber.h"
#include "Core/Debugger/WebSocket/WebSocketUtils.h"
#include "Core/MemMap.h"

class WebSocketMemoryInfoState : public DebuggerSubscriber {
public:
	WebSocketMemoryInfoState() {
	}
	~WebSocketMemoryInfoState() {
		UpdateOverride(false);
	}

	void Mapping(DebuggerRequest &req);
	void Config(DebuggerRequest &req);
	void Set(DebuggerRequest &req);
	void List(DebuggerRequest &req);
	void Search(DebuggerRequest &req);

protected:
	void UpdateOverride(bool flag);

	bool detailOverride_ = false;
};

DebuggerSubscriber *WebSocketMemoryInfoInit(DebuggerEventHandlerMap &map) {
	auto p = new WebSocketMemoryInfoState();
	map["memory.mapping"] = std::bind(&WebSocketMemoryInfoState::Mapping, p, std::placeholders::_1);
	map["memory.info.config"] = std::bind(&WebSocketMemoryInfoState::Config, p, std::placeholders::_1);
	map["memory.info.set"] = std::bind(&WebSocketMemoryInfoState::Set, p, std::placeholders::_1);
	map["memory.info.list"] = std::bind(&WebSocketMemoryInfoState::List, p, std::placeholders::_1);
	map["memory.info.search"] = std::bind(&WebSocketMemoryInfoState::Search, p, std::placeholders::_1);

	return p;
}

void WebSocketMemoryInfoState::UpdateOverride(bool flag) {
	if (detailOverride_ && !flag)
		MemBlockReleaseDetailed();
	if (!detailOverride_ && flag)
		MemBlockOverrideDetailed();
	detailOverride_ = flag;
}

// List memory map data (memory.mapping)
//
// No parameters.
//
// Response (same event name):
//  - ranges: array of objects:
//     - type: one of "ram", "vram", "sram".
//     - subtype: "primary" or "mirror".
//     - name: string, friendly name.
//     - address: number, start address of range.
//     - size: number, in bytes.
//
// Note: Even if you set false, may stay enabled if set by user or another debug session.
void WebSocketMemoryInfoState::Mapping(DebuggerRequest &req) {
	struct MemRange {
		const char *type;
		const char *subtype;
		const char *name;
		const uint32_t address;
		const uint32_t size;
	};
	constexpr uint32_t kernelMemorySize = PSP_GetKernelMemoryEnd() - PSP_GetKernelMemoryBase();
	constexpr uint32_t volatileMemorySize = PSP_GetVolatileMemoryEnd() - PSP_GetVolatileMemoryStart();
	static constexpr MemRange ranges[] = {
		{ "sram", "primary", "Scratchpad", PSP_GetScratchpadMemoryBase(), Memory::SCRATCHPAD_SIZE },
		{ "vram", "primary", "VRAM", PSP_GetVidMemBase(), Memory::VRAM_SIZE },
		{ "vram", "mirror", "VRAM (Swizzled)", PSP_GetVidMemBase() + Memory::VRAM_SIZE * 1, Memory::VRAM_SIZE },
		{ "vram", "mirror", "VRAM (Mirror)", PSP_GetVidMemBase() + Memory::VRAM_SIZE * 2, Memory::VRAM_SIZE },
		{ "vram", "mirror", "VRAM (Swizzled + Interleaved)", PSP_GetVidMemBase() + Memory::VRAM_SIZE * 3, Memory::VRAM_SIZE },
		{ "ram", "primary", "Kernel Memory", 0x80000000 | PSP_GetKernelMemoryBase(), kernelMemorySize },
		{ "ram", "primary", "Volatile Memory", PSP_GetVolatileMemoryStart(), volatileMemorySize },
		// Size is specially calculated.
		{ "ram", "primary", "User Memory", PSP_GetUserMemoryBase(), 0 },
	};

	JsonWriter &json = req.Respond();
	json.pushArray("ranges");
	for (auto range : ranges) {
		uint32_t size = range.size;
		if (size == 0) {
			size = Memory::g_MemorySize;
			if (size == 0) {
				size = Memory::RAM_NORMAL_SIZE;
			}
			size -= kernelMemorySize + volatileMemorySize;
		}
		json.pushDict();
		json.writeString("type", range.type);
		json.writeString("subtype", range.subtype);
		json.writeString("name", range.name);
		json.writeUint("address", range.address);
		json.writeUint("size", size);
		json.pop();

		// Also write the uncached range.
		json.pushDict();
		json.writeString("type", range.type);
		json.writeString("subtype", "mirror");
		json.writeString("name", std::string("Uncached ") + range.name);
		json.writeUint("address", 0x40000000 | range.address);
		json.writeUint("size", size);
		json.pop();
	}
	json.pop();
}

// Update memory info tracking config (memory.info.config)
//
// Parameters:
//  - detailed: optional, boolean to force enable detailed tracking (perf impact.)
//
// Response (same event name):
//  - detailed: boolean state of tracking before any changes.
//
// Note: Even if you set false, may stay enabled if set by user or another debug session.
void WebSocketMemoryInfoState::Config(DebuggerRequest &req) {
	bool setDetailed = req.HasParam("detailed");
	bool detailed = false;
	if (!req.ParamBool("detailed", &detailed, DebuggerParamType::OPTIONAL))
		return;

	JsonWriter &json = req.Respond();
	json.writeBool("detailed", MemBlockInfoDetailed());

	if (setDetailed)
		UpdateOverride(detailed);
}

static MemBlockFlags FlagFromType(const std::string &type) {
	if (type == "write")
		return MemBlockFlags::WRITE;
	if (type == "texture")
		return MemBlockFlags::TEXTURE;
	if (type == "alloc")
		return MemBlockFlags::ALLOC;
	if (type == "suballoc")
		return MemBlockFlags::SUB_ALLOC;
	if (type == "free")
		return MemBlockFlags::FREE;
	if (type == "subfree")
		return MemBlockFlags::SUB_FREE;
	return MemBlockFlags::SKIP_MEMCHECK;
}

static std::string TypeFromFlag(const MemBlockFlags &flag) {
	if (flag & MemBlockFlags::WRITE)
		return "write";
	else if (flag & MemBlockFlags::TEXTURE)
		return "texture";
	else if (flag & MemBlockFlags::ALLOC)
		return "alloc";
	else if (flag & MemBlockFlags::SUB_ALLOC)
		return "suballoc";
	return "error";
}

// Update memory info tagging (memory.info.set)
//
// Parameters:
//  - address: number representing start address of the range to modify.
//  - size: number, bytes from start address.
//  - type: string, one of:
//     - "write" for last modification information.
//     - "texture" for last texture usage information.
//     - "alloc" for allocation information.
//     - "suballoc" for allocations within an existing allocation.
//     - "free" to mark a previous allocation and its suballocations freed (ignores tag.)
//     - "subfree" to mark a previous suballocation freed (ignores tag.)
//  - tag: string label to give the memory.  Optional if type if free or subfree.
//  - pc: optional, number indicating PC address for this tag.
//
// Response (same event name) with no extra data.
//
// Note: Only one tag per type is maintained for any given memory address.
// Small extent info may be ignored unless detailed tracking enabled (see memory.info.config.)
void WebSocketMemoryInfoState::Set(DebuggerRequest &req) {
	if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
		return req.Fail("CPU not started");

	std::string type;
	if (!req.ParamString("type", &type))
		return;
	std::string tag;
	if (type != "free" && type != "subfree") {
		if (!req.ParamString("tag", &tag))
			return;
	}
	uint32_t addr;
	if (!req.ParamU32("address", &addr))
		return;
	uint32_t size;
	if (!req.ParamU32("size", &size))
		return;
	uint32_t pc = currentMIPS->pc;
	if (!req.ParamU32("pc", &pc, false, DebuggerParamType::OPTIONAL))
		return;

	MemBlockFlags flags = MemBlockFlags::SKIP_MEMCHECK | FlagFromType(type);
	if (flags == MemBlockFlags::SKIP_MEMCHECK)
		return req.Fail("Invaid type - expecting write, texture, alloc, suballoc, free, or subfree");

	if (!Memory::IsValidAddress(addr))
		return req.Fail("Invalid address");
	else if (!Memory::IsValidRange(addr, size))
		return req.Fail("Invalid size");

	NotifyMemInfoPC(flags, addr, size, pc, tag.c_str(), tag.size());
	req.Respond();
}

// List memory info tags for address range (memory.info.list)
//
// Parameters:
//  - address: number representing start address of the range.
//  - size: number, bytes from start address.
//  - type: optional string to limit information to one of:
//     - "write" for last modification information.
//     - "texture" for last texture usage information.
//     - "alloc" for allocation information.
//     - "suballoc" for allocations within an existing allocation.
//
// Response (same event name):
//  - extents: array of objects:
//     - type: one of the above type string values.
//     - address: number (may be outside requested range if overlapping.)
//     - size: number (may be outside requested range if overlapping.)
//     - ticks: number indicating tick counter as of last tag.
//     - pc: number address of last tag.
//     - tag: string tag for this memory extent.
//     - allocated: boolean, if this extent is marked as allocated (for alloc/suballoc types.)
void WebSocketMemoryInfoState::List(DebuggerRequest &req) {
	if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
		return req.Fail("CPU not started");

	std::string type;
	if (!req.ParamString("type", &type, DebuggerParamType::OPTIONAL))
		return;
	uint32_t addr;
	if (!req.ParamU32("address", &addr))
		return;
	uint32_t size;
	if (!req.ParamU32("size", &size))
		return;

	// Allow type to be omitted.
	MemBlockFlags flags = MemBlockFlags::SKIP_MEMCHECK | FlagFromType(type);
	if (flags == MemBlockFlags::SKIP_MEMCHECK && req.HasParam("type"))
		return req.Fail("Invaid type - expecting write, texture, alloc, suballoc, free, or subfree");

	if (!Memory::IsValidAddress(addr))
		return req.Fail("Invalid address");
	else if (!Memory::IsValidRange(addr, size))
		return req.Fail("Invalid size");

	std::vector<MemBlockInfo> results;
	if (flags == MemBlockFlags::SKIP_MEMCHECK)
		results = FindMemInfo(addr, size);
	else
		results = FindMemInfoByFlag(flags, addr, size);

	JsonWriter &json = req.Respond();
	json.pushArray("extents");
	for (const auto &result : results) {
		json.pushDict();
		json.writeString("type", TypeFromFlag(result.flags));
		json.writeUint("address", result.start);
		json.writeUint("size", result.size);
		json.writeFloat("ticks", result.ticks);
		json.writeUint("pc", result.pc);
		json.writeString("tag", result.tag);
		json.writeBool("allocated", result.allocated);
		json.pop();
	}
	json.pop();
}

// Search memory info tags for a string (memory.info.search)
//
// Parameters:
//  - address: optional number representing start address of the range.
//  - end: optional end address as a number (otherwise uses start address.)
//  - match: string to search for within tag.
//  - type: optional string to limit information to one of:
//     - "write" for last modification information.
//     - "texture" for last texture usage information.
//     - "alloc" for allocation information.
//     - "suballoc" for allocations within an existing allocation.
//
// Response (same event name):
//  - extent: null, or matching object containing:
//     - type: one of the above type string values.
//     - address: number (may be outside requested range if overlapping.)
//     - size: number (may be outside requested range if overlapping.)
//     - ticks: number indicating tick counter as of last tag.
//     - pc: number address of last tag.
//     - tag: string tag for this memory extent.
//     - allocated: boolean, if this extent is marked as allocated (for alloc/suballoc types.)
//
// Note: may not be fast.
void WebSocketMemoryInfoState::Search(DebuggerRequest &req) {
	if (!currentDebugMIPS->isAlive() || !Memory::IsActive())
		return req.Fail("CPU not started");

	uint32_t start = 0;
	if (!req.ParamU32("address", &start, false, DebuggerParamType::OPTIONAL))
		return;
	uint32_t end = start;
	if (!req.ParamU32("end", &end, false, DebuggerParamType::OPTIONAL))
		return;
	std::string type;
	if (!req.ParamString("type", &type, DebuggerParamType::OPTIONAL))
		return;
	std::string match;
	if (!req.ParamString("match", &match))
		return;

	// Allow type to be omitted.
	MemBlockFlags flags = MemBlockFlags::SKIP_MEMCHECK | FlagFromType(type);
	if (flags == MemBlockFlags::SKIP_MEMCHECK && req.HasParam("type"))
		return req.Fail("Invaid type - expecting write, texture, alloc, suballoc, free, or subfree");

	start = RoundMemAddressUp(start);
	end = RoundMemAddressUp(end);
	std::transform(match.begin(), match.end(), match.begin(), ::tolower);

	bool found = false;
	MemBlockInfo foundResult;

	uint32_t addr = start;
	constexpr uint32_t CHUNK_SIZE = 0x1000;
	do {
		uint32_t chunk_end = addr + CHUNK_SIZE;
		if (addr < end && chunk_end >= end) {
			chunk_end = end;
		}

		std::vector<MemBlockInfo> results;
		if (flags == MemBlockFlags::SKIP_MEMCHECK)
			results = FindMemInfo(addr, chunk_end - addr);
		else
			results = FindMemInfoByFlag(flags, addr, chunk_end - addr);

		for (const auto &result : results) {
			std::string lowercase = result.tag;
			std::transform(lowercase.begin(), lowercase.end(), lowercase.begin(), ::tolower);

			if (lowercase.find(match) != lowercase.npos) {
				found = true;
				foundResult = result;
				break;
			}
		}
		addr = RoundMemAddressUp(chunk_end);
	} while (!found && addr != end);

	JsonWriter &json = req.Respond();
	if (found) {
		json.pushDict("extent");
		json.writeString("type", TypeFromFlag(foundResult.flags));
		json.writeUint("address", foundResult.start);
		json.writeUint("size", foundResult.size);
		json.writeFloat("ticks", foundResult.ticks);
		json.writeUint("pc", foundResult.pc);
		json.writeString("tag", foundResult.tag);
		json.writeBool("allocated", foundResult.allocated);
		json.pop();
	} else {
		json.writeNull("extent");
	}
}