// Copyright (c) 2018- 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 "Common/StringUtils.h"
#include "Core/Debugger/Breakpoints.h"
#include "Core/Debugger/DisassemblyManager.h"
#include "Core/Debugger/SymbolMap.h"
#include "Core/Debugger/WebSocket/BreakpointSubscriber.h"
#include "Core/Debugger/WebSocket/WebSocketUtils.h"
#include "Core/MIPS/MIPSDebugInterface.h"

DebuggerSubscriber *WebSocketBreakpointInit(DebuggerEventHandlerMap &map) {
	// No need to bind or alloc state, these are all global.
	map["cpu.breakpoint.add"] = &WebSocketCPUBreakpointAdd;
	map["cpu.breakpoint.update"] = &WebSocketCPUBreakpointUpdate;
	map["cpu.breakpoint.remove"] = &WebSocketCPUBreakpointRemove;
	map["cpu.breakpoint.list"] = &WebSocketCPUBreakpointList;

	map["memory.breakpoint.add"] = &WebSocketMemoryBreakpointAdd;
	map["memory.breakpoint.update"] = &WebSocketMemoryBreakpointUpdate;
	map["memory.breakpoint.remove"] = &WebSocketMemoryBreakpointRemove;
	map["memory.breakpoint.list"] = &WebSocketMemoryBreakpointList;

	return nullptr;
}

struct WebSocketCPUBreakpointParams {
	uint32_t address = 0;
	bool hasEnabled = false;
	bool hasLog = false;
	bool hasCondition = false;
	bool hasLogFormat = false;

	bool enabled;
	bool log;
	std::string condition;
	PostfixExpression compiledCondition;
	std::string logFormat;

	bool Parse(DebuggerRequest &req) {
		if (!currentDebugMIPS->isAlive()) {
			req.Fail("CPU not started");
			return false;
		}

		if (!req.ParamU32("address", &address))
			return false;

		hasEnabled = req.HasParam("enabled");
		if (hasEnabled) {
			if (!req.ParamBool("enabled", &enabled))
				return false;
		}
		hasLog = req.HasParam("log");
		if (hasLog) {
			if (!req.ParamBool("log", &log))
				return false;
		}
		hasCondition = req.HasParam("condition");
		if (hasCondition) {
			if (!req.ParamString("condition", &condition))
				return false;
			if (!initExpression(currentDebugMIPS, condition.c_str(), compiledCondition)) {
				req.Fail(StringFromFormat("Could not parse expression syntax: %s", getExpressionError()));
				return false;
			}
		}
		hasLogFormat = req.HasParam("logFormat");
		if (hasLogFormat) {
			if (!req.ParamString("logFormat", &logFormat))
				return false;
		}

		return true;
	}

	void Apply() {
		if (hasCondition && !condition.empty()) {
			BreakPointCond cond;
			cond.debug = currentDebugMIPS;
			cond.expressionString = condition;
			cond.expression = compiledCondition;
			g_breakpoints.ChangeBreakPointAddCond(address, cond);
		} else if (hasCondition && condition.empty()) {
			g_breakpoints.ChangeBreakPointRemoveCond(address);
		}

		if (hasLogFormat) {
			g_breakpoints.ChangeBreakPointLogFormat(address, logFormat);
		}

		// TODO: Fix this interface.
		if (hasLog && !hasEnabled) {
			g_breakpoints.IsAddressBreakPoint(address, &enabled);
			hasEnabled = true;
		}
		if (hasLog && hasEnabled) {
			BreakAction result = BREAK_ACTION_IGNORE;
			if (log)
				result |= BREAK_ACTION_LOG;
			if (enabled)
				result |= BREAK_ACTION_PAUSE;
			g_breakpoints.ChangeBreakPoint(address, result);
		} else if (hasEnabled) {
			g_breakpoints.ChangeBreakPoint(address, enabled);
		}
	}
};

// Add a new CPU instruction breakpoint (cpu.breakpoint.add)
//
// Parameters:
//  - address: unsigned integer address of instruction to break at.
//  - enabled: optional boolean, whether to actually enter stepping when this breakpoint trips.
//  - log: optional boolean, whether to log when this breakpoint trips.
//  - condition: optional string expression to evaluate - breakpoint does not trip if false.
//  - logFormat: optional string to log when breakpoint trips, may include {expression} parts.
//
// Response (same event name) with no extra data.
//
// Note: will replace any breakpoint at the same address.
void WebSocketCPUBreakpointAdd(DebuggerRequest &req) {
	WebSocketCPUBreakpointParams params;
	if (!params.Parse(req))
		return;

	g_breakpoints.AddBreakPoint(params.address);
	params.Apply();
	req.Respond();
}

// Update a CPU instruction breakpoint (cpu.breakpoint.update)
//
// Parameters:
//  - address: unsigned integer address of instruction to break at.
//  - enabled: optional boolean, whether to actually enter stepping when this breakpoint trips.
//  - log: optional boolean, whether to log when this breakpoint trips.
//  - condition: optional string expression to evaluate - breakpoint does not trip if false.
//  - logFormat: optional string to log when breakpoint trips, may include {expression} parts.
//
// Response (same event name) with no extra data.
void WebSocketCPUBreakpointUpdate(DebuggerRequest &req) {
	WebSocketCPUBreakpointParams params;
	if (!params.Parse(req))
		return;
	bool enabled;
	if (!g_breakpoints.IsAddressBreakPoint(params.address, &enabled))
		return req.Fail("Breakpoint not found");

	params.Apply();
	req.Respond();
}

// Remove a CPU instruction breakpoint (cpu.breakpoint.remove)
//
// Parameters:
//  - address: unsigned integer address of instruction to break at.
//
// Response (same event name) with no extra data.
void WebSocketCPUBreakpointRemove(DebuggerRequest &req) {
	if (!currentDebugMIPS->isAlive()) {
		return req.Fail("CPU not started");
	}

	uint32_t address;
	if (!req.ParamU32("address", &address))
		return;

	g_breakpoints.RemoveBreakPoint(address);
	req.Respond();
}

// List all CPU instruction breakpoints (cpu.breakpoint.list)
//
// No parameters.
//
// Response (same event name):
//  - breakpoints: array of objects, each with properties:
//     - address: unsigned integer address of instruction to break at.
//     - enabled: boolean, whether to actually enter stepping when this breakpoint trips.
//     - log: optional boolean, whether to log when this breakpoint trips.
//     - condition: null, or string expression to evaluate - breakpoint does not trip if false.
//     - logFormat: null, or string to log when breakpoint trips, may include {expression} parts.
//     - symbol: null, or string label or symbol at breakpoint address.
//     - code: string disassembly of breakpoint address.
void WebSocketCPUBreakpointList(DebuggerRequest &req) {
	if (!currentDebugMIPS->isAlive()) {
		return req.Fail("CPU not started");
	}

	JsonWriter &json = req.Respond();
	json.pushArray("breakpoints");
	auto bps = g_breakpoints.GetBreakpoints();
	for (const auto &bp : bps) {
		if (bp.temporary)
			continue;

		json.pushDict();
		json.writeUint("address", bp.addr);
		json.writeBool("enabled", bp.IsEnabled());
		json.writeBool("log", (bp.result & BREAK_ACTION_LOG) != 0);
		if (bp.hasCond)
			json.writeString("condition", bp.cond.expressionString);
		else
			json.writeNull("condition");
		if (!bp.logFormat.empty())
			json.writeString("logFormat", bp.logFormat);
		else
			json.writeNull("logFormat");
		std::string symbol = g_symbolMap->GetLabelString(bp.addr);
		if (symbol.empty())
			json.writeNull("symbol");
		else
			json.writeString("symbol", symbol);

		DisassemblyLineInfo line;
		g_disassemblyManager.getLine(g_disassemblyManager.getStartAddress(bp.addr), true, line, currentDebugMIPS);
		json.writeString("code", line.name + " " + line.params);

		json.pop();
	}
	json.pop();
}

struct WebSocketMemoryBreakpointParams {
	uint32_t address = 0;
	uint32_t end = 0;
	bool hasEnabled = false;
	bool hasLog = false;
	bool hasCond = false;
	bool hasCondition = false;
	bool hasLogFormat = false;

	bool enabled = true;
	bool log = true;
	MemCheckCondition cond = MEMCHECK_READWRITE;
	std::string condition;
	PostfixExpression compiledCondition;
	std::string logFormat;

	bool Parse(DebuggerRequest &req) {
		if (!currentDebugMIPS->isAlive()) {
			req.Fail("CPU not started");
			return false;
		}

		if (!req.ParamU32("address", &address))
			return false;
		uint32_t size;
		if (!req.ParamU32("size", &size))
			return false;
		if (address + size < address) {
			req.Fail("Size is too large");
			return false;
		}
		end = size == 0 ? 0 : address + size;

		hasEnabled = req.HasParam("enabled");
		if (hasEnabled) {
			if (!req.ParamBool("enabled", &enabled))
				return false;
		}
		hasLog = req.HasParam("log");
		if (hasLog) {
			if (!req.ParamBool("log", &log))
				return false;
		}
		hasCond = req.HasParam("read") || req.HasParam("write") || req.HasParam("change");
		if (hasCond) {
			bool read = false, write = false, change = false;
			if (!req.ParamBool("read", &read, DebuggerParamType::OPTIONAL) || !req.ParamBool("write", &write, DebuggerParamType::OPTIONAL) || !req.ParamBool("change", &change, DebuggerParamType::OPTIONAL))
				return false;
			int bits = (read ? MEMCHECK_READ : 0) | (write ? MEMCHECK_WRITE : 0) | (change ? MEMCHECK_WRITE_ONCHANGE : 0);
			cond = MemCheckCondition(bits);
		}
		hasCondition = req.HasParam("condition");
		if (hasCondition) {
			if (!req.ParamString("condition", &condition))
				return false;
			if (!initExpression(currentDebugMIPS, condition.c_str(), compiledCondition)) {
				req.Fail(StringFromFormat("Could not parse expression syntax: %s", getExpressionError()));
				return false;
			}
		}
		hasLogFormat = req.HasParam("logFormat");
		if (hasLogFormat) {
			if (!req.ParamString("logFormat", &logFormat))
				return false;
		}

		return true;
	}

	BreakAction Result(bool adding) {
		int bits = MEMCHECK_READWRITE;
		if (adding || (hasLog && hasEnabled)) {
			bits = (enabled ? BREAK_ACTION_PAUSE : 0) | (log ? BREAK_ACTION_LOG : 0);
		} else {
			MemCheck prev;
			if (g_breakpoints.GetMemCheck(address, end, &prev))
				bits = prev.result;

			if (hasEnabled)
				bits = (bits & ~BREAK_ACTION_PAUSE) | (enabled ? BREAK_ACTION_PAUSE : 0);
			if (hasLog)
				bits = (bits & ~BREAK_ACTION_LOG) | (log ? BREAK_ACTION_LOG : 0);
		}

		return BreakAction(bits);
	}

	void Apply() {
		if (hasCondition && !condition.empty()) {
			BreakPointCond cond;
			cond.debug = currentDebugMIPS;
			cond.expressionString = condition;
			cond.expression = compiledCondition;
			g_breakpoints.ChangeMemCheckAddCond(address, end, cond);
		} else if (hasCondition && condition.empty()) {
			g_breakpoints.ChangeMemCheckRemoveCond(address, end);
		}
		if (hasLogFormat) {
			g_breakpoints.ChangeMemCheckLogFormat(address, end, logFormat);
		}
	}
};

// Add a new memory breakpoint (memory.breakpoint.add)
//
// Parameters:
//  - address: unsigned integer address for the start of the memory range.
//  - size: unsigned integer specifying size of memory range.
//  - enabled: optional boolean, whether to actually enter stepping when this breakpoint trips.
//  - log: optional boolean, whether to log when this breakpoint trips.
//  - read: optional boolean, whether to trip on any read to this address.
//  - write: optional boolean, whether to trip on any write to this address.
//  - change: optional boolean, whether to trip on a write to this address which modifies data
//    (or any write that may modify data.)
//  - condition: optional string expression to evaluate - breakpoint does not trip if false.
//  - logFormat: optional string to log when breakpoint trips, may include {expression} parts.
//
// Response (same event name) with no extra data.
//
// Note: will replace any breakpoint that has the same start address and size.
void WebSocketMemoryBreakpointAdd(DebuggerRequest &req) {
	WebSocketMemoryBreakpointParams params;
	if (!params.Parse(req))
		return;

	g_breakpoints.AddMemCheck(params.address, params.end, params.cond, params.Result(true));
	params.Apply();
	req.Respond();
}

// Update a memory breakpoint (memory.breakpoint.update)
//
// Parameters:
//  - address: unsigned integer address for the start of the memory range.
//  - size: unsigned integer specifying size of memory range.
//  - enabled: optional boolean, whether to actually enter stepping when this breakpoint trips.
//  - log: optional boolean, whether to log when this breakpoint trips.
//  - read: optional boolean, whether to trip on any read to this address.
//  - write: optional boolean, whether to trip on any write to this address.
//  - change: optional boolean, whether to trip on a write to this address which modifies data
//    (or any write that may modify data.)
//  - condition: optional string expression to evaluate - breakpoint does not trip if false.
//  - logFormat: optional string to log when breakpoint trips, may include {expression} parts.
//
// Response (same event name) with no extra data.
void WebSocketMemoryBreakpointUpdate(DebuggerRequest &req) {
	WebSocketMemoryBreakpointParams params;
	if (!params.Parse(req))
		return;

	MemCheck mc;
	if (!g_breakpoints.GetMemCheck(params.address, params.end, &mc))
		return req.Fail("Breakpoint not found");

	g_breakpoints.ChangeMemCheck(params.address, params.end, params.cond, params.Result(true));
	params.Apply();
	req.Respond();
}

// Remove a memory breakpoint (memory.breakpoint.remove)
//
// Parameters:
//  - address: unsigned integer address for the start of the memory range.
//  - size: unsigned integer specifying size of memory range.
//
// Response (same event name) with no extra data.
void WebSocketMemoryBreakpointRemove(DebuggerRequest &req) {
	if (!currentDebugMIPS->isAlive()) {
		return req.Fail("CPU not started");
	}

	uint32_t address;
	if (!req.ParamU32("address", &address))
		return;
	uint32_t size;
	if (!req.ParamU32("size", &size))
		return;

	g_breakpoints.RemoveMemCheck(address, size == 0 ? 0 : address + size);
	req.Respond();
}

// List all memory breakpoints (memory.breakpoint.list)
//
// No parameters.
//
// Response (same event name):
//  - breakpoints: array of objects, each with properties:
//     - address: unsigned integer address for the start of the memory range.
//     - size: unsigned integer specifying size of memory range.
//     - enabled: boolean, whether to actually enter stepping when this breakpoint trips.
//     - log: optional boolean, whether to log when this breakpoint trips.
//     - read: optional boolean, whether to trip on any read to this address.
//     - write: optional boolean, whether to trip on any write to this address.
//     - change: optional boolean, whether to trip on a write to this address which modifies data
//       (or any write that may modify data.)
//     - condition: null, or string expression to evaluate - breakpoint does not trip if false.
//     - logFormat: null, or string to log when breakpoint trips, may include {expression} parts.
//     - symbol: null, or string label or symbol at breakpoint address.
void WebSocketMemoryBreakpointList(DebuggerRequest &req) {
	if (!currentDebugMIPS->isAlive()) {
		return req.Fail("CPU not started");
	}

	JsonWriter &json = req.Respond();
	json.pushArray("breakpoints");
	auto mcs = g_breakpoints.GetMemChecks();
	for (const auto &mc : mcs) {
		json.pushDict();
		json.writeUint("address", mc.start);
		json.writeUint("size", mc.end == 0 ? 0 : mc.end - mc.start);
		json.writeBool("enabled", mc.IsEnabled());
		json.writeBool("log", (mc.result & BREAK_ACTION_LOG) != 0);
		json.writeBool("read", (mc.cond & MEMCHECK_READ) != 0);
		json.writeBool("write", (mc.cond & MEMCHECK_WRITE) != 0);
		json.writeBool("change", (mc.cond & MEMCHECK_WRITE_ONCHANGE) != 0);
		json.writeUint("hits", mc.numHits);
		if (mc.hasCondition)
			json.writeString("condition", mc.condition.expressionString);
		else
			json.writeNull("condition");
		if (!mc.logFormat.empty())
			json.writeString("logFormat", mc.logFormat);
		else
			json.writeNull("logFormat");
		std::string symbol = g_symbolMap->GetLabelString(mc.start);
		if (symbol.empty())
			json.writeNull("symbol");
		else
			json.writeString("symbol", symbol);

		json.pop();
	}
	json.pop();
}
