#include "instruction_editor_dialog.h"

#include "Emu/Cell/SPUThread.h"
#include "Emu/CPU/CPUThread.h"
#include "Emu/CPU/CPUDisAsm.h"
#include "Emu/Cell/lv2/sys_spu.h"

#include <QFontDatabase>
#include <QVBoxLayout>
#include <QMessageBox>
#include <QPushButton>
#include <QCheckBox>

constexpr auto qstr = QString::fromStdString;

extern bool ppu_patch(u32 addr, u32 value);

extern std::string format_spu_func_info(u32 addr, cpu_thread* spu);

instruction_editor_dialog::instruction_editor_dialog(QWidget *parent, u32 _pc, CPUDisAsm* _disasm, std::function<cpu_thread*()> func)
	: QDialog(parent)
	, m_pc(_pc)
	, m_disasm(_disasm->copy_type_erased())
	, m_get_cpu(std::move(func))
{
	setWindowTitle(tr("Edit instruction"));
	setAttribute(Qt::WA_DeleteOnClose);
	setMinimumSize(300, sizeHint().height());

	const auto cpu = m_get_cpu();

	m_cpu_offset = cpu && cpu->id_type() == 2 ? static_cast<spu_thread&>(*cpu).ls : vm::g_sudo_addr;

	QVBoxLayout* vbox_panel(new QVBoxLayout());
	QHBoxLayout* hbox_panel(new QHBoxLayout());
	QVBoxLayout* vbox_left_panel(new QVBoxLayout());
	QVBoxLayout* vbox_right_panel(new QVBoxLayout());
	QHBoxLayout* hbox_b_panel(new QHBoxLayout());

	QPushButton* button_ok(new QPushButton(tr("OK")));
	QPushButton* button_cancel(new QPushButton(tr("Cancel")));
	button_ok->setFixedWidth(80);
	button_cancel->setFixedWidth(80);

	m_instr = new QLineEdit(this);
	m_instr->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
	m_instr->setMaxLength(8);
	m_instr->setMaximumWidth(65);

	m_disasm->change_mode(cpu_disasm_mode::normal);
	m_disasm->disasm(m_pc);
	m_preview = new QLabel(qstr(m_disasm->last_opcode), this);

	// Layouts
	vbox_left_panel->addWidget(new QLabel(tr("Address:     ")));
	vbox_left_panel->addWidget(new QLabel(tr("Instruction: ")));
	vbox_left_panel->addWidget(new QLabel(tr("Preview:     ")));

	vbox_right_panel->addWidget(new QLabel(qstr(fmt::format("%08x", m_pc))));
	vbox_right_panel->addWidget(m_instr);
	vbox_right_panel->addWidget(m_preview);

	if (cpu && cpu->id_type() == 2)
	{
		// Print block information as if this instruction is its beginning
		vbox_left_panel->addWidget(new QLabel(tr("Block Info:  ")));
		m_func_info = new QLabel("", this);
		vbox_right_panel->addWidget(m_func_info);

		m_func_info->setText(qstr(format_spu_func_info(m_pc, cpu)));
	}

	if (cpu && cpu->id_type() == 2)
	{
		const auto& spu = static_cast<spu_thread&>(*cpu);

		if (spu.group && spu.group->max_num > 1)
		{
			m_apply_for_spu_group = new QCheckBox(tr("For SPUs Group"));
			vbox_left_panel->addWidget(m_apply_for_spu_group);
			vbox_right_panel->addWidget(new QLabel("")); // For alignment
		}
	}

	vbox_right_panel->setAlignment(Qt::AlignLeft);

	hbox_b_panel->addWidget(button_ok);
	hbox_b_panel->addWidget(button_cancel);
	hbox_b_panel->setAlignment(Qt::AlignCenter);

	// Main Layout
	hbox_panel->addLayout(vbox_left_panel);
	hbox_panel->addSpacing(10);
	hbox_panel->addLayout(vbox_right_panel);
	vbox_panel->addLayout(hbox_panel);
	vbox_panel->addSpacing(10);
	vbox_panel->addLayout(hbox_b_panel);
	setLayout(vbox_panel);

	// Events
	connect(button_ok, &QAbstractButton::clicked, [this]()
	{
		const auto cpu = m_get_cpu();

		if (!cpu)
		{
			close();
			return;
		}

		bool ok;
		const ulong opcode = m_instr->text().toULong(&ok, 16);
		if (!ok || opcode > u32{umax})
		{
			QMessageBox::critical(this, tr("Error"), tr("Failed to parse PPU instruction."));
			return;
		}

		const be_t<u32> swapped{static_cast<u32>(opcode)};

		if (cpu->id_type() == 1)
		{
			if (!ppu_patch(m_pc, static_cast<u32>(opcode)))
			{
				QMessageBox::critical(this, tr("Error"), tr("Failed to patch PPU instruction."));
				return;
			}
		}
		else if (m_apply_for_spu_group && m_apply_for_spu_group->isChecked())
		{
			for (auto& spu : static_cast<spu_thread&>(*cpu).group->threads)
			{
				if (spu)
				{
					std::memcpy(spu->ls + m_pc, &swapped, 4);
				}
			}
		}
		else
		{
			std::memcpy(m_cpu_offset + m_pc, &swapped, 4);
		}

		accept();
	});
	connect(button_cancel, &QAbstractButton::clicked, this, &instruction_editor_dialog::reject);
	connect(m_instr, &QLineEdit::textChanged, this, &instruction_editor_dialog::updatePreview);

	updatePreview();
}

void instruction_editor_dialog::updatePreview() const
{
	bool ok;
	const be_t<u32> opcode{static_cast<u32>(m_instr->text().toULong(&ok, 16))};
	m_disasm->change_ptr(reinterpret_cast<const u8*>(&opcode) - std::intptr_t{m_pc});

	if (ok && m_disasm->disasm(m_pc))
	{
		m_preview->setText(qstr(m_disasm->last_opcode));
	}
	else
	{
		m_preview->setText(tr("Could not parse instruction."));
	}
}
