#include <algorithm>

#include "UI/JitCompareScreen.h"

#include "Core/MemMap.h"
#include "Core/MIPS/MIPSTables.h"
#include "Core/MIPS/JitCommon/JitBlockCache.h"
#include "Core/MIPS/JitCommon/JitCommon.h"
#include "Core/MIPS/JitCommon/JitState.h"

JitCompareScreen::JitCompareScreen() : UIDialogScreenWithBackground() {
	if (!MIPSComp::jit) {
		return;
	}
	JitBlockCacheDebugInterface *blockCacheDebug = MIPSComp::jit->GetBlockCacheDebugInterface();
	// The only defaults that make sense.
	if (blockCacheDebug->SupportsProfiling()) {
		listSort_ = ListSort::TIME_SPENT;
	} else {
		listSort_ = ListSort::BLOCK_LENGTH_DESC;
	}
	FillBlockList();
}

void JitCompareScreen::Flip() {
	using namespace UI;
	// If we add more, let's convert to a for loop.
	switch (viewMode_) {
	case ViewMode::DISASM:
		comparisonView_->SetVisibility(V_VISIBLE);
		blockListView_->SetVisibility(V_GONE);
		statsView_->SetVisibility(V_GONE);
		break;
	case ViewMode::BLOCK_LIST:
		comparisonView_->SetVisibility(V_GONE);
		blockListView_->SetVisibility(V_VISIBLE);
		statsView_->SetVisibility(V_GONE);
		break;
	case ViewMode::STATS:
		comparisonView_->SetVisibility(V_GONE);
		blockListView_->SetVisibility(V_GONE);
		statsView_->SetVisibility(V_VISIBLE);
		break;
	}
}

// Three panes: Block chooser, MIPS view, ARM/x86 view
void JitCompareScreen::CreateViews() {
	auto di = GetI18NCategory(I18NCat::DIALOG);
	auto dev = GetI18NCategory(I18NCat::DEVELOPER);

	using namespace UI;

	root_ = new LinearLayout(ORIENT_HORIZONTAL);

	ScrollView *leftColumnScroll = root_->Add(new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(200, FILL_PARENT)));
	LinearLayout *leftColumn = leftColumnScroll->Add(new LinearLayout(ORIENT_VERTICAL));

	comparisonView_ = root_->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0f)));
	comparisonView_->SetVisibility(V_VISIBLE);
	LinearLayout *blockTopBar = comparisonView_->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
	blockTopBar->Add(new Button("", ImageID("I_ARROW_UP")))->OnClick.Add([this](UI::EventParams &e) {
		viewMode_ = ViewMode::BLOCK_LIST;
		Flip();
		return UI::EVENT_DONE;
	});
	blockTopBar->Add(new Button("", ImageID("I_ARROW_LEFT")))->OnClick.Add([=](UI::EventParams &e) {
		if (currentBlock_ >= 1)
			currentBlock_--;
		UpdateDisasm();
		return UI::EVENT_DONE;
	});
	blockTopBar->Add(new Button("", ImageID("I_ARROW_RIGHT")))->OnClick.Add([=](UI::EventParams &e) {
		if (currentBlock_ < blockList_.size() - 1)
			currentBlock_++;
		UpdateDisasm();
		return UI::EVENT_DONE;
	});
	blockTopBar->Add(new Button(dev->T("Random")))->OnClick.Add([=](UI::EventParams &e) {
		if (blockList_.empty()) {
			return UI::EVENT_DONE;
		}
		currentBlock_ = rand() % blockList_.size();
		UpdateDisasm();
		return UI::EVENT_DONE;
	});

	blockAddr_ = blockTopBar->Add(new TextEdit("", dev->T("Block address"), ""));
	blockAddr_->OnEnter.Handle(this, &JitCompareScreen::OnAddressChange);
	blockName_ = blockTopBar->Add(new TextView(dev->T("No block")));
	blockStats_ = blockTopBar->Add(new TextView(""));

	LinearLayout *columns = comparisonView_->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(1.0f)));

	ScrollView *midColumnScroll = columns->Add(new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0f)));
	LinearLayout *midColumn = midColumnScroll->Add(new LinearLayout(ORIENT_VERTICAL));
	midColumn->SetTag("JitCompareLeftDisasm");
	leftDisasm_ = midColumn->Add(new LinearLayout(ORIENT_VERTICAL));
	leftDisasm_->SetSpacing(0.0f);

	ScrollView *rightColumnScroll = columns->Add(new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0f)));
	rightColumnScroll->SetTag("JitCompareRightDisasm");
	LinearLayout *rightColumn = rightColumnScroll->Add(new LinearLayout(ORIENT_VERTICAL));
	rightDisasm_ = rightColumn->Add(new LinearLayout(ORIENT_VERTICAL));
	rightDisasm_->SetSpacing(0.0f);

	blockListView_ = root_->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0f)));
	blockListView_->SetVisibility(V_GONE);

	// Should match the ListSort enum
	static ContextMenuItem sortMenu[] = {
		{ "Block number", "I_ARROW_UP" },
		{ "Block length", "I_ARROW_DOWN" },
		{ "Block length", "I_ARROW_UP" },
		{ "Time spent", "I_ARROW_DOWN" },
		{ "Executions", "I_ARROW_DOWN" },
	};
	int sortCount = ARRAY_SIZE(sortMenu);
	if (MIPSComp::jit) {
		JitBlockCacheDebugInterface *blockCacheDebug = MIPSComp::jit->GetBlockCacheDebugInterface();
		if (!blockCacheDebug->SupportsProfiling()) {
			sortCount -= 2;
		}
	}

	LinearLayout *listTopBar = blockListView_->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
	Button *sortButton = new Button(dev->T("Sort..."));
	listTopBar->Add(sortButton)->OnClick.Add([this, sortButton, sortCount](UI::EventParams &e) {
		PopupContextMenuScreen *contextMenu = new UI::PopupContextMenuScreen(sortMenu, sortCount, I18NCat::DEVELOPER, sortButton);
		screenManager()->push(contextMenu);
		contextMenu->OnChoice.Add([=](EventParams &e) -> UI::EventReturn {
			if (e.a < (int)ListSort::MAX) {
				listSort_ = (ListSort)e.a;
				UpdateDisasm();
			}
			return UI::EVENT_DONE;
		});
		return UI::EVENT_DONE;
	});

	ScrollView *blockScroll = blockListView_->Add(new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0f)));
	blockListContainer_ = blockScroll->Add(new LinearLayout(ORIENT_VERTICAL));

	statsView_ = root_->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(1.0f)));
	statsView_->SetVisibility(V_GONE);

	LinearLayout *statsTopBar = statsView_->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));
	ScrollView *statsScroll = statsView_->Add(new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(1.0f)));
	statsContainer_ = statsScroll->Add(new LinearLayout(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT)));

	// leftColumn->Add(new Choice(dev->T("By Address")))->OnClick.Handle(this, &JitCompareScreen::OnSelectBlock);
	leftColumn->Add(new Choice(dev->T("All")))->OnClick.Add([=](UI::EventParams &e) {
		listType_ = ListType::ALL_BLOCKS;
		viewMode_ = ViewMode::BLOCK_LIST;
		UpdateDisasm();
		return UI::EVENT_DONE;
	});
	leftColumn->Add(new Choice(dev->T("FPU")))->OnClick.Add([=](UI::EventParams &e) {
		listType_ = ListType::FPU_BLOCKS;
		viewMode_ = ViewMode::BLOCK_LIST;
		UpdateDisasm();
		return UI::EVENT_DONE;
	});
	leftColumn->Add(new Choice(dev->T("VFPU")))->OnClick.Add([=](UI::EventParams &e) {
		listType_ = ListType::VFPU_BLOCKS;
		viewMode_ = ViewMode::BLOCK_LIST;
		UpdateDisasm();
		return UI::EVENT_DONE;
	});

	leftColumn->Add(new Choice(dev->T("Stats")))->OnClick.Handle(this, &JitCompareScreen::OnShowStats);
	leftColumn->Add(new Choice(di->T("Back")))->OnClick.Handle<UIScreen>(this, &UIScreen::OnBack);
	UpdateDisasm();
}

void JitCompareScreen::FillBlockList() {
	JitBlockCacheDebugInterface *blockCacheDebug = MIPSComp::jit->GetBlockCacheDebugInterface();
	blockList_.clear();
	int64_t sumTotalNanos = 0;
	int64_t sumExecutions = 0;
	bool profiling = blockCacheDebug->SupportsProfiling();
	for (int i = 0; i < blockCacheDebug->GetNumBlocks(); i++) {
		if (!blockCacheDebug->IsValidBlock(i)) {
			continue;
		}

		switch (listType_) {
		case ListType::ALL_BLOCKS:
			blockList_.push_back(i);
			break;
		case ListType::FPU_BLOCKS:
		case ListType::VFPU_BLOCKS:
		{
			const uint64_t flags = listType_ == ListType::FPU_BLOCKS ? IS_FPU : IS_VFPU;
			// const uint64_t antiFlags = IS_SYSCALL;
			const uint64_t antiFlags = 0;
			JitBlockMeta meta = blockCacheDebug->GetBlockMeta(i);
			if (meta.valid) {
				for (u32 addr = meta.addr; addr < meta.addr + meta.sizeInBytes; addr += 4) {
					MIPSOpcode opcode = Memory::Read_Instruction(addr);
					MIPSInfo info = MIPSGetInfo(opcode);
					if ((info & flags) && !(info & antiFlags)) {
						blockList_.push_back(i);
						break;
					}
				}
			}
		}
		default:
			break;
		}

		if (profiling) {
			JitBlockProfileStats stats = blockCacheDebug->GetBlockProfileStats(i);
			sumTotalNanos += stats.totalNanos;
			sumExecutions += stats.executions;
		}
	}

	sumTotalNanos_ = sumTotalNanos;
	sumExecutions_ = sumExecutions;

	if (listSort_ == ListSort::BLOCK_NUM) {
		// Already sorted, effectively.
		return;
	}

	std::sort(blockList_.begin(), blockList_.end(), [=](const int &a_index, const int &b_index) {
		// First, check metadata sorts.
		switch (listSort_) {
		case ListSort::BLOCK_LENGTH_DESC:
		{
			JitBlockMeta a_meta = blockCacheDebug->GetBlockMeta(a_index);
			JitBlockMeta b_meta = blockCacheDebug->GetBlockMeta(b_index);
			return a_meta.sizeInBytes > b_meta.sizeInBytes;  // reverse for descending
		}
		case ListSort::BLOCK_LENGTH_ASC:
		{
			JitBlockMeta a_meta = blockCacheDebug->GetBlockMeta(a_index);
			JitBlockMeta b_meta = blockCacheDebug->GetBlockMeta(b_index);
			return a_meta.sizeInBytes < b_meta.sizeInBytes;
		}
		default:
			break;
		}
		JitBlockProfileStats a_stats = blockCacheDebug->GetBlockProfileStats(a_index);
		JitBlockProfileStats b_stats = blockCacheDebug->GetBlockProfileStats(b_index);
		switch (listSort_) {
		case ListSort::EXECUTIONS:
			return a_stats.executions > b_stats.executions;
		case ListSort::TIME_SPENT:
			return a_stats.totalNanos > b_stats.totalNanos;
		default:
			return false;
		}
	});
}

void JitCompareScreen::UpdateDisasm() {
	leftDisasm_->Clear();
	rightDisasm_->Clear();

	using namespace UI;

	if (!MIPSComp::jit) {
		return;
	}

	JitBlockCacheDebugInterface *blockCacheDebug = MIPSComp::jit->GetBlockCacheDebugInterface();
	if (viewMode_ == ViewMode::DISASM && (currentBlock_ < 0 || currentBlock_ >= (int)blockList_.size())) {
		viewMode_ = ViewMode::BLOCK_LIST;
	}

	FillBlockList();
	Flip();

	if (viewMode_ == ViewMode::DISASM) {
		char temp[256];
		snprintf(temp, sizeof(temp), "%d/%d", currentBlock_, (int)blockList_.size());
		blockName_->SetText(temp);

		int blockNum = blockList_[currentBlock_];

		if (!blockCacheDebug->IsValidBlock(blockNum)) {
			auto dev = GetI18NCategory(I18NCat::DEVELOPER);
			leftDisasm_->Add(new TextView(dev->T("No block")));
			rightDisasm_->Add(new TextView(dev->T("No block")));
			blockStats_->SetText("");
			return;
		}

		JitBlockDebugInfo debugInfo = blockCacheDebug->GetBlockDebugInfo(blockNum);
		snprintf(temp, sizeof(temp), "%08x", debugInfo.originalAddress);
		blockAddr_->SetText(temp);

		// Alright. First generate the MIPS disassembly.

		// TODO: Need a way to communicate branch continuing.
		for (const auto &line : debugInfo.origDisasm) {
			leftDisasm_->Add(new TextView(line, FLAG_DYNAMIC_ASCII, false))->SetFocusable(true);
		}

		// TODO : When we have both target and IR, need a third column.
		if (debugInfo.targetDisasm.size()) {
			for (const auto &line : debugInfo.targetDisasm) {
				rightDisasm_->Add(new TextView(line, FLAG_DYNAMIC_ASCII, false))->SetFocusable(true);
			}
		} else {
			for (const auto &line : debugInfo.irDisasm) {
				rightDisasm_->Add(new TextView(line, FLAG_DYNAMIC_ASCII, false))->SetFocusable(true);
			}
		}

		int numMips = leftDisasm_->GetNumSubviews();
		int numHost = rightDisasm_->GetNumSubviews();
		double bloat = 100.0 * numHost / numMips;
		if (blockCacheDebug->SupportsProfiling()) {
			JitBlockProfileStats stats = blockCacheDebug->GetBlockProfileStats(blockNum);
			int execs = (int)stats.executions;
			double us = (double)stats.totalNanos / 1000000.0;
			double percentage = 100.0 * (double)stats.totalNanos / (double)sumTotalNanos_;
			snprintf(temp, sizeof(temp), "%d runs, %0.2f ms, %0.2f%%, bloat: %0.1f%%", execs, us, percentage, bloat);
		} else {
			snprintf(temp, sizeof(temp), "bloat: %0.1f%%", bloat);
		}
		blockStats_->SetText(temp);
	} else if (viewMode_ == ViewMode::BLOCK_LIST) {
		blockListContainer_->Clear();
		bool profiling = blockCacheDebug->SupportsProfiling();
		for (int i = 0; i < std::min(100, (int)blockList_.size()); i++) {
			int blockNum = blockList_[i];
			JitBlockMeta meta = blockCacheDebug->GetBlockMeta(blockNum);
			char temp[512], small[512];
			if (profiling) {
				JitBlockProfileStats stats = blockCacheDebug->GetBlockProfileStats(blockNum);
				int execs = (int)stats.executions;
				double us = (double)stats.totalNanos / 1000000.0;
				double percentage = 100.0 * (double)stats.totalNanos / (double)sumTotalNanos_;
				snprintf(temp, sizeof(temp), "%08x: %d instrs (%d runs, %0.2f ms, %0.2f%%)", meta.addr, meta.sizeInBytes / 4, execs, us, percentage);
			} else {
				snprintf(temp, sizeof(temp), "%08x: %d instrs", meta.addr, meta.sizeInBytes / 4);
			}
			snprintf(small, sizeof(small), "Small text");
			Choice *blockChoice = blockListContainer_->Add(new Choice(temp, small));
			blockChoice->OnClick.Handle(this, &JitCompareScreen::OnBlockClick);
		}
	} else {  // viewMode_ == ViewMode::STATS
		statsContainer_->Clear();

		BlockCacheStats bcStats{};
		blockCacheDebug->ComputeStats(bcStats);

		char stats[1024];
		snprintf(stats, sizeof(stats),
			"Num blocks: %d\n"
			"Average Bloat: %0.2f%%\n"
			"Min Bloat: %0.2f%%  (%08x)\n"
			"Max Bloat: %0.2f%%  (%08x)\n",
			blockCacheDebug->GetNumBlocks(),
			100.0 * bcStats.avgBloat,
			100.0 * bcStats.minBloat, bcStats.minBloatBlock,
			100.0 * bcStats.maxBloat, bcStats.maxBloatBlock);

		statsContainer_->Add(new TextView(stats));
	}
}

UI::EventReturn JitCompareScreen::OnBlockClick(UI::EventParams &e) {
	int blockIndex = blockListContainer_->IndexOfSubview(e.v);
	if (blockIndex >= 0) {
		viewMode_ = ViewMode::DISASM;
		currentBlock_ = blockIndex;
		UpdateDisasm();
	}
	return UI::EVENT_DONE;
}

UI::EventReturn JitCompareScreen::OnAddressChange(UI::EventParams &e) {
	std::lock_guard<std::recursive_mutex> guard(MIPSComp::jitLock);
	if (!MIPSComp::jit) {
		return UI::EVENT_DONE;
	}
	JitBlockCacheDebugInterface *blockCache = MIPSComp::jit->GetBlockCacheDebugInterface();
	if (!blockCache)
		return UI::EVENT_DONE;
	u32 addr;
	if (blockAddr_->GetText().size() > 8)
		return UI::EVENT_DONE;
	if (1 == sscanf(blockAddr_->GetText().c_str(), "%08x", &addr)) {
		if (Memory::IsValidAddress(addr)) {
			currentBlock_ = blockCache->GetBlockNumberFromStartAddress(addr);
			UpdateDisasm();
		}
	}
	return UI::EVENT_DONE;
}

UI::EventReturn JitCompareScreen::OnShowStats(UI::EventParams &e) {
	std::lock_guard<std::recursive_mutex> guard(MIPSComp::jitLock);
	if (!MIPSComp::jit) {
		return UI::EVENT_DONE;
	}

	viewMode_ = ViewMode::STATS;
	UpdateDisasm();
	return UI::EVENT_DONE;
}


UI::EventReturn JitCompareScreen::OnSelectBlock(UI::EventParams &e) {
	auto dev = GetI18NCategory(I18NCat::DEVELOPER);

	auto addressPrompt = new AddressPromptScreen(dev->T("Block address"));
	addressPrompt->OnChoice.Handle(this, &JitCompareScreen::OnBlockAddress);
	screenManager()->push(addressPrompt);
	return UI::EVENT_DONE;
}

UI::EventReturn JitCompareScreen::OnBlockAddress(UI::EventParams &e) {
	std::lock_guard<std::recursive_mutex> guard(MIPSComp::jitLock);
	if (!MIPSComp::jit) {
		return UI::EVENT_DONE;
	}

	JitBlockCacheDebugInterface *blockCache = MIPSComp::jit->GetBlockCacheDebugInterface();
	if (!blockCache)
		return UI::EVENT_DONE;

	if (Memory::IsValidAddress(e.a)) {
		currentBlock_ = blockCache->GetBlockNumberFromStartAddress(e.a);
	} else {
		currentBlock_ = -1;
	}
	UpdateDisasm();
	return UI::EVENT_DONE;
}

/*
void JitCompareScreen::OnRandomBlock(int flag) {
	std::lock_guard<std::recursive_mutex> guard(MIPSComp::jitLock);
	if (!MIPSComp::jit) {
		return;
	}
	JitBlockCacheDebugInterface *blockCache = MIPSComp::jit->GetBlockCacheDebugInterface();
	if (!blockCache)
		return;

	int numBlocks = blockCache->GetNumBlocks();
	if (numBlocks > 0) {
		bool anyWanted = false;
		int tries = 0;
		while (!anyWanted && tries < numBlocks) {
			currentBlock_ = rand() % numBlocks;
			if (blockCache->IsValidBlock(currentBlock_)) {
				JitBlockDebugInfo b = blockCache->GetBlockDebugInfo(currentBlock_);
				u32 mipsBytes = (u32)b.origDisasm.size() * 4;
				for (u32 addr = b.originalAddress; addr < b.originalAddress + mipsBytes; addr += 4) {
					MIPSOpcode opcode = Memory::Read_Instruction(addr);
					if (MIPSGetInfo(opcode) & flag) {
						char temp[256];
						MIPSDisAsm(opcode, addr, temp, sizeof(temp));
						// INFO_LOG(Log::HLE, "Stopping at random instruction: %08x %s", addr, temp);
						anyWanted = true;
						break;
					}
				}
			}
			tries++;
		}

		if (!anyWanted)
			currentBlock_ = -1;
	}
	UpdateDisasm();
}*/

void AddressPromptScreen::CreatePopupContents(UI::ViewGroup *parent) {
	using namespace UI;

	auto dev = GetI18NCategory(I18NCat::DEVELOPER);

	addrView_ = new TextView(dev->T("Enter address"), ALIGN_HCENTER, false);
	parent->Add(addrView_);

	ViewGroup *grid = new GridLayout(GridLayoutSettings(60, 40));
	parent->Add(grid);

	for (int i = 0; i < 16; ++i) {
		char temp[16];
		snprintf(temp, 16, " %X ", i);
		buttons_[i] = new Button(temp);
		grid->Add(buttons_[i])->OnClick.Handle(this, &AddressPromptScreen::OnDigitButton);
	}

	parent->Add(new Button(dev->T("Backspace")))->OnClick.Handle(this, &AddressPromptScreen::OnBackspace);
}

void AddressPromptScreen::OnCompleted(DialogResult result) {
	if (result == DR_OK) {
		UI::EventParams e{};
		e.v = root_;
		e.a = addr_;
		OnChoice.Trigger(e);
	}
}

UI::EventReturn AddressPromptScreen::OnDigitButton(UI::EventParams &e) {
	for (int i = 0; i < 16; ++i) {
		if (buttons_[i] == e.v) {
			AddDigit(i);
		}
	}
	return UI::EVENT_DONE;
}

UI::EventReturn AddressPromptScreen::OnBackspace(UI::EventParams &e) {
	BackspaceDigit();
	return UI::EVENT_DONE;
}

void AddressPromptScreen::AddDigit(int n) {
	if ((addr_ & 0xF0000000) == 0) {
		addr_ = addr_ * 16 + n;
	}
	UpdatePreviewDigits();
}

void AddressPromptScreen::BackspaceDigit() {
	addr_ /= 16;
	UpdatePreviewDigits();
}

void AddressPromptScreen::UpdatePreviewDigits() {
	if (addr_ != 0) {
		char temp[32];
		snprintf(temp, 32, "%8X", addr_);
		addrView_->SetText(temp);
	} else {
		auto dev = GetI18NCategory(I18NCat::DEVELOPER);
		addrView_->SetText(dev->T("Enter address"));
	}
}

bool AddressPromptScreen::key(const KeyInput &key) {
	if (key.flags & KEY_DOWN) {
		if (key.keyCode >= NKCODE_0 && key.keyCode <= NKCODE_9) {
			AddDigit(key.keyCode - NKCODE_0);
		} else if (key.keyCode >= NKCODE_A && key.keyCode <= NKCODE_F) {
			AddDigit(10 + key.keyCode - NKCODE_A);
			// NKCODE_DEL is backspace.
		} else if (key.keyCode == NKCODE_DEL) {
			BackspaceDigit();
		} else if (key.keyCode == NKCODE_ENTER) {
			TriggerFinish(DR_OK);
		} else {
			return UIDialogScreen::key(key);
		}
	} else {
		return UIDialogScreen::key(key);
	}
	return true;
}
