#include <cctype>
#include <cmath>
#include <iomanip>
#include <cstdio>
#include <sstream>

#include "ext/imgui/imgui.h"
#include "ext/imgui/imgui_internal.h"
#include "ext/imgui/imgui_impl_thin3d.h"

#include "ext/xxhash.h"
#include "Common/StringUtils.h"
#include "Common/File/FileUtil.h"
#include "Core/Config.h"
#include "Core/MemMap.h"
#include "Core/Reporting.h"
#include "Core/RetroAchievements.h"
#include "Common/System/Display.h"

#include "Core/System.h"
#include "UI/ImDebugger/ImDebugger.h"
#include "UI/ImDebugger/ImMemView.h"

static const char* searchtypes[] = {"u8", "u16", "u32", "u64", "float", "string", "string16", "bytesequence"};

ImMemView::ImMemView() {
	windowStart_ = curAddress_;
	selectRangeStart_ = curAddress_;
	selectRangeEnd_ = curAddress_ + 1;
	lastSelectReset_ = curAddress_;
}

ImMemView::~ImMemView() {}

static uint32_t pickTagColor(std::string_view tag) {
	uint32_t colors[6] = { 0xFF301010, 0xFF103030, 0xFF403010, 0xFF103000, 0xFF301030, 0xFF101030 };
	int which = XXH3_64bits(tag.data(), tag.length()) % ARRAY_SIZE(colors);
	return colors[which];
}

void ImMemView::Draw(ImDrawList *drawList) {
	if (!debugger_->isAlive()) {
		return;
	}

	ImGui_PushFixedFont();

	rowHeight_ = ImGui::GetTextLineHeightWithSpacing();
	charWidth_ = ImGui::CalcTextSize("W", nullptr, false, -1.0f).x;
	charHeight_ = ImGui::GetTextLineHeight();

	offsetPositionY_ = offsetLine * rowHeight_;

	addressStartX_ = charWidth_;
	hexStartX_ = addressStartX_ + 9 * charWidth_;
	asciiStartX_ = hexStartX_ + (rowSize_ * 3 + 1) * charWidth_;

	const ImVec2 canvas_p0 = ImGui::GetCursorScreenPos();      // ImDrawList API uses screen coordinates!
	const ImVec2 canvas_sz = ImGui::GetContentRegionAvail();   // Resize canvas to what's available
	const ImVec2 canvas_p1 = ImVec2(canvas_p0.x + canvas_sz.x, canvas_p0.y + canvas_sz.y);

	// This will catch our interactions
	bool pressed = ImGui::InvisibleButton("canvas", canvas_sz, ImGuiButtonFlags_MouseButtonLeft | ImGuiButtonFlags_MouseButtonRight);
	const bool is_hovered = ImGui::IsItemHovered(); // Hovered
	const bool is_active = ImGui::IsItemActive();   // Held

	if (pressed) {
		// INFO_LOG(Log::System, "Pressed");
	}
	ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY);

	visibleRows_ = canvas_sz.y / rowHeight_;

	if (displayOffsetScale_) {
		// visibleRows_ is calculated based on the size of the control, but X rows have already been used for the offsets and are no longer usable
		visibleRows_ -= offsetSpace;
	}

	Bounds bounds;
	bounds.x = canvas_p0.x;
	bounds.y = canvas_p0.y;
	bounds.w = canvas_p1.x - canvas_p0.x;
	bounds.h = canvas_p1.y - canvas_p0.y;

	drawList->PushClipRect(canvas_p0, canvas_p1, true);
	drawList->AddRectFilled(canvas_p0, canvas_p1, IM_COL32(25, 25, 25, 255));

	if (displayOffsetScale_)
		drawOffsetScale(drawList);

	std::vector<MemBlockInfo> memRangeInfo = FindMemInfoByFlag(highlightFlags_, windowStart_, (visibleRows_ + 1) * rowSize_);

	_assert_msg_(((windowStart_ | rowSize_) & 3) == 0, "readMemory() can't handle unaligned reads");

	// draw one extra row that may be partially visible
	for (int i = 0; i < visibleRows_ + 1; i++) {
		int rowY = rowHeight_ * i;
		// Skip the first X rows to make space for the offsets.
		if (displayOffsetScale_)
			rowY += rowHeight_ * offsetSpace;

		char temp[32];
		uint32_t address = windowStart_ + i * rowSize_;
		snprintf(temp, sizeof(temp), "%08x", address);
		drawList->AddText(ImVec2(canvas_p0.x + addressStartX_, canvas_p0.y + rowY), IM_COL32(0xE0, 0xE0, 0xE0, 0xFF), temp);

		union {
			uint32_t words[4];
			uint8_t bytes[16];
		} memory;
		int valid = debugger_ != nullptr && debugger_->isAlive() ? Memory::ClampValidSizeAt(address, 16) / 4 : 0;
		for (int i = 0; i < valid; ++i) {
			memory.words[i] = debugger_->readMemory(address + i * 4);
		}

		for (int j = 0; j < rowSize_; j++) {
			const uint32_t byteAddress = (address + j) & ~0xC0000000;
			std::string_view tag;
			bool tagContinues = false;
			for (const auto &info : memRangeInfo) {
				if (info.start <= byteAddress && info.start + info.size > byteAddress) {
					tag = info.tag;  // doesn't take ownership!
					tagContinues = byteAddress + 1 < info.start + info.size;
				}
			}

			int hexX = hexStartX_ + j * 3 * charWidth_;
			int hexLen = 2;
			int asciiX = asciiStartX_ + j * (charWidth_ + 2);

			char c;
			if (valid) {
				snprintf(temp, sizeof(temp), "%02x ", memory.bytes[j]);
				c = (char)memory.bytes[j];
				if (memory.bytes[j] < 32 || memory.bytes[j] >= 128)
					c = '.';
			} else {
				truncate_cpy(temp, "??");
				c = '.';
			}

			ImColor standardBG = 0xFF000000;
			bool continueRect = false;

			ImColor hexBGCol = 0;
			ImColor hexTextCol = 0xFFFFFFFF;
			ImColor asciiBGCol = 0;
			ImColor asciiTextCol = 0xFFFFFFFF;
			int underline = -1;

			const ImColor primarySelFg = 0xFFFFFFFF;
			const ImColor primarySelBg = 0xFFFF9933;
			const ImColor secondarySelFg = 0xFFFFFFFF;
			const ImColor secondarySelBg = 0xFF808080;

			const ImColor zeroColor = 0x888888ff;
			const ImColor searchResBg = 0xFF108010;

			if (address + j >= selectRangeStart_ && address + j < selectRangeEnd_) {
				if (asciiSelected_) {
					hexBGCol = secondarySelBg;
					hexTextCol = secondarySelFg;
					asciiBGCol = primarySelBg;
					asciiTextCol = primarySelFg;
				} else {
					hexBGCol = primarySelBg;
					hexTextCol = primarySelFg;
					asciiBGCol = secondarySelBg;
					asciiTextCol = secondarySelFg;
					if (address + j == curAddress_)
						underline = selectedNibble_;
				}
				if (!tag.empty() && tagContinues) {
					continueRect = true;
				}
			} else if (!tag.empty()) {
				if (memSearch_.status == SEARCH_OK && address+j >= memSearch_.matchAddress && address +j < memSearch_.matchAddress + memSearch_.data.size()) {
					hexBGCol = searchResBg;
				} else {
					hexBGCol = pickTagColor(tag);
				}
				continueRect = tagContinues;
				asciiBGCol = hexBGCol;
			}

			ImColor fg = hexTextCol;
			ImColor bg = hexBGCol;
			// SelectObject(hdc, underline == 0 ? (HGDIOBJ)underlineFont : (HGDIOBJ)font);
			if (bg != 0) {
				int bgWidth = 2; // continueRect ? 3 : 2;
				drawList->AddRectFilled(ImVec2(canvas_p0.x + hexX - 1, canvas_p0.y + rowY), ImVec2(canvas_p0.x + hexX + charWidth_ * bgWidth, canvas_p0.y + rowY + charHeight_), bg);
			}
			if (drawZeroDark_ && temp[0] == '0' && temp[1] == '0') {
				// if the byte is all zero make it darker
				fg = zeroColor;
			}
			drawList->AddText(ImVec2(canvas_p0.x + hexX, canvas_p0.y + rowY), fg, &temp[0], &temp[2]);
			if (underline >= 0) {
				float x = canvas_p0.x + hexX + underline * charWidth_;
				drawList->AddRectFilled(ImVec2(x, canvas_p0.y + rowY + charHeight_ - 2), ImVec2(x + charWidth_, canvas_p0.y + rowY + charHeight_), IM_COL32(0xFF, 0xFF, 0xFF, 0xFF));
			}

			fg = asciiTextCol;
			bg = asciiBGCol;
			if (bg) {
				drawList->AddRectFilled(ImVec2(canvas_p0.x + asciiX, canvas_p0.y + rowY), ImVec2(canvas_p0.x + asciiX + charWidth_, canvas_p0.y + rowY + charHeight_), bg);
			}
			drawList->AddText(ImVec2(canvas_p0.x + asciiX, canvas_p0.y + rowY), fg, &c, &c + 1);
		}
	}

	ImGuiIO& io = ImGui::GetIO();
	ImVec2 mousePos = ImVec2(io.MousePos.x - canvas_p0.x, io.MousePos.y - canvas_p0.y);
	if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
		// INFO_LOG(Log::System, "Mousedown %f,%f active:%d hover:%d", mousePos.x, mousePos.y, is_active, is_hovered);
		onMouseDown(mousePos.x, mousePos.y, 1);
	}
	if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
		// INFO_LOG(Log::CPU, "Mousedown %f,%f active:%d hover:%d", mousePos.x, mousePos.y, is_active, is_hovered);
		onMouseDown(mousePos.x, mousePos.y, 2);
	}
	if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
		// INFO_LOG(Log::System, "Mouseup %f,%f active:%d hover:%d", mousePos.x, mousePos.y, is_active, is_hovered);
		if (is_hovered) {
			onMouseUp(mousePos.x, mousePos.y, 1);
		}
	}
	if (ImGui::IsMouseDragging(ImGuiMouseButton_Left)) {
		// INFO_LOG(Log::System, "Mousedrag %f,%f active:%d hover:%d", mousePos.x, mousePos.y, is_active, is_hovered);
		if (is_hovered) {
			onMouseMove(mousePos.x, mousePos.y, 1);
		}
	}

	if (is_hovered) {
		if (io.MouseWheel > 0.0f) {  // TODO: Scale steps by the value.
			windowStart_ -= rowSize_ * 4;
		} else if (io.MouseWheel < 0.0f) {
			windowStart_ += rowSize_ * 4;
		}
	}

	ProcessKeyboardShortcuts(ImGui::IsItemFocused());

	ImGui_PopFont();

	ImGui::OpenPopupOnItemClick("memcontext", ImGuiPopupFlags_MouseButtonRight);
	PopupMenu();

	drawList->PopClipRect();
}

// We don't have a scroll bar - though maybe we should build one.
/*
void ImMemView::onVScroll(WPARAM wParam, LPARAM lParam) {
	switch (wParam & 0xFFFF) {
	case SB_LINEDOWN:
		ScrollWindow(1, GotoModeFromModifiers(false));
		break;
	case SB_LINEUP:
		ScrollWindow(-1, GotoModeFromModifiers(false));
		break;
	case SB_PAGEDOWN:
		ScrollWindow(visibleRows_, GotoModeFromModifiers(false));
		break;
	case SB_PAGEUP:
		ScrollWindow(-visibleRows_, GotoModeFromModifiers(false));
		break;
	default:
		return;
	}
}
*/

void ImMemView::ProcessKeyboardShortcuts(bool focused) {
	if (!focused) {
		return;
	}

	ImGuiIO& io = ImGui::GetIO();

	if (io.KeyMods & ImGuiMod_Ctrl) {
		if (ImGui::IsKeyPressed(ImGuiKey_G)) {
			/*
			u32 addr;
			if (executeExpressionWindow(wnd, debugger_, addr) == false)
				return;
			gotoAddr(addr);
			return;
			*/
		}

		if (ImGui::IsKeyPressed(ImGuiKey_P)) {
			PasteFromByteClipboard();
			return;
		}
		if (ImGui::IsKeyPressed(ImGuiKey_C)) {
			CopyToByteClipboard();
			return;
		}
	} else {
		if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
			ScrollCursor(rowSize_, GotoModeFromModifiers(false));
			return;
		}
		if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
			ScrollCursor(-rowSize_, GotoModeFromModifiers(false));
			return;
		}
		if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) {
			ScrollCursor(-1, GotoModeFromModifiers(false));
			return;
		}
		if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) {
			ScrollCursor(1, GotoModeFromModifiers(false));
			return;
		}
		if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) {
			ScrollWindow(visibleRows_, GotoModeFromModifiers(false));
			return;
		}
		if (ImGui::IsKeyPressed(ImGuiKey_PageUp)) {
			ScrollWindow(-visibleRows_, GotoModeFromModifiers(false));
			return;
		}

		if (editableMemory_) {
			if (!asciiSelected_) {
				for (int i = ImGuiKey_0; i != ImGuiKey_G; i++) {
					if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(i))) {
						EditMemory(i - ImGuiKey_0);
						return;
					}
				}
				for (int i = ImGuiKey_Keypad0; i != ImGuiKey_KeypadDecimal; i++) {
					if (ImGui::IsKeyPressed(static_cast<ImGuiKey>(i))) {
						EditMemory(i - ImGuiKey_Keypad0);
						return;
					}
				}
			}
		}
	}
}

void ImMemView::EditMemory(int newNibble) {
	uint8_t og = Memory::ReadUnchecked_U8(curAddress_);
	u8 out;
	if (selectedNibble_) {
		out = (og & 0xF0) | newNibble;
	} else {
		out = (og & 0x0F) | (newNibble << 4);
	}
	Memory::WriteUnchecked_U8((u8)out, curAddress_);
	ScrollCursor(1, GotoMode::RESET);
}

void ImMemView::CopyToByteClipboard() {
	byteClipboard_.clear();
	uint32_t size = selectRangeEnd_ - selectRangeStart_;
	byteClipboard_.resize(size);
	Memory::MemcpyUnchecked(byteClipboard_.data(), curAddress_, size);
}

void ImMemView::PasteFromByteClipboard() {
	if (editableMemory_)
		Memory::MemcpyUnchecked(curAddress_, byteClipboard_.data(), (u32)byteClipboard_.size());
}

void ImMemView::toggleEditableMemory(bool toggle) {
	editableMemory_ = toggle;
}


void ImMemView::onChar(int c) {
	ImGuiIO& io = ImGui::GetIO();

	if (io.KeyMods & ImGuiMod_Ctrl) {
		return;
	}

	if (!Memory::IsValidAddress(curAddress_)) {
		ScrollCursor(1, GotoMode::RESET);
		return;
	}

	if (asciiSelected_) {
		Memory::WriteUnchecked_U8((u8)c, curAddress_);
		ScrollCursor(1, GotoMode::RESET);
	} else {
		c = tolower(c);
		int inputValue = -1;

		if (c >= '0' && c <= '9') inputValue = c - '0';
		if (c >= 'a' && c <= 'f') inputValue = c - 'a' + 10;

		if (inputValue >= 0) {
			int shiftAmount = (1 - selectedNibble_) * 4;

			u8 oldValue = Memory::ReadUnchecked_U8(curAddress_);
			oldValue &= ~(0xF << shiftAmount);
			u8 newValue = oldValue | (inputValue << shiftAmount);
			Memory::WriteUnchecked_U8(newValue, curAddress_);
			ScrollCursor(1, GotoMode::RESET);
		}
	}

	Reporting::NotifyDebugger();
}

ImMemView::GotoMode ImMemView::GotoModeFromModifiers(bool isRightClick) {
	GotoMode mode = GotoMode::RESET;
	auto &io = ImGui::GetIO();
	if (isRightClick) {
		mode = GotoMode::RESET_IF_OUTSIDE;
	} else if (io.KeyMods & ImGuiMod_Shift) {
		if (io.KeyMods & ImGuiMod_Ctrl)
			mode = GotoMode::EXTEND;
		else
			mode = GotoMode::FROM_CUR;
	}
	return mode;
}

void ImMemView::onMouseDown(float x, float y, int button) {
	if (Achievements::HardcoreModeActive())
		return;

	GotoPoint(x, y, GotoModeFromModifiers(button == 2));
}

void ImMemView::PopupMenu() {
	if (ImGui::BeginPopup("memcontext")) {

		int32_t selectedSize = selectRangeEnd_ - selectRangeStart_;
		bool enable16 = !asciiSelected_ && (selectedSize == 1 || (selectedSize & 1) == 0);
		bool enable32 = !asciiSelected_ && (selectedSize == 1 || (selectedSize & 3) == 0);
		/*
		if (ImGui::MenuItem("Dump memory")) {
			DumpMemoryWindow dump(wnd, debugger_);
			dump.exec();
			break;
		}
		*/
		ImGui::SeparatorText("Internal clipboard");
		if (ImGui::MenuItem("Copy bytes")) {
			CopyToByteClipboard();
		}
		if (ImGui::MenuItem("Paste bytes")) {
			PasteFromByteClipboard();
		}
		ImGui::SeparatorText("System clipboard");
		if (ImGui::MenuItem("Copy value (8-bit)")) {
			size_t tempSize = 3 * selectedSize + 1;
			char *temp = new char[tempSize];
			memset(temp, 0, tempSize);

			// it's admittedly not really useful like this
			if (asciiSelected_) {
				for (uint32_t p = selectRangeStart_; p != selectRangeEnd_; ++p) {
					uint8_t c = Memory::IsValidAddress(p) ? Memory::ReadUnchecked_U8(p) : '.';
					if (c < 32 || c >= 128)
						c = '.';
					temp[p - selectRangeStart_] = c;
				}
			} else {
				char *pos = temp;
				for (uint32_t p = selectRangeStart_; p != selectRangeEnd_; ++p) {
					uint8_t c = Memory::IsValidAddress(p) ? Memory::ReadUnchecked_U8(p) : 0xFF;
					pos += snprintf(pos, tempSize - (pos - temp + 1), "%02X ", c);
				}
				// Clear the last space.
				if (pos > temp)
					*(pos - 1) = '\0';
			}

			System_CopyStringToClipboard(temp);
			delete[] temp;
		}

		if (ImGui::MenuItem("Copy value (16-bit)")) {
			size_t tempSize = 5 * ((selectedSize + 1) / 2) + 1;
			char *temp = new char[tempSize];
			memset(temp, 0, tempSize);

			char *pos = temp;
			for (uint32_t p = selectRangeStart_; p < selectRangeEnd_; p += 2) {
				uint16_t c = Memory::IsValidRange(p, 2) ? Memory::ReadUnchecked_U16(p) : 0xFFFF;
				pos += snprintf(pos, tempSize - (pos - temp + 1), "%04X ", c);
			}
			// Clear the last space.
			if (pos > temp)
				*(pos - 1) = '\0';

			System_CopyStringToClipboard(temp);
			delete[] temp;
		}

		if (ImGui::MenuItem("Copy value (32-bit)")) {
			size_t tempSize = 9 * ((selectedSize + 3) / 4) + 1;
			char *temp = new char[tempSize];
			memset(temp, 0, tempSize);

			char *pos = temp;
			for (uint32_t p = selectRangeStart_; p < selectRangeEnd_; p += 4) {
				uint32_t c = Memory::IsValidRange(p, 4) ? Memory::ReadUnchecked_U32(p) : 0xFFFFFFFF;
				pos += snprintf(pos, tempSize - (pos - temp + 1), "%08X ", c);
			}
			// Clear the last space.
			if (pos > temp)
				*(pos - 1) = '\0';

			System_CopyStringToClipboard(temp);
			delete[] temp;
		}

		if (ImGui::MenuItem("Copy value (float32)")) {
			char temp[64];
			snprintf(temp, sizeof(temp), "%f", Memory::IsValidAddress(curAddress_) ? Memory::Read_Float(curAddress_) : NAN);
			System_CopyStringToClipboard(temp);
		}
		/*
		case ID_MEMVIEW_EXTENTBEGIN:
		{
			std::vector<MemBlockInfo> memRangeInfo = FindMemInfoByFlag(highlightFlags_, curAddress_, 1);
			uint32_t addr = curAddress_;
			for (MemBlockInfo info : memRangeInfo) {
				addr = info.start;
			}
			gotoAddr(addr);
			break;
		}

		case ID_MEMVIEW_EXTENTEND:
		{
			std::vector<MemBlockInfo> memRangeInfo = FindMemInfoByFlag(highlightFlags_, curAddress_, 1);
			uint32_t addr = curAddress_;
			for (MemBlockInfo info : memRangeInfo) {
				addr = info.start + info.size - 1;
			}
			gotoAddr(addr);
			break;
		}
		*/
		if (ImGui::MenuItem("Copy address")) {
			char temp[24];
			snprintf(temp, sizeof(temp), "0x%08X", curAddress_);
			System_CopyStringToClipboard(temp);
		}
		ImGui::SeparatorText("Misc");
		if (ImGui::MenuItem("Goto in disasm")) {
			/*
			if (disasmWindow) {
				disasmWindow->Goto(curAddress_);
				disasmWindow->Show(true);
			}
			*/
		}
		ImGui::EndPopup();
	}
}

void ImMemView::onMouseUp(float x, float y, int button) {
	if (button == 2) {

		// HMENU menu = GetContextMenu(ContextMenuID::MEMVIEW);
		// EnableMenuItem(menu, ID_MEMVIEW_COPYVALUE_16, enable16 ? MF_ENABLED : MF_GRAYED);
		// EnableMenuItem(menu, ID_MEMVIEW_COPYVALUE_32, enable32 ? MF_ENABLED : MF_GRAYED);
		// EnableMenuItem(menu, ID_MEMVIEW_COPYFLOAT_32, enable32 ? MF_ENABLED : MF_GRAYED);

	}

	/*
	int x = LOWORD(lParam);
	int y = HIWORD(lParam);
	ReleaseCapture();
	*/
	GotoPoint(x, y, GotoModeFromModifiers(button == 2));
}

void ImMemView::onMouseMove(float x, float y, int button) {
	if (Achievements::HardcoreModeActive())
		return;

	if (button & 1) {
		GotoPoint(x, y, GotoModeFromModifiers(button == 2));
	}
}

void ImMemView::updateStatusBarText() {
	std::vector<MemBlockInfo> memRangeInfo = FindMemInfoByFlag(highlightFlags_, curAddress_, 1);
	char text[512];
	snprintf(text, sizeof(text), "%08x", curAddress_);
	// There should only be one.
	for (MemBlockInfo info : memRangeInfo) {
		snprintf(text, sizeof(text), "%08x - %s %08x-%08x (PC %08x / %lld ticks)", curAddress_, info.tag.c_str(), info.start, info.start + info.size, info.pc, (long long)info.ticks);
	}
	statusMessage_ = text;
}

void ImMemView::UpdateSelectRange(uint32_t target, GotoMode mode) {
	if (mode == GotoMode::FROM_CUR && lastSelectReset_ == 0) {
		lastSelectReset_ = curAddress_;
	}

	switch (mode) {
	case GotoMode::RESET:
		selectRangeStart_ = target;
		selectRangeEnd_ = target + 1;
		lastSelectReset_ = target;
		break;

	case GotoMode::RESET_IF_OUTSIDE:
		if (target < selectRangeStart_ || target >= selectRangeEnd_) {
			selectRangeStart_ = target;
			selectRangeEnd_ = target + 1;
			lastSelectReset_ = target;
		}
		break;

	case GotoMode::FROM_CUR:
		selectRangeStart_ = lastSelectReset_ > target ? target : lastSelectReset_;
		selectRangeEnd_ = selectRangeStart_ == lastSelectReset_ ? target + 1 : lastSelectReset_ + 1;
		break;

	case GotoMode::EXTEND:
		if (target < selectRangeStart_)
			selectRangeStart_ = target;
		if (target > selectRangeEnd_)
			selectRangeEnd_ = target;
		break;
	}
	curAddress_ = target;
}

void ImMemView::GotoPoint(int x, int y, GotoMode mode) {
	int line = y / rowHeight_;
	int lineAddress = windowStart_ + line * rowSize_;

	if (displayOffsetScale_) {
		// ignore clicks on the offset space
		if (line < offsetSpace) {
			updateStatusBarText();
			return;
		}
		// since each row has been written X rows down from where the window expected it to be written the target of the clicks must be adjusted
		lineAddress -= rowSize_ * offsetSpace;
	}

	uint32_t target = curAddress_;
	uint32_t targetNibble = selectedNibble_;
	bool targetAscii = asciiSelected_;
	if (x >= asciiStartX_) {
		int col = (x - asciiStartX_) / (charWidth_ + 2);
		if (col >= rowSize_)
			return;

		targetAscii = true;
		target = lineAddress + col;
		targetNibble = 0;
	} else if (x >= hexStartX_) {
		int col = (x - hexStartX_) / charWidth_;
		if ((col / 3) >= rowSize_)
			return;

		switch (col % 3) {
		case 0: targetNibble = 0; break;
		case 1: targetNibble = 1; break;
		// case 2: return;		// don't change position when clicking on the space
		case 2: targetNibble = 0; break;  // TODO: split the difference? Anyway, this feels better.
		}

		targetAscii = false;
		target = lineAddress + col / 3;
	}

	if (target != curAddress_ || targetNibble != selectedNibble_ || targetAscii != asciiSelected_) {
		selectedNibble_ = targetNibble;
		asciiSelected_ = targetAscii;
		UpdateSelectRange(target, mode);
		updateStatusBarText();
	}
}

void ImMemView::gotoAddr(unsigned int addr) {
	int lines = visibleRows_;
	u32 windowEnd = windowStart_ + lines * rowSize_;

	curAddress_ = addr;
	lastSelectReset_ = curAddress_;
	selectRangeStart_ = curAddress_;
	selectRangeEnd_ = curAddress_ + 1;
	selectedNibble_ = 0;

	if (curAddress_ < windowStart_ || curAddress_ >= windowEnd) {
		windowStart_ = curAddress_ & ~15;
	}

	updateStatusBarText();
}

void ImMemView::ScrollWindow(int lines, GotoMode mode) {
	windowStart_ += lines * rowSize_;

	UpdateSelectRange(curAddress_ + lines * rowSize_, mode);

	updateStatusBarText();
}

void ImMemView::ScrollCursor(int bytes, GotoMode mode) {
	if (!asciiSelected_ && bytes == 1) {
		if (selectedNibble_ == 0) {
			selectedNibble_ = 1;
			bytes = 0;
		} else {
			selectedNibble_ = 0;
		}
	} else if (!asciiSelected_ && bytes == -1) {
		if (selectedNibble_ == 0) {
			selectedNibble_ = 1;
		} else {
			selectedNibble_ = 0;
			bytes = 0;
		}
	}

	UpdateSelectRange(curAddress_ + bytes, mode);

	u32 windowEnd = windowStart_ + visibleRows_ * rowSize_;
	if (curAddress_ < windowStart_) {
		windowStart_ = curAddress_ & ~15;
	} else if (curAddress_ >= windowEnd) {
		windowStart_ = (curAddress_ - (visibleRows_ - 1) * rowSize_) & ~15;
	}

	updateStatusBarText();
}


bool ImMemView::ParseSearchString(const char* query, MemorySearchType mode) {
	memSearch_.data.clear();
	switch (mode) {
		case FLOAT_32:{
			float flt = std::strtof(query, nullptr);
			uint32_t* tmp = (uint32_t*) &flt;
			for (int i = 0; i < 4; i++) {
				memSearch_.data.push_back((uint8_t)(*tmp & 0xff));
				*tmp >>= 8;
			}
		}
			break;
		case BITS_8:
		case BITS_16:
		case BITS_32:
		case BITS_64:{
			long long ll = std::strtoll(query, nullptr, 0);
			int bytes = 1 << mode;
			for (int i = 0; i < bytes; i++) {
				memSearch_.data.push_back((uint8_t)(ll & 0xff));
				ll >>= 8;
			}
		}
			break;
		case STRING:{
			while(*query != 0) {
				memSearch_.data.push_back(*query);
				query++;
			}
		}
			break;
		case STRING_16:{
			// for now limited to ascii.
			while(*query != 0) {
				char c = *query;
				if (c > 0x1f && c < 0x7f) {
					memSearch_.data.push_back(*query);
					memSearch_.data.push_back(0);
				}
				query++;
			}
		}break;
		case BYTE_SEQ:{
			char* s = strdup(query);
			size_t len = strlen(s);
			for (size_t index = 0; index < len; ) {
				if (isspace(s[index])) {
					index++;
					continue;
				}
				u8 value = 0;
				for (int i = 0; i < 2 && index < len; i++) {
					char c = tolower(s[index++]);
					if (c >= 'a' && c <= 'f') {
						value |= (c - 'a' + 10) << (1 - i) * 4;
					} else  if (c >= '0' && c <= '9') {
						value |= (c - '0') << (1 - i) * 4;
					} else {
						return false;
					}
				}
				memSearch_.data.push_back(value);
			}
			free(s);
		}
			break;
		default:
			break;
	}

	return true;
}
/*
std::vector<u32> ImMemView::searchString(const std::string &searchQuery) {
	std::vector<u32> searchResAddrs;

	std::vector<u8> searchData;
	if (!ParseSearchString(searchQuery, false, searchData))
		return searchResAddrs;

	if (searchData.empty())
		return searchResAddrs;

	std::vector<std::pair<u32, u32>> memoryAreas;
	memoryAreas.emplace_back(PSP_GetScratchpadMemoryBase(), PSP_GetScratchpadMemoryEnd());
	// Ignore the video memory mirrors.
	memoryAreas.emplace_back(PSP_GetVidMemBase(), 0x04200000);
	memoryAreas.emplace_back(PSP_GetKernelMemoryBase(), PSP_GetUserMemoryEnd());

	for (const auto &area : memoryAreas) {
		const u32 segmentStart = area.first;
		const u32 segmentEnd = area.second - (u32)searchData.size();

		for (u32 pos = segmentStart; pos < segmentEnd; pos++) {
			if ((pos % 256) == 0 && ImGui::IsKeyDown(ImGuiKey_Escape)) {
				return searchResAddrs;
			}

			const u8 *ptr = Memory::GetPointerUnchecked(pos);
			if (memcmp(ptr, searchData.data(), searchData.size()) == 0) {
				searchResAddrs.push_back(pos);
			}
		}
	}

	return searchResAddrs;
};
*/
MemorySearchStatus ImMemView::search(bool continueSearch) {
	if (!PSP_IsInited())
		return SEARCH_PSP_NOT_INIT;
	if (continueSearch == false /*|| searchQuery_.empty()*/) {
		memSearch_.searchAddress = curAddress_ + 1;
	} else {
		memSearch_.searchAddress = memSearch_.matchAddress + 1;
	}

	std::vector<std::pair<u32, u32>> memoryAreas;
	// Ignore the video memory mirrors.
	memoryAreas.emplace_back(PSP_GetVidMemBase(), 0x04200000);
	memoryAreas.emplace_back(PSP_GetKernelMemoryBase(), PSP_GetUserMemoryEnd());
	memoryAreas.emplace_back(PSP_GetScratchpadMemoryBase(), PSP_GetScratchpadMemoryEnd());

	// NOTE:
	// this currently stops at the first found value
	// we could look for all matches
	// and put them in a matches list
	// and display as;
	// <match idx><memory region><addr><what matched>
	const u8 *data = memSearch_.data.data();
	const size_t size = memSearch_.data.size();

	for (size_t i = 0; i < memoryAreas.size(); i++) {
		memSearch_.segmentStart = memoryAreas[i].first;
		memSearch_.segmentEnd = memoryAreas[i].second;

		// better safe than sorry, I guess
		if (!Memory::IsValidAddress(memSearch_.segmentStart))
			continue;
		const u8 *dataPointer = Memory::GetPointerUnchecked(memSearch_.segmentStart);

		if (memSearch_.searchAddress < memSearch_.segmentStart)
			memSearch_.searchAddress = memSearch_.segmentStart;
		if (memSearch_.searchAddress >= memSearch_.segmentEnd)
			continue;

		int index = memSearch_.searchAddress - memSearch_.segmentStart;
		int endIndex = memSearch_.segmentEnd - memSearch_.segmentStart - (int)size;
		while (index < endIndex) {
			if (memcmp(&dataPointer[index], data, size) == 0) {
				memSearch_.matchAddress = index + memSearch_.segmentStart;
				gotoAddr(memSearch_.matchAddress);
				return SEARCH_OK;
			}
			index++;
		}
	}

	statusMessage_ = "Not found";
	return SEARCH_NOTFOUND;
}

// for now type as int
void ImMemView::initSearch(const char* str, MemorySearchType type) {
		memSearch_.status = SEARCH_INITIAL;
		ParseSearchString(str, type);
		memSearch_.status = search(false);
}
void ImMemView::continueSearch() {
		memSearch_.status = SEARCH_INITIAL;
		memSearch_.status = search(true);

}
void ImMemView::drawOffsetScale(ImDrawList *drawList) {
	int currentX = addressStartX_;

	drawList->AddText(ImVec2(currentX, offsetPositionY_), 0xFF600000, "Offset");
	// SetTextColor(hdc, 0x600000);
	// TextOutA(hdc, currentX, offsetPositionY_, "Offset", 6);

	// the start offset, the size of the hex addresses and one space
	currentX = addressStartX_ + ((8 + 1) * charWidth_);

	char temp[64];
	for (int i = 0; i < 16; i++) {
		snprintf(temp, sizeof(temp), "%02X", i);
		drawList->AddText(ImVec2(currentX, offsetPositionY_), 0xFF600000, temp);
		currentX += 3 * charWidth_; // hex and space
	}
}

void ImMemView::toggleOffsetScale(CommonToggles toggle) {
	if (toggle == On)
		displayOffsetScale_ = true;
	else if (toggle == Off)
		displayOffsetScale_ = false;

	updateStatusBarText();
}

void ImMemView::setHighlightType(MemBlockFlags flags) {
	if (highlightFlags_ != flags) {
		highlightFlags_ = flags;
		updateStatusBarText();
	}
}

void ImMemView::toggleDrawZeroDark(bool toggle) {
	drawZeroDark_ = toggle;
}

MemorySearchStatus ImMemView::SearchStatus() {
	return memSearch_.status;
}
uint32_t ImMemView::SearchMatchAddress() {
	return memSearch_.matchAddress;
}
bool ImMemView::SearchEmpty() {
	return memSearch_.data.empty();
}

void ImMemDumpWindow::Draw(ImConfig &cfg, MIPSDebugInterface *debug) {
	ImGui::SetNextWindowSize(ImVec2(200, 300), ImGuiCond_FirstUseEver);

	if (!ImGui::Begin(Title(), &cfg.memDumpOpen)) {
		ImGui::End();
		return;
	}

	if (ImGui::Button("User RAM (0x08800000)")) {
		address_ = 0x08800000;
		size_ = 0x01800000;  // 24MB
	}

	ImGui::InputScalar("Starting address", ImGuiDataType_U32, &address_, NULL, NULL, "%08X");
	ImGui::InputScalar("Size", ImGuiDataType_U32, &size_, NULL, NULL, "%08X");

	ImGui::InputText("Filename", filename_, ARRAY_SIZE(filename_));

	const char* modes[] = { "Raw", "Disassembly" };
	int modeIndex = static_cast<int>(mode_);
	if (ImGui::Combo("Memory Dump Mode", &modeIndex, modes, IM_ARRAYSIZE(modes))) {
		// Update the current mode if the user selects a new one
		mode_ = static_cast<MemDumpMode>(modeIndex);
	}

	if (ImGui::Button(mode_ == MemDumpMode::Raw ? "Dump to file" : "Disassemble to file")) {
		uint32_t validSize = Memory::ClampValidSizeAt(address_, size_);
		if (validSize != size_) {
			errorMsg_ = "Address range out of bounds";
			if (Memory::IsValidAddress(address_)) {
				size_ = validSize;
			}
		} else if (strlen(filename_) == 0) {
			errorMsg_ = "Please specify a valid filename";
		} else {
			FILE *file = File::OpenCFile(Path(filename_), "wb");
			if (!file) {
				errorMsg_ = "Couldn't open file for writing";
			} else {
				if (mode_ == MemDumpMode::Raw) {
					const uint8_t *ptr = Memory::GetPointer(address_);
					fwrite(ptr, 1, size_, file);
				} else {
					std::string disassembly = DisassembleRange(address_, size_, true, debug);
					fprintf(file, "%s", disassembly.c_str());
				}
				errorMsg_.clear();
				fclose(file);
			}
		}
	}

	if (!errorMsg_.empty()) {
		ImGui::TextUnformatted(errorMsg_.data(), errorMsg_.data() + errorMsg_.size());
	}

	ImGui::End();
}

#define OPEN_SEARCH_FORM() \
	if (!(searchFormFlags_ & ImGuiTreeNodeFlags_DefaultOpen)) \
		searchFormFlags_ ^= ImGuiTreeNodeFlags_DefaultOpen; \
	focusSearchValueInput_ = true

void ImMemWindow::ProcessKeyboardShortcuts() {
	if (ImGui::IsKeyPressed(ImGuiKey_F)) {
		OPEN_SEARCH_FORM();
		selectedSearchType_ = STRING;
		return;
	}

	if (ImGui::IsKeyPressed(ImGuiKey_B)) {
		OPEN_SEARCH_FORM();
		selectedSearchType_ = BITS_8;
		// XXX: ctrl+b already restarts the emulation
		// SDL/SDLMain.cpp
		return;
	}

	if (ImGui::IsKeyPressed(ImGuiKey_H)) {
		OPEN_SEARCH_FORM();
		selectedSearchType_ = BITS_16;
		return;
	}
	if (ImGui::IsKeyPressed(ImGuiKey_U)) {
		OPEN_SEARCH_FORM();
		selectedSearchType_ = BITS_32;
		return;
	}
	if (ImGui::IsKeyPressed(ImGuiKey_X)) {
		OPEN_SEARCH_FORM();
		selectedSearchType_ = BYTE_SEQ;
		return;
	}
	if (ImGui::IsKeyPressed(ImGuiKey_N)) {
		memView_.continueSearch();
		return;
	}
}

void ImMemWindow::Draw(MIPSDebugInterface *mipsDebug, ImConfig &cfg, ImControl &control, int index) {
	ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver);
	if (!ImGui::Begin(Title(index), &cfg.memViewOpen[index])) {
		ImGui::End();
		return;
	}

	// Toolbars

	ImGui::InputScalar("Go to addr: ", ImGuiDataType_U32, &gotoAddr_, NULL, NULL, "%08X");
	if (ImGui::IsItemDeactivatedAfterEdit()) {
		memView_.gotoAddr(gotoAddr_);
	}
	ImGui::SameLine();
	if (ImGui::SmallButton("Go")) {
		memView_.gotoAddr(gotoAddr_);
	}
	ImGui::SameLine();
	if (ImGui::SmallButton("Settings")) {
		ImGui::OpenPopup("disSettings");
	}
	if (ImGui::BeginPopup("disSettings")) {
		if (ImGui::Checkbox("Darken Zeros", &drawZeroDark_)) {
			memView_.toggleDrawZeroDark(drawZeroDark_);
		}
		if (ImGui::Checkbox("Edit Memory", &editableMemory_)) {
			memView_.toggleEditableMemory(editableMemory_);
		}
		ImGui::EndPopup();
	}
	ImGui::Separator();
	if (ImGui::CollapsingHeader("Memory Search", searchFormFlags_)) {
		ImGui::Combo("type", reinterpret_cast<int*>(&selectedSearchType_), searchtypes, IM_ARRAYSIZE(searchtypes));

		if (focusSearchValueInput_) {
			ImGui::SetKeyboardFocusHere(0);
			focusSearchValueInput_ = false;
		}
		if (ImGui::InputText("data", searchStr_, IM_ARRAYSIZE(searchStr_), ImGuiInputTextFlags_EnterReturnsTrue) || ImGui::Button("Search")) {
			memView_.initSearch(searchStr_, selectedSearchType_);
			// TODO: transfer focus on the memview.
		}
		ImGui::SameLine();

		bool isEmpty = memView_.SearchEmpty();
		if (isEmpty)
			ImGui::BeginDisabled(true);
		if (ImGui::Button("Next")) {
			memView_.continueSearch();
		}
		if (isEmpty)
			ImGui::EndDisabled();
		int searchStatus = memView_.SearchStatus();
		if (searchStatus != 0)
			ImGui::SameLine();
		switch (searchStatus) {
			case SEARCH_OK:
				ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "match found 0x%x", memView_.SearchMatchAddress());
				break;
			case SEARCH_NOTFOUND:
				ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "no match found");
				break;
			default:
				break;
		}
	}
	ImGui::Separator();

	ImVec2 size(0, -ImGui::GetFrameHeightWithSpacing());

	auto node = [&](const char *title, uint32_t start, uint32_t len) {
		if (ImGui::TreeNode(title)) {
			if (ImGui::Selectable("(start)", cfg.selectedMemoryBlock == start)) {
				cfg.selectedMemoryBlock = start;
				GotoAddr(start);
			}
			const std::vector<MemBlockInfo> info = FindMemInfo(start, len);
			for (auto &iter : info) {
				ImGui::PushID(iter.start);
				if (ImGui::Selectable(iter.tag.c_str(), cfg.selectedMemoryBlock == iter.start)) {
					cfg.selectedMemoryBlock = iter.start;
					GotoAddr(iter.start);
				}
				ImGui::PopID();
			}
			const u32 end = start + len;
			if (ImGui::Selectable("(end)", cfg.selectedMemoryBlock == end)) {
				cfg.selectedMemoryBlock = end;
				GotoAddr(end);
			}
			ImGui::TreePop();
		}
	};

	// Main views - list of interesting addresses to the left, memory view to the right.
	if (ImGui::BeginChild("addr_list", ImVec2(200.0f, size.y), ImGuiChildFlags_ResizeX)) {
		node("Scratch", 0x00010000, 0x00004000);
		node("Kernel RAM", 0x08000000, 0x00800000);
		node("User RAM", 0x08800000, 0x01800000);
		node("VRAM", 0x04000000, 0x00200000);
	}

	ImGui::EndChild();

	ImGui::SameLine();

	if (ImGui::BeginChild("memview", size)) {
		memView_.Draw(ImGui::GetWindowDrawList());
	}
	ImGui::EndChild();

	StatusBar(memView_.StatusMessage());

	ImGui::End();
	if (!ImGui::GetIO().WantTextInput) {
		ProcessKeyboardShortcuts();
	}
}

const char *ImMemWindow::Title(int index) {
	static const char *const titles[4] = { "Memory 1", "Memory 2", "Memory 3", "Memory 4" };
	return titles[index];
}
