#include <regex>
#include <sstream>

#include "ext/imgui/imgui.h"

#include "Common/System/Request.h"
#include "Core/MemMap.h"
#include "Core/Debugger/Breakpoints.h"
#include "Core/MIPS/MIPSDebugInterface.h"

#include "UI/ImDebugger/ImStructViewer.h"

static auto COLOR_GRAY = ImVec4(0.45f, 0.45f, 0.45f, 1);
static auto COLOR_RED = ImVec4(1, 0, 0, 1);

enum class BuiltInType {
	Bool,
	Char,
	Int8,
	Int16,
	Int32,
	Int64,
	TerminatedString,
	Float,
	Void,
};

struct BuiltIn {
	BuiltInType type;
	ImGuiDataType imGuiType;
	const char* hexFormat;
};

static const std::unordered_map<std::string, BuiltIn> knownBuiltIns = {
	{"/bool", {BuiltInType::Bool, ImGuiDataType_U8, "%hhx"}},

	{"/char", {BuiltInType::Char, ImGuiDataType_S8, "%hhx"}},
	{"/uchar", {BuiltInType::Char, ImGuiDataType_U8, "%hhx"}},

	{"/byte", {BuiltInType::Int8, ImGuiDataType_U8, "%hhx"}},
	{"/sbyte", {BuiltInType::Int8, ImGuiDataType_S8, "%hhx"}},
	{"/undefined1", {BuiltInType::Int8, ImGuiDataType_U8, "%hhx"}},

	{"/word", {BuiltInType::Int16, ImGuiDataType_U16, "%hx"}},
	{"/sword", {BuiltInType::Int16, ImGuiDataType_S16, "%hx"}},
	{"/ushort", {BuiltInType::Int16, ImGuiDataType_U16, "%hx"}},
	{"/short", {BuiltInType::Int16, ImGuiDataType_S16, "%hx"}},
	{"/undefined2", {BuiltInType::Int16, ImGuiDataType_U16, "%hx"}},

	{"/dword", {BuiltInType::Int32, ImGuiDataType_U32, "%lx"}},
	{"/sdword", {BuiltInType::Int32, ImGuiDataType_S32, "%lx"}},
	{"/uint", {BuiltInType::Int32, ImGuiDataType_U32, "%lx"}},
	{"/int", {BuiltInType::Int32, ImGuiDataType_S32, "%lx"}},
	{"/ulong", {BuiltInType::Int32, ImGuiDataType_U32, "%lx"}},
	{"/long", {BuiltInType::Int32, ImGuiDataType_S32, "%lx"}},
	{"/undefined4", {BuiltInType::Int32, ImGuiDataType_U32, "%lx"}},

	{"/qword", {BuiltInType::Int64, ImGuiDataType_U64, "%llx"}},
	{"/sqword", {BuiltInType::Int64, ImGuiDataType_S64, "%llx"}},
	{"/ulonglong", {BuiltInType::Int64, ImGuiDataType_U64, "%llx"}},
	{"/longlong", {BuiltInType::Int64, ImGuiDataType_S64, "%llx"}},
	{"/undefined8", {BuiltInType::Int64, ImGuiDataType_U64, "%llx"}},

	{"/TerminatedCString", {BuiltInType::TerminatedString, -1, nullptr}},

	{"/float", {BuiltInType::Float, ImGuiDataType_Float, nullptr}},
	{"/float4", {BuiltInType::Float, ImGuiDataType_Float, nullptr}},

	{"/void", {BuiltInType::Void, -1, nullptr}},
};

static void DrawBuiltInEditPopup(const BuiltIn& builtIn, const u32 address) {
	if (builtIn.imGuiType == -1) {
		return;
	}
	ImGui::OpenPopupOnItemClick("edit", ImGuiPopupFlags_MouseButtonRight);
	if (ImGui::BeginPopup("edit")) {
		if (ImGui::Selectable("Set to zero")) {
			switch (builtIn.type) {
				case BuiltInType::Bool:
				case BuiltInType::Char:
				case BuiltInType::Int8:
					Memory::Write_U8(0, address);
					break;
				case BuiltInType::Int16:
					Memory::Write_U16(0, address);
					break;
				case BuiltInType::Int32:
					Memory::Write_U32(0, address);
					break;
				case BuiltInType::Int64:
					Memory::Write_U64(0, address);
					break;
				case BuiltInType::Float:
					Memory::Write_Float(0, address);
					break;
				default:
					break;
			}
		}
		void* data = Memory::GetPointerWriteUnchecked(address);
		if (builtIn.hexFormat) {
			ImGui::DragScalar("Value (hex)", builtIn.imGuiType, data, 0.2f, nullptr, nullptr, builtIn.hexFormat);
		}
		ImGui::DragScalar("Value", builtIn.imGuiType, data, 0.2f);
		ImGui::EndPopup();
	}
}

static void DrawIntBuiltInEditPopup(const u32 address, const u32 length) {
	switch (length) {
		case 1:
			DrawBuiltInEditPopup(knownBuiltIns.at("/byte"), address);
			break;
		case 2:
			DrawBuiltInEditPopup(knownBuiltIns.at("/word"), address);
			break;
		case 4:
			DrawBuiltInEditPopup(knownBuiltIns.at("/dword"), address);
			break;
		case 8:
			DrawBuiltInEditPopup(knownBuiltIns.at("/qword"), address);
			break;
		default:
			break;
	}
}

static void DrawBuiltInContent(const BuiltIn& builtIn, const u32 address) {
	switch (builtIn.type) {
		case BuiltInType::Bool:
			ImGui::Text("= %s", Memory::Read_U8(address) ? "true" : "false");
			break;
		case BuiltInType::Char: {
			const u8 value = Memory::Read_U8(address);
			if (std::isprint(value)) {
				ImGui::Text("= %x '%c'", value, value);
			} else {
				ImGui::Text("= %x", value);
			}
			break;
		}
		case BuiltInType::Int8:
			ImGui::Text("= %x", Memory::Read_U8(address));
			break;
		case BuiltInType::Int16:
			ImGui::Text("= %x", Memory::Read_U16(address));
			break;
		case BuiltInType::Int32:
			ImGui::Text("= %x", Memory::Read_U32(address));
			break;
		case BuiltInType::Int64:
			ImGui::Text("= %llx", Memory::Read_U64(address));
			break;
		case BuiltInType::TerminatedString:
			if (Memory::IsValidNullTerminatedString(address)) {
				ImGui::Text("= \"%s\"", Memory::GetCharPointerUnchecked(address));
			} else {
				ImGui::Text("= %x <invalid string @ %x>", Memory::Read_U8(address), address);
			}
			break;
		case BuiltInType::Float:
			ImGui::Text("= %f", Memory::Read_Float(address));
			break;
		case BuiltInType::Void:
			ImGui::Text("<void type>");
		default:
			return;
	}
	DrawBuiltInEditPopup(builtIn, address);
}

static u64 ReadMemoryInt(const u32 address, const u32 length) {
	switch (length) {
		case 1:
			return Memory::Read_U8(address);
		case 2:
			return Memory::Read_U16(address);
		case 4:
			return Memory::Read_U32(address);
		case 8:
			return Memory::Read_U64(address);
		default:
			return 0;
	}
}

static constexpr int COLUMN_NAME = 0;
static constexpr int COLUMN_TYPE = 1;
static constexpr int COLUMN_CONTENT = 2;

void ImStructViewer::Draw(MIPSDebugInterface* mipsDebug, bool* open) {
	mipsDebug_ = mipsDebug;
	ImGui::SetNextWindowSize(ImVec2(750, 550), ImGuiCond_FirstUseEver);
	if (!ImGui::Begin("Struct viewer", open) || !mipsDebug->isAlive() || !Memory::IsActive()) {
		ImGui::End();
		return;
	}
	if (ghidraClient_.Ready()) {
		ghidraClient_.UpdateResult();
		if (!fetchedAtLeastOnce_ && !ghidraClient_.Failed()) {
			fetchedAtLeastOnce_ = true;
		}
	}

	ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
	if (fetchedAtLeastOnce_) {
		DrawStructViewer();
	} else {
		DrawConnectionSetup();
	}
	ImGui::PopStyleVar();

	ImGui::End();
}

void ImStructViewer::DrawConnectionSetup() {
	ImGui::TextWrapped("Struct viewer visualizes data in memory using types from your Ghidra project.");
	ImGui::TextWrapped("To get started install the ghidra-rest-api plugin and start the Rest API server.");
	ImGui::TextWrapped("When ready press the connect button below.");

	ImGui::BeginDisabled(!ghidraClient_.Idle());
	ImGui::PushItemWidth(120);
	ImGui::InputText("Host", ghidraHost_, IM_ARRAYSIZE(ghidraHost_));
	ImGui::SameLine();
	ImGui::InputInt("Port", &ghidraPort_, 0);
	ImGui::SameLine();
	if (ImGui::Button("Connect")) {
		ghidraClient_.FetchAll(ghidraHost_, ghidraPort_);
	}
	ImGui::PopItemWidth();
	ImGui::EndDisabled();

	if (ghidraClient_.Idle() && ghidraClient_.Failed()) {
		ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
		ImGui::TextWrapped("Error: %s", ghidraClient_.result.error.c_str());
		ImGui::PopStyleColor();
	}
}

void ImStructViewer::DrawStructViewer() {
	ImGui::BeginDisabled(!ghidraClient_.Idle());
	if (ImGui::Button("Refresh data types")) {
		ghidraClient_.FetchAll(ghidraHost_, ghidraPort_);
	}
	ImGui::EndDisabled();

	if (ghidraClient_.Idle() && ghidraClient_.Failed()) {
		ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
		ImGui::SameLine();
		ImGui::TextWrapped("Error: %s", ghidraClient_.result.error.c_str());
		ImGui::PopStyleColor();
	}

	if (ImGui::BeginTabBar("##tabs", ImGuiTabBarFlags_Reorderable)) {
		if (ImGui::BeginTabItem("Globals")) {
			DrawGlobals();
			ImGui::EndTabItem();
		}
		if (ImGui::BeginTabItem("Watch")) {
			DrawWatch();
			ImGui::EndTabItem();
		}
		ImGui::EndTabBar();
	}
}

void ImStructViewer::DrawGlobals() {
	globalFilter_.Draw();
	if (ImGui::BeginTable("##globals", 3,
	                      ImGuiTableFlags_BordersOuter | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY |
	                      ImGuiTableFlags_RowBg)) {
		ImGui::TableSetupScrollFreeze(0, 1);
		ImGui::TableSetupColumn("Field");
		ImGui::TableSetupColumn("Type");
		ImGui::TableSetupColumn("Content");
		ImGui::TableHeadersRow();

		for (const auto& symbol : ghidraClient_.result.symbols) {
			if (!symbol.label || !symbol.userDefined || symbol.dataTypePathName.empty()) {
				continue;
			}
			if (!globalFilter_.PassFilter(symbol.name.c_str())) {
				continue;
			}
			DrawType(symbol.address, 0, symbol.dataTypePathName, nullptr, symbol.name.c_str(), -1);
		}

		ImGui::EndTable();
	}
}

void ImStructViewer::DrawWatch() {
	DrawNewWatchEntry();
	ImGui::Dummy(ImVec2(1, 6));

	watchFilter_.Draw();
	if (ImGui::BeginTable("##watch", 3,
	                      ImGuiTableFlags_BordersOuter | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY |
	                      ImGuiTableFlags_RowBg)) {
		ImGui::TableSetupScrollFreeze(0, 1);
		ImGui::TableSetupColumn("Field");
		ImGui::TableSetupColumn("Type");
		ImGui::TableSetupColumn("Content");
		ImGui::TableHeadersRow();

		int watchIndex = -1;
		for (const auto& watch : watches_) {
			watchIndex++;
			if (!watchFilter_.PassFilter(watch.name.c_str())) {
				continue;
			}
			u32 address = 0;
			if (!watch.expression.empty()) {
				u32 val;
				PostfixExpression postfix;
				if (mipsDebug_->initExpression(watch.expression.c_str(), postfix)
				    && mipsDebug_->parseExpression(postfix, val)) {
					address = val;
				}
			} else {
				address = watch.address;
			}
			DrawType(address, 0, watch.typePathName, nullptr, watch.name.c_str(), watchIndex);
		}
		if (removeWatchIndex_ != -1) {
			watches_.erase(watches_.begin() + removeWatchIndex_);
			removeWatchIndex_ = -1;
		}
		if (addWatch_.address != 0) {
			watches_.push_back(addWatch_);
			addWatch_ = Watch();
		}

		ImGui::EndTable();
	}
}

void ImStructViewer::DrawNewWatchEntry() {
	ImGui::PushItemWidth(150);
	ImGui::InputText("Name", newWatch_.name, IM_ARRAYSIZE(newWatch_.name));

	ImGui::SameLine();
	if (ImGui::BeginCombo("Type", newWatch_.typeDisplayName.c_str())) {
		if (ImGui::IsWindowAppearing()) {
			ImGui::SetKeyboardFocusHere(0);
		}
		newWatch_.typeFilter.Draw();
		for (const auto& entry : ghidraClient_.result.types) {
			const auto& type = entry.second;
			if (newWatch_.typeFilter.PassFilter(type.displayName.c_str())) {
				ImGui::PushID(type.pathName.c_str());
				if (ImGui::Selectable(type.displayName.c_str(), newWatch_.typePathName == type.pathName)) {
					newWatch_.typePathName = type.pathName;
					newWatch_.typeDisplayName = type.displayName;
				}
				if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && ImGui::BeginTooltip()) {
					ImGui::Text("%s (%s)", type.displayName.c_str(), type.pathName.c_str());
					ImGui::Text("Length: %x (aligned: %x)", type.length, type.alignedLength);
					ImGui::EndTooltip();
				}
				ImGui::PopID();
			}
		}
		ImGui::EndCombo();
	}

	ImGui::SameLine();
	ImGui::InputText("Expression", newWatch_.expression, IM_ARRAYSIZE(newWatch_.expression));
	ImGui::SameLine();
	ImGui::Checkbox("Dynamic", &newWatch_.dynamic);
	if (ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip | ImGuiHoveredFlags_DelayNormal))
		ImGui::SetTooltip("When checked the expression will be\nre-evaluated on each frame.");

	ImGui::PopItemWidth();

	ImGui::SameLine();
	if (ImGui::Button("Add watch")) {
		u32 val;
		PostfixExpression postfix;
		if (newWatch_.typePathName.empty()) {
			newWatch_.error = "type can't be empty";
		} else if (!mipsDebug_->initExpression(newWatch_.expression, postfix)
		           || !mipsDebug_->parseExpression(postfix, val)) {
			newWatch_.error = "invalid expression";
		} else {
			std::string watchName = newWatch_.name;
			if (watchName.empty()) {
				watchName = "<watch>";
			}
			watches_.emplace_back(Watch{
				newWatch_.dynamic ? newWatch_.expression : "",
				newWatch_.dynamic ? 0 : val,
				newWatch_.typePathName,
				newWatch_.dynamic ? watchName + " (" + newWatch_.expression + ")" : watchName
			});
			memset(newWatch_.name, 0, sizeof(newWatch_.name));
			memset(newWatch_.expression, 0, sizeof(newWatch_.name));
			newWatch_.dynamic = false;
			newWatch_.error = "";
			newWatch_.typeFilter.Clear();
			// Not clearing the actual selected type on purpose here, user will have to reselect one anyway and maybe
			// there is a chance they will reuse the current one
		}
	}
	if (!newWatch_.error.empty()) {
		ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
		ImGui::TextWrapped("Error: %s", newWatch_.error.c_str());
		ImGui::PopStyleColor();
	}
}

static void DrawTypeColumn(
	const std::string& format,
	const std::string& typeDisplayName,
	const u32 base,
	const u32 offset
) {
	ImGui::TableSetColumnIndex(COLUMN_TYPE);
	ImGui::Text(format.c_str(), typeDisplayName.c_str());
	ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY);
	ImGui::SameLine();
	if (offset != 0) {
		ImGui::Text("@ %x+%x", base, offset);
	} else {
		ImGui::Text("@ %x", base);
	}
	ImGui::PopStyleColor();
}

static void DrawArrayContent(
	const std::unordered_map<std::string, GhidraType>& types,
	const GhidraType& type,
	const u32 address
) {
	if (type.arrayElementLength != 1 || !types.count(type.arrayTypePathName)) {
		return;
	}
	const auto& arrayType = types.at(type.arrayTypePathName);
	bool charElement = false;
	if (arrayType.kind == TYPEDEF && types.count(arrayType.typedefBaseTypePathName)) {
		const auto& baseArrayType = types.at(arrayType.typedefBaseTypePathName);
		charElement = baseArrayType.pathName == "/char";
	} else {
		charElement = arrayType.pathName == "/char";
	}
	if (!charElement) {
		return;
	}
	const char* charPointer = Memory::GetCharPointerUnchecked(address);
	std::string text(charPointer, charPointer + type.arrayElementCount);
	text = std::regex_replace(text, std::regex("\n"), "\\n");
	ImGui::Text("= \"%s\"", text.c_str());
}

static void DrawPointerText(const u32 value) {
	if (Memory::IsValidAddress(value)) {
		ImGui::Text("* %x", value);
		return;
	}
	ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY);
	if (value == 0) {
		ImGui::Text("* NULL");
	} else {
		ImGui::Text("* <invalid pointer: %x>", value);
	}
	ImGui::PopStyleColor();
}

static void DrawPointerContent(
	const std::unordered_map<std::string, GhidraType>& types,
	const GhidraType& type,
	const u32 value
) {
	if (!types.count(type.pointerTypePathName)) {
		DrawPointerText(value);
		return;
	}
	const auto& pointedType = types.at(type.pointerTypePathName);
	bool charPointerElement = false;
	if (pointedType.kind == TYPEDEF && types.count(pointedType.typedefBaseTypePathName)) {
		const auto& basePointedType = types.at(pointedType.typedefBaseTypePathName);
		charPointerElement = basePointedType.pathName == "/char";
	} else {
		charPointerElement = pointedType.pathName == "/char";
	}
	if (!charPointerElement || !Memory::IsValidNullTerminatedString(value)) {
		DrawPointerText(value);
		return;
	}
	const char* charPointer = Memory::GetCharPointerUnchecked(value);
	std::string text(charPointer);
	text = std::regex_replace(text, std::regex("\n"), "\\n");
	ImGui::Text("= \"%s\"", text.c_str());
}

// Formatting enum value to a nice string as it would look in code with 'or' operator (e.g. "ALIGN_TOP | ALIGN_LEFT")
static std::string FormatEnumValue(const std::vector<GhidraEnumMember>& enumMembers, const u64 value) {
	std::stringstream ss;
	bool hasPrevious = false;
	for (const auto& member : enumMembers) {
		if (value & member.value) {
			if (hasPrevious) {
				ss << " | ";
			}
			ss << member.name;
			hasPrevious = true;
		}
	}
	return ss.str();
}

// This will be potentially called a lot of times in a frame so not using string here
static void FormatIndexedMember(char* buffer, const size_t bufferSize, const std::string& name, const u32 index) {
	snprintf(buffer, bufferSize, "%s[%x]", name.c_str(), index);
}

void ImStructViewer::DrawType(
	const u32 base,
	const u32 offset,
	const std::string& typePathName,
	const char* typeDisplayNameOverride,
	const char* name,
	const int watchIndex,
	const ImGuiTreeNodeFlags extraTreeNodeFlags
) {
	const auto& types = ghidraClient_.result.types;
	// Generic pointer is not included in the type listing, need to resolve it manually to void*
	if (typePathName == "/pointer") {
		DrawType(base, offset, "/void *", "pointer", name, watchIndex);
		return;
	}
	// Undefined itself doesn't exist as a type, let's just display first byte in that case
	if (typePathName == "/undefined") {
		DrawType(base, offset, "/undefined1", "undefined", name, watchIndex);
		return;
	}

	const bool hasType = types.count(typePathName) != 0;

	// Resolve typedefs as early as possible
	if (hasType) {
		const auto& type = types.at(typePathName);
		if (type.kind == TYPEDEF) {
			DrawType(base, offset, type.typedefBaseTypePathName, type.displayName.c_str(), name,
			         watchIndex);
			return;
		}
	}

	const u32 address = base + offset;
	ImGui::PushID(static_cast<int>(address));
	ImGui::PushID(watchIndex);

	// Text and Tree nodes are less high than framed widgets, using AlignTextToFramePadding() we add vertical spacing
	// to make the tree lines equal high.
	ImGui::TableNextRow();
	ImGui::TableSetColumnIndex(COLUMN_NAME);
	ImGui::AlignTextToFramePadding();
	// Flags used for nodes that can't be further opened
	const ImGuiTreeNodeFlags leafFlags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen |
	                                     extraTreeNodeFlags;

	// Type is missing in fetched types, this can happen e.g. if type used for watch is removed from Ghidra
	if (!hasType) {
		ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
		DrawContextMenu(base, offset, 0, typePathName, name, watchIndex, nullptr);
		ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
		DrawTypeColumn("<missing type: %s>", typePathName, base, offset);
		ImGui::PopStyleColor();
		ImGui::PopID();
		ImGui::PopID();
		return;
	}

	const auto& type = types.at(typePathName);
	const std::string typeDisplayName =
			typeDisplayNameOverride == nullptr ? type.displayName : typeDisplayNameOverride;

	// Handle cases where pointers or expressions point to invalid memory
	if (!Memory::IsValidAddress(address)) {
		ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
		DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, nullptr);
		DrawTypeColumn("%s", typeDisplayName, base, offset);
		ImGui::TableSetColumnIndex(COLUMN_CONTENT);
		ImGui::PushStyleColor(ImGuiCol_Text, COLOR_GRAY);
		ImGui::Text("<invalid address: %x>", address);
		ImGui::PopStyleColor();
		ImGui::PopID();
		ImGui::PopID();
		return;
	}

	// For each type we create tree node with the field name and fill type column
	// Content column and edit popup is only set for types where it makes sense
	switch (type.kind) {
		case ENUM: {
			ImGui::TreeNodeEx("Enum", leafFlags, "%s", name);
			const u64 enumValue = ReadMemoryInt(address, type.length);
			DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, &enumValue);
			DrawTypeColumn("%s", typeDisplayName, base, offset);

			ImGui::TableSetColumnIndex(COLUMN_CONTENT);
			const std::string enumString = FormatEnumValue(type.enumMembers, enumValue);
			ImGui::Text("= %llx (%s)", enumValue, enumString.c_str());
			DrawIntBuiltInEditPopup(address, type.length);
			break;
		}
		case POINTER: {
			const bool nodeOpen = ImGui::TreeNodeEx("Pointer", extraTreeNodeFlags, "%s", name);
			const u32 pointer = Memory::Read_U32(address);
			const u64 pointer64 = pointer;
			DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, &pointer64);
			DrawTypeColumn("%s", typeDisplayName, base, offset);

			ImGui::TableSetColumnIndex(COLUMN_CONTENT);
			DrawPointerContent(types, type, pointer);

			if (nodeOpen) {
				if (types.count(type.pointerTypePathName)) {
					const auto& pointedType = types.at(type.pointerTypePathName);

					const auto countStateId = ImGui::GetID("PointerElementCount");
					const int pointerElementCount = ImGui::GetStateStorage()->GetInt(countStateId, 1);

					// A pointer to unsized type (e.g. function or void) can't have more than one element
					if (pointedType.alignedLength > 0) {
						ImGui::TableNextRow();
						ImGui::TableSetColumnIndex(COLUMN_NAME);
						if (ImGui::Button("Show more")) {
							ImGui::GetStateStorage()->SetInt(countStateId, pointerElementCount + 1);
						}
						if (pointerElementCount > 1) {
							ImGui::SameLine();
							ImGui::Text("(showing %x)", pointerElementCount);
						}
					}

					for (int i = 0; i < pointerElementCount; i++) {
						char nameBuffer[256];
						FormatIndexedMember(nameBuffer, sizeof(nameBuffer), name, i);
						// A pointer always creates extra node in the tree so using DefaultOpen to spare user one click
						DrawType(pointer, i * pointedType.alignedLength, type.pointerTypePathName,
						         nullptr, nameBuffer, -1, ImGuiTreeNodeFlags_DefaultOpen);
					}
				}
				ImGui::TreePop();
			}
			break;
		}
		case ARRAY: {
			const bool nodeOpen = ImGui::TreeNodeEx("Array", extraTreeNodeFlags, "%s", name);
			DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, nullptr);
			DrawTypeColumn("%s", typeDisplayName, base, offset);

			ImGui::TableSetColumnIndex(COLUMN_CONTENT);
			DrawArrayContent(types, type, address);

			if (nodeOpen) {
				for (int i = 0; i < type.arrayElementCount; i++) {
					char nameBuffer[256];
					FormatIndexedMember(nameBuffer, sizeof(nameBuffer), name, i);
					DrawType(base, offset + i * type.arrayElementLength, type.arrayTypePathName,
					         nullptr, nameBuffer, -1);
				}
				ImGui::TreePop();
			}
			break;
		}
		case STRUCTURE:
		case UNION: {
			const bool nodeOpen = ImGui::TreeNodeEx("Composite", extraTreeNodeFlags, "%s", name);
			DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, nullptr);
			DrawTypeColumn("%s", typeDisplayName, base, offset);

			if (nodeOpen) {
				for (const auto& member : type.compositeMembers) {
					DrawType(base, offset + member.offset, member.typePathName, nullptr,
					         member.fieldName.c_str(), -1);
				}
				ImGui::TreePop();
			}
			break;
		}
		case FUNCTION_DEFINITION:
			ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
			DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, nullptr);
			DrawTypeColumn("%s", typeDisplayName, base, offset);

			ImGui::TableSetColumnIndex(COLUMN_CONTENT);
			ImGui::Text("<function definition>"); // TODO could be go to in disassembler here
			break;
		case BUILT_IN: {
			ImGui::TreeNodeEx("Field", leafFlags, "%s", name);

			if (knownBuiltIns.count(typePathName)) {
				// This will copy float as int, but we can live with that for now
				const u64 value = ReadMemoryInt(address, type.alignedLength);
				DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, &value);
				DrawTypeColumn("%s", typeDisplayName, base, offset);
				ImGui::TableSetColumnIndex(COLUMN_CONTENT);
				DrawBuiltInContent(knownBuiltIns.at(typePathName), address);
			} else {
				// Some built in types are rather obscure so we don't handle every possible one
				ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
				DrawTypeColumn("<unsupported built in: %s>", typePathName, base, offset);
				ImGui::PopStyleColor();
			}
			break;
		}
		default: {
			// At this point there is most likely some issue in the Ghidra plugin and the type wasn't
			// classified to any category
			ImGui::TreeNodeEx("Field", leafFlags, "%s", name);
			DrawContextMenu(base, offset, type.alignedLength, typePathName, name, watchIndex, nullptr);
			ImGui::PushStyleColor(ImGuiCol_Text, COLOR_RED);
			DrawTypeColumn("<not implemented type: %s>", typeDisplayName, base, offset);
			ImGui::PopStyleColor();
			break;
		}
	}

	ImGui::PopID();
	ImGui::PopID();
}

static void CopyHexNumberToClipboard(u64 value) {
	std::stringstream ss;
	ss << std::hex << value;
	const std::string valueString = ss.str();
	System_CopyStringToClipboard(valueString);
}

void ImStructViewer::DrawContextMenu(
	const u32 base,
	const u32 offset,
	const int length,
	const std::string& typePathName,
	const char* name,
	const int watchIndex,
	const u64* value
) {
	ImGui::OpenPopupOnItemClick("context", ImGuiPopupFlags_MouseButtonRight);
	if (ImGui::BeginPopup("context")) {
		const u32 address = base + offset;

		if (ImGui::MenuItem("Copy address")) {
			CopyHexNumberToClipboard(address);
		}
		if (value && ImGui::MenuItem("Copy value")) {
			CopyHexNumberToClipboard(*value);
		}

		// This might be called when iterating over existing watches so can't modify the watch vector directly here
		if (watchIndex < 0) {
			if (ImGui::MenuItem("Add watch")) {
				addWatch_.address = address;
				addWatch_.typePathName = typePathName;
				addWatch_.name = name;
			}
		} else if (watchIndex < watches_.size()) {
			if (ImGui::MenuItem("Remove watch")) {
				removeWatchIndex_ = watchIndex;
			}
		}

		// if (ImGui::MenuItem("Go to in memory view")) {
		// TODO imgui memory view not yet implemented
		// }

		// Memory breakpoints are only possible for sized types
		if (length > 0) {
			const u32 end = address + length;
			MemCheck memCheck;
			const bool hasMemCheck = CBreakPoints::GetMemCheck(address, end, &memCheck);
			if (hasMemCheck) {
				if (ImGui::MenuItem("Remove memory breakpoint")) {
					CBreakPoints::RemoveMemCheck(address, end);
				}
			}
			const bool canAddRead = !hasMemCheck || !(memCheck.cond & MEMCHECK_READ);
			const bool canAddWrite = !hasMemCheck || !(memCheck.cond & MEMCHECK_WRITE);
			const bool canAddWriteOnChange = !hasMemCheck || !(memCheck.cond & MEMCHECK_WRITE_ONCHANGE);
			if ((canAddRead || canAddWrite || canAddWriteOnChange) && ImGui::BeginMenu("Add memory breakpoint")) {
				if (canAddRead && canAddWrite && ImGui::MenuItem("Read/Write")) {
					constexpr auto cond = static_cast<MemCheckCondition>(MEMCHECK_READ | MEMCHECK_WRITE);
					CBreakPoints::AddMemCheck(address, end, cond, BREAK_ACTION_PAUSE);
				}
				if (canAddRead && ImGui::MenuItem("Read")) {
					CBreakPoints::AddMemCheck(address, end, MEMCHECK_READ, BREAK_ACTION_PAUSE);
				}
				if (canAddWrite && ImGui::MenuItem("Write")) {
					CBreakPoints::AddMemCheck(address, end, MEMCHECK_WRITE, BREAK_ACTION_PAUSE);
				}
				if (canAddWriteOnChange && ImGui::MenuItem("Write Change")) {
					constexpr auto cond = static_cast<MemCheckCondition>(MEMCHECK_WRITE | MEMCHECK_WRITE_ONCHANGE);
					CBreakPoints::AddMemCheck(address, end, cond, BREAK_ACTION_PAUSE);
				}
				ImGui::EndMenu();
			}
		}

		ImGui::EndPopup();
	}
}