/*
Copyright (C) 2015 Ruslan Kabatsayev <b7.10110111@gmail.com>

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, either version 2 of the License, or
(at your option) any later version.

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 for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "DialogEditSIMDRegister.h"
#include "FloatX.h"
#include "QLongValidator.h"
#include "QULongValidator.h"
#include "NumberEdit.h"
#include <QApplication>
#include <QDebug>
#include <QGridLayout>
#include <QLineEdit>
#include <QRadioButton>
#include <QRegExp>
#include <QRegExpValidator>
#include <QKeyEvent>
#include <cstring>
#include <limits>
#include <type_traits>

#include "EntryGridKeyUpDownEventFilter.h"

namespace ODbgRegisterView {

template <std::size_t numEntries>
void DialogEditSIMDRegister::setupEntries(const QString &label, std::array<NumberEdit *, numEntries> &entries, int row, const char *slot, int naturalWidthInChars) {

	auto contentsGrid = qobject_cast<QGridLayout *>(layout());

	contentsGrid->addWidget(new QLabel(label, this), row, ENTRIES_FIRST_COL - 1);
	for (std::size_t entryIndex = 0; entryIndex < numEntries; ++entryIndex) {
		auto &    entry         = entries[entryIndex];
		const int bytesPerEntry = numBytes / numEntries;
		entry                   = new NumberEdit(ENTRIES_FIRST_COL + bytesPerEntry * (numEntries - 1 - entryIndex), bytesPerEntry, this);
		entry->setNaturalWidthInChars(naturalWidthInChars);
		connect(entry, SIGNAL(textEdited(const QString &)), this, slot);
		entry->installEventFilter(this);
	}
}

DialogEditSIMDRegister::DialogEditSIMDRegister(QWidget *parent) : QDialog(parent),
		byteHexValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]{0,2}"), this)),
		wordHexValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]{0,4}"), this)),
		dwordHexValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]{0,8}"), this)),
		qwordHexValidator(new QRegExpValidator(QRegExp("[0-9a-fA-F]{0,16}"), this)),
		byteSignedValidator(new QLongValidator(INT8_MIN, INT8_MAX, this)),
		wordSignedValidator(new QLongValidator(INT16_MIN, INT16_MAX, this)),
		dwordSignedValidator(new QLongValidator(INT32_MIN, INT32_MAX, this)),
		qwordSignedValidator(new QLongValidator(INT64_MIN, INT64_MAX, this)),
		byteUnsignedValidator(new QULongValidator(0, UINT8_MAX, this)),
		wordUnsignedValidator(new QULongValidator(0, UINT16_MAX, this)),
		dwordUnsignedValidator(new QULongValidator(0, UINT32_MAX, this)),
		qwordUnsignedValidator(new QULongValidator(0, UINT64_MAX, this)),
		float32Validator(new FloatXValidator<float>(this)),
		float64Validator(new FloatXValidator<double>(this)),
		intMode(NumberDisplayMode::Hex) {

	setWindowTitle(tr("Edit SIMD Register"));
	setModal(true);
	const auto allContentsGrid = new QGridLayout(this);

	for (int byteIndex = 0; byteIndex < numBytes; ++byteIndex) {
		columnLabels[byteIndex] = new QLabel(std::to_string(byteIndex).c_str(), this);
		columnLabels[byteIndex]->setAlignment(Qt::AlignCenter);
		allContentsGrid->addWidget(columnLabels[byteIndex], BYTE_INDICES_ROW, ENTRIES_FIRST_COL + numBytes - 1 - byteIndex);
	}

	setupEntries(tr("Byte"), bytes, BYTES_ROW, SLOT(onByteEdited()), 4);
	setupEntries(tr("Word"), words, WORDS_ROW, SLOT(onWordEdited()), 6);
	setupEntries(tr("Doubleword"), dwords, DWORDS_ROW, SLOT(onDwordEdited()), 11);
	setupEntries(tr("Quadword"), qwords, QWORDS_ROW, SLOT(onQwordEdited()), 21);
	setupEntries(tr("float32"), floats32, FLOATS32_ROW, SLOT(onFloat32Edited()), 14);
	setupEntries(tr("float64"), floats64, FLOATS64_ROW, SLOT(onFloat64Edited()), 24);

	for (const auto &entry : floats32) {
		entry->setValidator(float32Validator);
	}

	for (const auto &entry : floats64) {
		entry->setValidator(float64Validator);
	}

	hexSignOKCancelLayout = new QHBoxLayout();

	{
		const auto hexSignRadiosLayout = new QVBoxLayout();
		radioHex                       = new QRadioButton(tr("Hexadecimal"), this);
		connect(radioHex, SIGNAL(toggled(bool)), this, SLOT(onHexToggled(bool)));
		radioHex->setChecked(true); // must be after connecting of toggled()
		hexSignRadiosLayout->addWidget(radioHex);

		radioSigned = new QRadioButton(tr("Signed"), this);
		connect(radioSigned, SIGNAL(toggled(bool)), this, SLOT(onSignedToggled(bool)));
		hexSignRadiosLayout->addWidget(radioSigned);

		radioUnsigned = new QRadioButton(tr("Unsigned"), this);
		connect(radioUnsigned, SIGNAL(toggled(bool)), this, SLOT(onUnsignedToggled(bool)));
		hexSignRadiosLayout->addWidget(radioUnsigned);

		hexSignOKCancelLayout->addLayout(hexSignRadiosLayout);
	}

	{
		const auto okCancelLayout = new QVBoxLayout();
		okCancelLayout->addItem(new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding));

		okCancel = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok, Qt::Horizontal, this);
		connect(okCancel, SIGNAL(accepted()), this, SLOT(accept()));
		connect(okCancel, SIGNAL(rejected()), this, SLOT(reject()));
		okCancelLayout->addWidget(okCancel);

		hexSignOKCancelLayout->addLayout(okCancelLayout);
	}

	resetLayout();

	for (int byte = numBytes - 1; byte > 0; --byte) {
		setTabOrder(bytes[byte], bytes[byte - 1]);
	}
	setTabOrder(bytes.back(), words.front());

	for (int word = numBytes / 2 - 1; word > 0; --word) {
		setTabOrder(words[word], words[word - 1]);
	}
	setTabOrder(words.back(), dwords.front());

	for (int dword = numBytes / 4 - 1; dword > 0; --dword) {
		setTabOrder(dwords[dword], dwords[dword - 1]);
	}
	setTabOrder(dwords.back(), qwords.front());

	for (int qword = numBytes / 8 - 1; qword > 0; --qword) {
		setTabOrder(qwords[qword], qwords[qword - 1]);
	}
	setTabOrder(qwords.back(), floats32.front());

	for (int float32 = numBytes / 4 - 1; float32 > 0; --float32) {
		setTabOrder(floats32[float32], floats32[float32 - 1]);
	}
	setTabOrder(floats32.back(), floats64.front());

	for (int float64 = numBytes / 8 - 1; float64 > 0; --float64) {
		setTabOrder(floats64[float64], floats64[float64 - 1]);
	}
	setTabOrder(floats64.front(), radioHex);

	setTabOrder(radioHex, radioSigned);
	setTabOrder(radioSigned, radioUnsigned);
	setTabOrder(radioUnsigned, okCancel);
}

template <typename T>
void DialogEditSIMDRegister::updateFloatEntries(const std::array<NumberEdit *, numBytes / sizeof(T)> &entries, NumberEdit *notUpdated) {

	for (std::size_t i = 0; i < entries.size(); ++i) {
		if (entries[i] == notUpdated) {
			continue;
		}

		T value;
		std::memcpy(&value, &value_[i * sizeof(value)], sizeof(value));
		entries[i]->setText(formatFloat(value));
	}
}

template <typename T>
void DialogEditSIMDRegister::updateIntegralEntries(const std::array<NumberEdit *, numBytes / sizeof(T)> &entries, NumberEdit *notUpdated) {

	for (std::size_t i = 0; i < entries.size(); ++i) {
		if (entries[i] == notUpdated) {
			continue;
		}

		T value;
		std::memcpy(&value, &value_[i * sizeof(value)], sizeof(value));
		formatInteger(entries[i], value);
	}
}

void DialogEditSIMDRegister::updateAllEntriesExcept(NumberEdit *notUpdated) {
	updateIntegralEntries<std::uint8_t>(bytes, notUpdated);
	updateIntegralEntries<std::uint16_t>(words, notUpdated);
	updateIntegralEntries<std::uint32_t>(dwords, notUpdated);
	updateIntegralEntries<std::uint64_t>(qwords, notUpdated);
	updateFloatEntries<edb::value32>(floats32, notUpdated);
	updateFloatEntries<edb::value64>(floats64, notUpdated);
}

void DialogEditSIMDRegister::resetLayout() {

	auto layout = qobject_cast<QGridLayout *>(this->layout());

	for (int col = ENTRIES_FIRST_COL; col < TOTAL_COLS; ++col) {
		int i = numBytes - 1 - (col - ENTRIES_FIRST_COL);

		columnLabels[i]->show();

		const auto &byte = bytes[i];
		layout->addWidget(byte, BYTES_ROW, byte->column(), 1, byte->colSpan());
		byte->show();

		const auto &word = words[i / 2];
		layout->addWidget(word, WORDS_ROW, word->column(), 1, word->colSpan());
		word->show();

		const auto &dword = dwords[i / 4];
		layout->addWidget(dword, DWORDS_ROW, dword->column(), 1, dword->colSpan());
		dword->show();

		const auto &qword = qwords[i / 8];
		layout->addWidget(qword, QWORDS_ROW, qword->column(), 1, qword->colSpan());
		qword->show();

		const auto &float32 = floats32[i / 4];
		layout->addWidget(float32, FLOATS32_ROW, float32->column(), 1, float32->colSpan());
		float32->show();

		const auto &float64 = floats64[i / 8];
		layout->addWidget(float64, FLOATS64_ROW, float64->column(), 1, float64->colSpan());
		float64->show();
	}

	for (int row = ENTRIES_FIRST_ROW; row < ROW_AFTER_ENTRIES; ++row)
		layout->itemAtPosition(row, LABELS_COL)->widget()->show();

	layout->removeItem(hexSignOKCancelLayout);
	hexSignOKCancelLayout->setParent(nullptr);
	layout->addLayout(hexSignOKCancelLayout, ROW_AFTER_ENTRIES, ENTRIES_FIRST_COL, 1, numBytes);
}

void DialogEditSIMDRegister::hideColumns(EntriesCols afterLastToHide) {

	auto layout = qobject_cast<QGridLayout *>(this->layout());

	for (int col = ENTRIES_FIRST_COL; col < afterLastToHide; ++col) {
		int i = numBytes - 1 - (col - ENTRIES_FIRST_COL);
		Q_ASSERT(0 < i && std::size_t(i) < bytes.size());

		columnLabels[i]->hide();

		// Spanned entries shouldn't just be hidden. If they are still in the grid,
		// then we get extra spacing between invisible columns, which is unwanted.
		// So we have to also remove them from the layout.
		layout->removeWidget(bytes[i]);
		bytes[i]->hide();

		layout->removeWidget(words[i / 2]);
		words[i / 2]->hide();

		layout->removeWidget(dwords[i / 4]);
		dwords[i / 4]->hide();

		layout->removeWidget(qwords[i / 8]);
		qwords[i / 8]->hide();

		layout->removeWidget(floats32[i / 4]);
		floats32[i / 4]->hide();

		layout->removeWidget(floats64[i / 8]);
		floats64[i / 8]->hide();
	}

	layout->removeItem(hexSignOKCancelLayout);
	hexSignOKCancelLayout->setParent(nullptr);
	layout->addLayout(hexSignOKCancelLayout, ROW_AFTER_ENTRIES, afterLastToHide, 1, TOTAL_COLS - afterLastToHide);
}

void DialogEditSIMDRegister::hideRows(EntriesRows rowToHide) {

	auto layout = qobject_cast<QGridLayout *>(this->layout());

	for (int col = 0; col < TOTAL_COLS; ++col) {
		const auto item = layout->itemAtPosition(rowToHide, col);
		if (item && item->widget()) {
			item->widget()->hide();
		}
	}
}

bool DialogEditSIMDRegister::eventFilter(QObject* obj, QEvent* event) {
	return entryGridKeyUpDownEventFilter(this,obj,event);
}

void DialogEditSIMDRegister::set_value(const Register &newReg) {
	resetLayout();
	assert(newReg.bitSize() <= 8 * sizeof value_);
	reg = newReg;
	util::markMemory(&value_, value_.size());
	if (QRegExp("mm[0-7]").exactMatch(reg.name())) {
		const auto value = reg.value<edb::value64>();
		std::memcpy(&value_, &value, sizeof value);
		hideColumns(MMX_FIRST_COL);
		// MMX registers are never used in float computations, so hide useless rows
		hideRows(FLOATS32_ROW);
		hideRows(FLOATS64_ROW);
	} else if (QRegExp("xmm[0-9]+").exactMatch(reg.name())) {
		const auto value = reg.value<edb::value128>();
		std::memcpy(&value_, &value, sizeof value);
		hideColumns(XMM_FIRST_COL);
	} else if (QRegExp("ymm[0-9]+").exactMatch(reg.name())) {
		const auto value = reg.value<edb::value256>();
		std::memcpy(&value_, &value, sizeof value);
		hideColumns(YMM_FIRST_COL);
	} else
		qCritical() << "DialogEditSIMDRegister::set_value(" << reg.name() << "): register type unsupported";
	setWindowTitle(tr("Modify %1").arg(reg.name().toUpper()));
	updateAllEntriesExcept(nullptr);
}

void DialogEditSIMDRegister::set_current_element(RegisterViewModelBase::Model::ElementSize size, NumberDisplayMode format, int elementIndex) {
	using namespace RegisterViewModelBase;
	if (format != intMode && format != NumberDisplayMode::Float) {
		switch (format) {
		case NumberDisplayMode::Hex:
			radioHex->setChecked(true);
			break;
		case NumberDisplayMode::Signed:
			radioSigned->setChecked(true);
			break;
		case NumberDisplayMode::Unsigned:
			radioUnsigned->setChecked(true);
			break;
		case NumberDisplayMode::Float:
			break; // silence the compiler, we'll never get here
		}
	}

	NumberEdit *edit = bytes[0];

	if (format == NumberDisplayMode::Float) {
		edit = floats32[0];
		if (size == Model::ElementSize::DWORD)
			edit = floats32[elementIndex];
		else if (size == Model::ElementSize::QWORD)
			edit = floats64[elementIndex];
	} else {
		switch (size) {
		case Model::ElementSize::BYTE:
			edit = bytes[elementIndex];
			break;
		case Model::ElementSize::WORD:
			edit = words[elementIndex];
			break;
		case Model::ElementSize::DWORD:
			edit = dwords[elementIndex];
			break;
		case Model::ElementSize::QWORD:
			edit = qwords[elementIndex];
			break;
		default:
			EDB_PRINT_AND_DIE("Unexpected size ", static_cast<long>(size));
		}
	}
	edit->setFocus(Qt::OtherFocusReason);
}

std::uint64_t DialogEditSIMDRegister::readInteger(const NumberEdit *const edit) const {
	bool ok;
	switch (intMode) {
	case NumberDisplayMode::Hex:
		return edit->text().toULongLong(&ok, 16);
		break;
	case NumberDisplayMode::Signed:
		return edit->text().toLongLong(&ok);
		break;
	case NumberDisplayMode::Unsigned:
		return edit->text().toULongLong(&ok);
		break;
	default:
		Q_ASSERT("Unexpected integer display mode" && 0);
		return 0xbadbadbadbadbad1;
	}
}

template <typename Integer>
void DialogEditSIMDRegister::formatInteger(NumberEdit *const edit, Integer integer) const {
	switch (intMode) {
	case NumberDisplayMode::Hex:
		edit->setText(QString("%1").arg(integer, 2 * sizeof integer, 16, QChar('0')));
		break;
	case NumberDisplayMode::Signed:
		typedef typename std::remove_reference<Integer>::type Int;
		typedef typename std::make_signed<Int>::type          Signed;
		edit->setText(QString("%1").arg(static_cast<Signed>(integer)));
		break;
	case NumberDisplayMode::Unsigned:
		edit->setText(QString("%1").arg(integer));
		break;
	default:
		Q_ASSERT("Unexpected integer display mode" && 0);
		return;
	}
}

template <typename Integer>
void DialogEditSIMDRegister::onIntegerEdited(QObject *sender, const std::array<NumberEdit *, numBytes / sizeof(Integer)> &elements) {
	const auto  changedElementEdit = qobject_cast<NumberEdit *>(sender);
	std::size_t elementIndex       = std::find(elements.begin(), elements.end(), changedElementEdit) - elements.begin();
	Integer     value              = readInteger(elements[elementIndex]);
	std::memcpy(&value_[elementIndex * sizeof(value)], &value, sizeof(value));
	updateAllEntriesExcept(elements[elementIndex]);
}

template <typename Float>
void DialogEditSIMDRegister::onFloatEdited(QObject *sender, const std::array<NumberEdit *, numBytes / sizeof(Float)> &elements) {
	const auto  changedFloatEdit = qobject_cast<NumberEdit *>(sender);
	std::size_t floatIndex       = std::find(elements.begin(), elements.end(), changedFloatEdit) - elements.begin();
	bool        ok               = false;
	auto        value            = readFloat<Float>(elements[floatIndex]->text(), ok);
	if (ok) {
		std::memcpy(&value_[floatIndex * sizeof(value)], &value, sizeof(value));
		updateAllEntriesExcept(elements[floatIndex]);
	}
}

void DialogEditSIMDRegister::onByteEdited() {
	onIntegerEdited<std::uint8_t>(sender(), bytes);
}

void DialogEditSIMDRegister::onWordEdited() {
	onIntegerEdited<std::uint16_t>(sender(), words);
}

void DialogEditSIMDRegister::onDwordEdited() {
	onIntegerEdited<std::uint32_t>(sender(), dwords);
}

void DialogEditSIMDRegister::onQwordEdited() {
	onIntegerEdited<std::uint64_t>(sender(), qwords);
}

void DialogEditSIMDRegister::onFloat32Edited() {
	onFloatEdited<float>(sender(), floats32);
}

void DialogEditSIMDRegister::onFloat64Edited() {
	onFloatEdited<double>(sender(), floats64);
}

void DialogEditSIMDRegister::onHexToggled(bool checked) {
	if ((checked && intMode != NumberDisplayMode::Hex) || !bytes.front()->validator()) {
		intMode = NumberDisplayMode::Hex;
		for (const auto &byte : bytes)
			byte->setValidator(byteHexValidator);
		for (const auto &word : words)
			word->setValidator(wordHexValidator);
		for (const auto &dword : dwords)
			dword->setValidator(dwordHexValidator);
		for (const auto &qword : qwords)
			qword->setValidator(qwordHexValidator);
		updateAllEntriesExcept(nullptr);
	}
}

void DialogEditSIMDRegister::onSignedToggled(bool checked) {
	if ((checked && intMode != NumberDisplayMode::Signed) || !bytes.front()->validator()) {
		intMode = NumberDisplayMode::Signed;
		for (const auto &byte : bytes)
			byte->setValidator(byteSignedValidator);
		for (const auto &word : words)
			word->setValidator(wordSignedValidator);
		for (const auto &dword : dwords)
			dword->setValidator(dwordSignedValidator);
		for (const auto &qword : qwords)
			qword->setValidator(qwordSignedValidator);
		updateAllEntriesExcept(nullptr);
	}
}

void DialogEditSIMDRegister::onUnsignedToggled(bool checked) {
	if ((checked && intMode != NumberDisplayMode::Unsigned) || !bytes.front()->validator()) {
		intMode = NumberDisplayMode::Unsigned;
		for (const auto &byte : bytes)
			byte->setValidator(byteUnsignedValidator);
		for (const auto &word : words)
			word->setValidator(wordUnsignedValidator);
		for (const auto &dword : dwords)
			dword->setValidator(dwordUnsignedValidator);
		for (const auto &qword : qwords)
			qword->setValidator(qwordUnsignedValidator);
		updateAllEntriesExcept(nullptr);
	}
}

Register DialogEditSIMDRegister::value() const {
	Register out(reg);
	out.setValueFrom(value_);
	return out;
}

}
