#include <cstdio>
#include <ctime>
#include <fstream>
#include <iostream>
#include <map>
#include <filesystem>
#include <regex>
#include <sstream>
#include <string>

#include <qabstractbutton.h>
#include <qapplication.h>
#include <qboxlayout.h>
#include <qcombobox.h>
#include <qdialog.h>
#include <qlabel.h>
#include <qlayoutitem.h>
#include <qmainwindow.h>
#include <qnamespace.h>
#include <qobjectdefs.h>
#include <qsettings.h>
#include <qsizepolicy.h>
#include <qt/QtWidgets/qwidget.h>
#include <qtoolbutton.h>
#include <qwidget.h>
#include <qwindow.h>
#include <qwindowdefs.h>

#include <Qsci/qsciscintilla.h>
#include <Qsci/qscilexercpp.h>
#include <QApplication>
#include <QMainWindow>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QToolButton>
#include <QFileSystemModel>
#include <QTreeView>
#include <QSplitter>
#include <QVariant>
#include <QMessageBox>
#include <QScrollArea>
#include <QSpacerItem>
#include <QFile>
#include <QTime>
#include <QRegularExpression>
#include <QStringList>
#include <QListView>
#include <QWindow>
#include <QMenuBar>
#include <QMenu>
#include <QFileDialog>
#include <qabstractitemmodel.h>
#include <QFormLayout>
#include <QSettings>
#include <QLineEdit>
#include <QComboBox>
#include <QSpinBox>
#include <QDoubleSpinBox>
#include <QCheckBox>
#include <QPushButton>
#include <QStandardPaths>
#include <QShortcut>
#include <QWidget>

#include "config.h"
#include "settings.h"
#include "mdemList.h"
#include "trainWindow.h"
#include "api.h"
#include "parser.h"
#include "qscilexer.h"

// Memorybase.
QString                             currentPath   = "";
std::string                         currentMdem   = "";
QFileSystemModel                   *model;
QTreeView                          *mdemList;
std::map<std::string, MdemBuffer*>  buffers;
MdemBuffer                         *currentMdemBuffer;

// Mdem list.
std::vector<Mdem*>  mdems       = std::vector<Mdem*>();
QVBoxLayout        *hMdemScroll;
QSpacerItem        *mdemSpacer;
ErrorView          *errorView;
Pagination         *pagination;
int                 perPage;

// Editor
QsciScintilla *editor;
QMainWindow   *editorWindow;
Mdem          *editMdem;

// Top labels.
QLabel       *membaseLabel;
QLabel       *mdemLabel;
QLabel    	 *lastPracticeLabel;

QMainWindow  *trainWindow;

void showBacklabels(Mdem *mdem) {
	for (size_t i = 0; i < mdem->backLabels.size(); ++i) {
		if (i < mdem->labelCount) {
			if (!mdem->backLabels[i]->isVisible()) {
				mdem->backLabels[i]->show();
			}
		} else {
			if (mdem->backLabels[i]->isVisible()) {
				mdem->backLabels[i]->hide();
			}
		}
	}
}

std::string outputMdem(std::vector<Question*> questions, time_t time = 0) {
	std::stringstream ss;

	int wrap_width = 80;
	if (settings->contains(SETTING_CHARACTER_WRAP)) {
		wrap_width = settings->value(SETTING_CHARACTER_WRAP).toInt();
	}

	if (time > 0) {
		auto timezoneOffset = settings->value(SETTING_TIMEZONE).toInt();
		/*time = time + 3600 * timezoneOffset;*/
		std::tm* tm = std::localtime(&time);
		char buffer[100];
		std::strftime(buffer, sizeof(buffer), "%d.%m.%Y %H\\:%M", tm);
		auto time = std::string(buffer);
		ss << time << std::endl;
	}

	for (auto question: questions) {
		ss << std::endl;
		std::string cooldownPart;
		if (question->cooldown != 0) {
			cooldownPart = std::format(" [{:.2f}]", question->cooldown);
		}
		ss << wrapText(
			std::format("-{}{} >\n",
			cooldownPart,
			" " + escapeText(question->questionText)),
			wrap_width
		);
		if (MultiElementQuestion* mw = dynamic_cast<MultiElementQuestion*>(question)) {
			for (auto choice: mw->choices) {
				char opener;
				if (choice.isCorrect) {
					opener = '+';
				} else {
					opener = '-';
				}
				std::string orderModifier;
				if (mw->type == MultiElementType::Order) {
					orderModifier = "^";
				}
				ss << 
					wrapText(
						std::format(
							"\t{}{} {}\n",
							opener,
							orderModifier,
							escapeText(choice.answer)
						)
					, wrap_width);
			}
		} else if (GroupQuestion* gq = dynamic_cast<GroupQuestion*>(question)) {
			for (auto group: gq->groups) {
				ss << wrapText(
					std::format(
						"\t- {}:\n",
						escapeText(group.name)
					)
				, wrap_width);
				for (auto element: group.elements) {
					ss << wrapText(
					std::format(
						"\t\t- {}\n",
						escapeText(element)
					)
					, wrap_width);
				}
			}
		}
	}
	return ss.str();
}

void makePages() {
    pagination->pages.clear();
	auto len = currentMdemBuffer->questions.size();
	perPage = settings->value(SETTING_PER_PAGE).toInt();
	auto pageAmount = len / perPage;
	if (len % perPage != 0) {
		pageAmount += 1;
	}
	for (size_t i = 0; i < pageAmount; i++) {
		size_t startingIndex = perPage * i ;
		size_t amount = perPage;
		if (i == currentMdemBuffer->questions.size() / perPage) {
            amount = currentMdemBuffer->questions.size() % perPage;
        }
        pagination->pages.push_back(
			Page{startingIndex, startingIndex + amount}
		);
    }
}

void setupMdem(Mdem *mdem, Question *question) {
	std::stringstream ss;
	if (question->cooldown > 0) {
		ss << std::format("[{:.2f}] ", question->cooldown);
	}
	ss << question->questionText;
	mdem->wFrontText.setText(
		QString::fromStdString(ss.str())	
	);
	if (MultiElementQuestion* mw = dynamic_cast<MultiElementQuestion*>(question)) {
		auto choices = mw->choices;
		for (size_t k = 0; k < choices.size(); ++k) {
			auto answer = choices[k].answer;
			switch (mw->type) {
				case MultiElementType::Order:
					answer = std::format("{}. {}", k + 1, answer); 
					break;
				case MultiElementType::MultiChoice:
					if (choices[k].isCorrect) {
						answer = std::format("+ {}", answer); 
					} else {
						answer = std::format("- {}", answer); 
					}
					break;
				case MultiElementType::Regular:
					answer = std::format("- {}", answer); 
					break;
			} 
			if (k < mdem->backLabels.size()) {
				mdem->backLabels[k]->setText(QString::fromStdString(answer));
			} else {
				auto label = new QLabel();
				label->setText(QString::fromStdString(answer));
				mdem->backLabels.push_back(label);
				mdem->wBack.layout()->addWidget(label);
			}
		}
		mdem->labelCount = choices.size();
	} else if (GroupQuestion* mw = dynamic_cast<GroupQuestion*>(question)) {
		auto groups = mw->groups;
		std::vector<std::string> elements;
		for (size_t k = 0; k < groups.size(); ++k) {
			auto answer = groups[k].name;
			elements.push_back(std::format("- {}:", answer)); 
			for (size_t l = 0; l < groups[k].elements.size(); ++l) {
				elements.push_back(std::format("    - {}", groups[k].elements[l])); 
			}
		}
		for (size_t k = 0; k < elements.size(); ++k) {
			if (k < mdem->backLabels.size()) {
				mdem->backLabels[k]->setText(QString::fromStdString(elements[k]));
			} else {
				auto label = new QLabel();
				label->setText(QString::fromStdString(elements[k]));
				mdem->backLabels.push_back(label);
				mdem->wBack.layout()->addWidget(label);
			}
		}
		mdem->labelCount = elements.size();
	}
}

void switchPage(int pageIdx);

std::string getFilename(std::string path) {
	static const std::regex lastPathElementExp = std::regex("(.+\\/)*(.+)"); 
	std::smatch matches;
	auto filenameMatched = std::regex_search(path, matches, lastPathElementExp); 
	return matches[2].str();
}

void updateMdemInfo(std::string filename, bool isChanged) {
	currentMdemBuffer->isModified = isChanged;
	if (filename.length() > 0) {
		std::stringstream ss;
		ss << std::format("mdem: {}", filename); 
		if (isChanged) {
			ss << "*";
		}
		mdemLabel->setText(QString::fromStdString(ss.str()));
		ss.str("");

		if (currentMdemBuffer->trainedAt > 0) {
			std::tm* tm = std::localtime(&currentMdemBuffer->trainedAt);
			char buffer[100];
			std::strftime(buffer, sizeof(buffer), "%d.%m.%Y %H:%M", tm);
			ss << "Last practiced: " << std::string(buffer);
		}
		lastPracticeLabel->setText(QString::fromStdString(ss.str()));
	} else {
		mdemLabel->setText("");
		lastPracticeLabel->setText("");
	}
}

Mdem* makeMdem() {
	auto mdem = new Mdem;
	mdem->wMdem.setLayout(&mdem->vMdem);
	
	QString id = QString("mdem_%1").arg(1);
	mdem->wMdem.setObjectName(id);

	// Front
	mdem->wFront.setMinimumHeight(60);
	mdem->wFront.setLayout(&mdem->hFront);
	mdem->wFront.setProperty("first", "true");
	mdem->wMdem.setStyleSheet(QString(
		"QWidget#%1 > QWidget {"
			"border-right:  1px solid gray;"
			"border-bottom: 1px solid gray;"
			"border-left:   1px solid gray;"
			"background: %2;"
		"} "
		"QWidget#%1 > QWidget[first=\"true\"] {"
			"border-top: 1px solid gray;"
		"}"
	).arg(id, MDEM_BACKGROUND));

	// Add Front Content
	mdem->hFront.addWidget(&mdem->wFrontText);
	mdem->hFront.addStretch(1);

	mdem->editButton.setText("Edit");
	QObject::connect(
		&mdem->editButton,
		&QToolButton::clicked,
		[mdem]() {
			editMdem = mdem;
			if (mdem->question) {
				editor->setText(
					QString::fromStdString(
						outputMdem(std::vector<Question*>{mdem->question})
					)
				);
				editorWindow->show();
				editor->setCursorPosition(1, 2);
			}
		}
	);
	mdem->hFront.addWidget(&mdem->editButton);

	mdem->deleteButton.setText("Delete");
	QObject::connect(
		&mdem->deleteButton,
		&QToolButton::clicked,
		[mdem]() {
			if (mdem->question) {
				Question* deleted = nullptr;
				for (size_t i = 0; i < currentMdemBuffer->questions.size(); ++i) {
					if (currentMdemBuffer->questions[i] == mdem->question) {
						currentMdemBuffer->questions.erase(currentMdemBuffer->questions.begin() + i);
						delete mdem->question;
						mdem->question = nullptr;
						updateMdemInfo(getFilename(currentMdem), true);
						break;
					}
				}
				makePages();
				switchPage(pagination->currentPage);
			}
			if (editMdem == mdem) {
				editorWindow->hide();
			}
		}
	);
	mdem->hFront.addWidget(&mdem->deleteButton);
	
	mdem->toggleVisibility.setText("Show");
	mdem->hFront.addWidget(&mdem->toggleVisibility);

	// Back
	QVBoxLayout *hBack = new QVBoxLayout();
	mdem->wBack.setLayout(&mdem->hBack);
	mdem->vMdem.addWidget(&mdem->wBack);

	// Add Back Content
	for (size_t i = 0; i < 20; ++i) {
		// @Improve: back label pooling
		QLabel *elBackText = new QLabel();
		mdem->hBack.addWidget(elBackText);
		mdem->backLabels.push_back(elBackText);
	}

	mdem->vMdem.addWidget(&mdem->wFront);
	mdem->vMdem.addWidget(&mdem->wBack);
	mdem->vMdem.setContentsMargins(0, 0, 0, 0);
	mdem->vMdem.setSpacing(0);

	mdem->wBack.hide();
	mdem->wMdem.hide();

	// Connect button to toggle view
	QObject::connect(&mdem->toggleVisibility, &QToolButton::clicked, [mdem]() {
		if (mdem->wBack.isVisible()) {
			mdem->wBack.hide();
			mdem->toggleVisibility.setText("Show");
		} else {
			mdem->wBack.show();
			mdem->toggleVisibility.setText("Hide");
		}
		showBacklabels(mdem);
	});
	return mdem;
}


void CreateMdems(std::vector<Question*>& questions) {
	hMdemScroll->removeItem(mdemSpacer);

    for (Mdem *mdem : mdems) {
        if (mdem->wMdem.isVisible()) {
            mdem->wMdem.hide();
        }
    }

	perPage = settings->value(SETTING_PER_PAGE).toInt();
	if (perPage > mdems.size()) {
		for (size_t i = mdems.size(); i < perPage; i++) {
			if (i >= mdems.size()) {
				auto mdem = makeMdem();
				mdems.push_back(mdem);
				hMdemScroll->addWidget(&mdem->wMdem);
			}
		}
	}

	for (size_t i = 0; i < questions.size(); ++i) {
		mdems[i]->question = questions[i];
		setupMdem(mdems[i], questions[i]);
		if (!mdems[i]->wMdem.isVisible()) {
		   mdems[i]->wMdem.show();
    	}
	}

	hMdemScroll->addItem(mdemSpacer);
}

void update(bool isChanged) {
	if (pagination->currentPage > -1) {
		switchPage(pagination->currentPage);
	}
	if (currentMdem.length() > 0) {
		updateMdemInfo(getFilename(currentMdem), isChanged);
	}
}

void switchPage(int pageIdx) {
    pagination->currentPage = pageIdx;

    // Hide all pagination buttons
    for (auto& button : pagination->paginationButtons) {
        button->hide();
    }

    int l = 0;
    char buffer[50];
    snprintf(buffer, sizeof(buffer), "Page: %d", pageIdx + 1);
    pagination->paginationLabel.setText(buffer);

    // Adjust mdem amount, hide widgets in mdems
    for (auto& mdem : mdems) {
        if (mdem->wBack.isVisible()) {
            mdem->wBack.hide();
            mdem->toggleVisibility.setText("Show");
        }
    }

    // Update pagination buttons
    for (int k = -DISTANCE; k <= DISTANCE; ++k) {
        if (pageIdx + k >= 0 && pageIdx + k < pagination->pages.size()) {
            auto button = pagination->paginationButtons[l];
            snprintf(buffer, sizeof(buffer), "%d", pageIdx + k + 1);
            button->setText(buffer);

            if (pageIdx + k != pageIdx) {
                button->show();
            } else {
                button->hide();
            }
            ++l;
        }
    }

    // Handle first and last buttons
    if (pageIdx > 0 && pagination->pages.size() > 1) {
        pagination->firstButton.show();
    } else {
        pagination->firstButton.hide();
    }

    if (pageIdx < pagination->pages.size() - 1 && pagination->pages.size() > 1) {
        pagination->lastButton.show();
    } else {
        pagination->lastButton.hide();
    }

    // Handle next and previous buttons
    if (!pagination->pages.empty() && pagination->currentPage < pagination->pages.size() - 1) {
        pagination->nextButton.show();
    } else {
        pagination->nextButton.hide();
    }

    if (!pagination->pages.empty() && pagination->currentPage >= 1) {
        pagination->prevButton.show();
    } else {
        pagination->prevButton.hide();
    }

    Page page;
	if (pagination->pages.size() <= 0) {
		page = Page();
	} else if (pageIdx < pagination->pages.size()){
		page = pagination->pages[pageIdx];
	} else {
		if (pageIdx - 1 < pagination->pages.size()) {
			page = pagination->pages[pageIdx -1];
		} else {
			page = pagination->pages[0];
		}
	}

    std::vector<Question*> pageSlice(
		currentMdemBuffer->questions.begin() + page.start,
		currentMdemBuffer->questions.begin() + page.end
	);
    CreateMdems(pageSlice);
}

void reloadMdem(std::string path) {
	if (path == "") {
		return;
	}
	
	auto toRemove = std::vector<std::string>();
	for (auto it = buffers.begin(); it != buffers.end(); ++it) {
		auto pair = *it;
		if (currentMdem.compare(path) != 0 &&
			(!pair.second->isModified || pair.second->error)) {
			toRemove.push_back(pair.first);
		}
	}
	for (auto key: toRemove) {
		buffers.erase(key);
	}

	MdemBuffer *buffer;
	auto filename = getFilename(path);
	if (currentMdem.compare(path) == 0) {
		buffer = currentMdemBuffer;
		currentMdem = path;
		if (buffers.contains(path)) {
			buffers.erase(path);
		}
	} else if (!buffers.contains(path)) {
		buffer = new MdemBuffer;
		buffers[path] = buffer;
		currentMdemBuffer = buffer;
		currentMdem = path;
	} else {
		buffer = buffers[path];
		currentMdemBuffer = buffer;
		makePages();
		switchPage(0);
		updateMdemInfo(getFilename(filename), buffer->isModified);
		currentMdem = path;
		errorView->box.hide();
		return;
	}

	start = std::chrono::high_resolution_clock::now();
	auto file = std::ifstream(path);
	std::string content;

	// Reset the mdem list.
	for (auto mdem: mdems) {
		mdem->wMdem.hide();
	}
	for (auto question: currentMdemBuffer->questions) {
		delete question;
	}
	currentMdemBuffer->questions.clear();

	if (file) {
		std::stringstream buffer;
		buffer << file.rdbuf();
		content = buffer.str();
		end = std::chrono::high_resolution_clock::now();
		showTimes = settings->value(SETTING_SHOW_TIMES).toBool();
		if (showTimes) {
			std::cout << showTime("I/O time") << std::endl;
		}
		debug = settings->value(SETTING_DEBUG).toBool();
		auto res = transpile(content);

		currentMdemBuffer->error = res.error.length() > 0;
		if (res.error == "") {
			if (res.value.lastTrainedAt == 0) {
				currentMdemBuffer->trainedAt = 0;
			} else {
				auto timezoneOffset = settings->value("timezone").toInt();
				currentMdemBuffer->trainedAt = res.value.lastTrainedAt - 3600 * timezoneOffset;
			}

			if (settings->value(SETTING_DEBUG).toBool()) {
				std::cout << std::format("Last trained at: {}", currentMdemBuffer->trainedAt) 
						  << std::endl;
			}

			currentMdemBuffer->questions = res.value.questions;
			errorView->box.hide();
		} else {
			currentMdemBuffer->trainedAt = 0;
			std::cout << std::format("Compilation error: {}", res.error) << std::endl;

			for (auto question: res.value.questions) {
				delete question;
			}

			// Show errors.
			hMdemScroll->removeItem(mdemSpacer);
			errorView->label.setText(
				QString::fromStdString(
					std::format(
						"Error while transpiling {}: {} ({}:{})",
						filename,
						res.error,
						res.row,
						res.column
					)
				)
			);
			errorView->box.show();

			hMdemScroll->addWidget(&errorView->box);
			hMdemScroll->addItem(mdemSpacer);
		}
		makePages();
		switchPage(0);
		updateMdemInfo(filename, false);
	} else {
		std::cout << std::format("Could not open the file: {}", currentPath.toStdString()) << std::endl;
	}

	for (auto mdem: mdems) {
		mdem->wBack.hide();
		mdem->toggleVisibility.setText("Show");
	}
	hideQuestionElements();
}

void pickDirectory(QString directory) {

	auto path = std::filesystem::path(directory.toStdString());
	if (
		!std::filesystem::exists(directory.toStdString()) ||
		!std::filesystem::is_directory(directory.toStdString())
	) {
		QMessageBox::information(
			nullptr,
			"Error",
			"The directory that is specified as the default memorybase does not exist."
		);
		return;
    }

	currentPath = directory;

	// Update tree view.
	if (directory.length() <= 0) {
		membaseLabel->setText(directory);
		return;
	}
    mdemList->setRootIndex(model->setRootPath(directory));
	std::smatch matches;
	
	// Update label.
	membaseLabel->setText(QString::fromStdString(
		std::format(
			"memorybase: {}",
			getFilename(currentPath.toStdString())
		)
	));

	reloadMdem("");
}

void setupEditorSave() {
	debug = settings->value(SETTING_DEBUG).toBool();
	showTimes = settings->value(SETTING_SHOW_TIMES).toBool();
	auto res = transpile(editor->text().toStdString());
	if (res.error.length() > 0) {
		currentMdemBuffer->trainedAt = 0;
		for (auto question: res.value.questions) {
			delete question;
		}
		QMessageBox::information(
			nullptr,
			"Editing error",
			QString::fromStdString(res.error)
		);
	} else {
		if (editMdem) {
			if (res.value.questions.size() <= 0) {
				QMessageBox::information(
					nullptr,
					"Editing error",
					"There are no questions in your input."
				);
			} else if (res.value.questions.size() == 1) {
				auto oldQuestion = editMdem->question;
				editMdem->question = res.value.questions[0];
				for (size_t i = 0; i < currentMdemBuffer->questions.size(); ++i) {
					if (currentMdemBuffer->questions[i] == oldQuestion) {
						currentMdemBuffer->questions.erase(currentMdemBuffer->questions.begin() + i);
						delete oldQuestion;
						currentMdemBuffer->questions.insert(currentMdemBuffer->questions.begin() + i, res.value.questions[0]);
						break;
					}
				}
				setupMdem(editMdem, res.value.questions[0]);
				showBacklabels(editMdem);
				editorWindow->hide();
				updateMdemInfo(getFilename(currentMdem), true);
			} else {
				QMessageBox::information(
					nullptr,
					"Editing error",
					"There are more than question in your input. Please enter one."
				);
			}
		} else {
			if (res.value.questions.size() <= 0) {
				QMessageBox::information(
					nullptr,
					"Editing error",
					"There are no questions in your input."
				);
			} else {
				currentMdemBuffer->questions.insert(
					currentMdemBuffer->questions.begin(),
					res.value.questions.begin(),
					res.value.questions.end()
				);
				makePages();
				switchPage(0);
				editorWindow->hide();
				updateMdemInfo(getFilename(currentMdem), true);
			}
		}
	}
};

void saveMdem() {
	start = std::chrono::high_resolution_clock::now();

	auto filename = getFilename(currentMdem);
	std::ofstream out(currentMdem);
	out << outputMdem(currentMdemBuffer->questions, currentMdemBuffer->trainedAt);
	updateMdemInfo(getFilename(currentMdem), false);
	end = std::chrono::high_resolution_clock::now();

	showTimes = settings->value(SETTING_SHOW_TIMES).toBool();
	if (showTimes) {
		std::cout << showTime("Saving time") << std::endl;
	}
}

QMainWindow *initMdemListWindow() {
    QMainWindow* window = new QMainWindow;
	pagination = new Pagination; 
	auto *toolbar = new Toolbar();

	// Setup related windows
	auto *settingsWindow = initSettings();
	trainWindow = initTrainWindow();

	{ // Menu bar.
		QMenuBar *menuBar = new QMenuBar;
		QFileDialog *fileDialog = new QFileDialog;

		QMenu *menu = new QMenu("File");
		QAction *actionOpen = menu->addAction("Open memorybase");
		QObject::connect(actionOpen, &QAction::triggered, [fileDialog]() {
			fileDialog->setDirectory(QDir::homePath());
			fileDialog->setFileMode(QFileDialog::FileMode::Directory);
			fileDialog->open();
			QObject::disconnect(fileDialog, 0, 0, 0);		
			fileDialog->connect(
				fileDialog,
				&QFileDialog::fileSelected,
				pickDirectory
			);
		});

		QAction *openSettings = menu->addAction("Settings");
		QObject::connect(
			openSettings,
			&QAction::triggered,
			[settingsWindow]() {
			settingsWindow->show();
		});

		menuBar->addMenu(menu);
		window->setMenuBar(menuBar);
	}

	{ // Editor.
		editorWindow = new QMainWindow;

		editorWindow->setWindowTitle("QScintilla Simple Editor");
		editorWindow->resize(800, 600);
		QWidget *wEditor = new QWidget;
		QVBoxLayout *vlEditor = new QVBoxLayout;
		wEditor->setLayout(vlEditor);
		
		QsciLexerCPP *lexer = new QsciLexerCPP;
		editor = new QsciScintilla;
		editor->setLexer(lexer);
		editor->setUtf8(true);
		editor->setMarginWidth(0, 15);
		editor->zoomIn(2);

		QHBoxLayout *buttonLayout = new QHBoxLayout;
		QWidget *editorButtons = new QWidget;
		auto btnSaveEditor = new QPushButton;

		editorButtons->setLayout(buttonLayout);
		btnSaveEditor->setText(QString::fromStdString("Save"));
		QObject::connect(
			btnSaveEditor,
			&QToolButton::clicked,
			setupEditorSave
		);
		buttonLayout->addWidget(btnSaveEditor);

		vlEditor->addWidget(editor);
		vlEditor->addWidget(editorButtons);

		QShortcut* shortcutSave = new QShortcut(QKeySequence("Ctrl+S"), editorWindow);
		QObject::connect(shortcutSave, &QShortcut::activated, []() {
			if (editor->isVisible()) {
				setupEditorSave();
			}
		});

		editorWindow->setCentralWidget(wEditor);
	}

	auto *wTop = new QWidget();
	{ // Top.
	
	  	// Main top layout.
		QHBoxLayout *hlTop = new QHBoxLayout();
		wTop->setLayout(hlTop);

		// Labels 
		auto *wLabels  = new QWidget();
		auto *vlLeftTop = new QVBoxLayout();
		wLabels->setLayout(vlLeftTop);
		wLabels->setMinimumSize(0, 40);

		QString labelStyle = "font-size: 17px; font-weight: 400;";
		// Memorybase label.
		membaseLabel   = new QLabel();
		membaseLabel->setStyleSheet(labelStyle);
		vlLeftTop->addWidget(membaseLabel);

		// Memorybase label.
		mdemLabel     = new QLabel();
		mdemLabel->setStyleSheet(labelStyle);
		vlLeftTop->addWidget(mdemLabel);

		lastPracticeLabel = new QLabel();
		lastPracticeLabel->setStyleSheet(labelStyle);
		vlLeftTop->addWidget(lastPracticeLabel);

		// Button layout.
		auto buttons         = new QWidget();
		auto vlButtons       = new QVBoxLayout();
		buttons->setLayout(vlButtons);

		auto buttonsTop      = new QWidget();
		auto hlButtonsTop    = new QHBoxLayout();
		buttonsTop->setLayout(hlButtonsTop);

		auto buttonsBottom   = new QWidget();
		auto hlButtonsBottom = new QHBoxLayout();
		buttonsBottom->setLayout(hlButtonsBottom);

		// Define buttons.

		QObject::connect(&toolbar->btnAdd, &QToolButton::clicked, []() {
			editMdem = nullptr;
			editorWindow->show();
			editor->setText("");
		});
		QObject::connect(&toolbar->btnLoad, &QToolButton::clicked, []() {
			reloadMdem(currentMdem);
		});              
		QObject::connect(&toolbar->btnSave, &QToolButton::clicked, []() {
			saveMdem();  
		});
		QObject::connect(
			&toolbar->btnPractice,
			&QToolButton::clicked,
			[toolbar]() {
				if (currentMdemBuffer) {
					trainWindow->show();
					trainWindow->resize(600, 300);
					initiatePractice(
						currentMdemBuffer,
						static_cast<PracticeAlgorithm>(
							toolbar->cbAlgorithm.currentData().toInt()
						)
					);
				}
			}
		);

		// Button content.
		toolbar->btnAdd.setText("Add");
		toolbar->btnSave.setText("Save");
		toolbar->btnLoad.setText("Load");
		toolbar->btnPractice.setText("Practice");

		toolbar->cbAlgorithm.addItem("Spaced",  SPACED);
		toolbar->cbAlgorithm.addItem("Random",  RANDOM);
		toolbar->cbAlgorithm.addItem("Primary", PRIMARY);

		// Add buttons.
		hlButtonsTop->addWidget(&toolbar->btnAdd);
		hlButtonsTop->addWidget(&toolbar->btnSave);
		hlButtonsTop->addWidget(&toolbar->btnLoad);
		hlButtonsBottom->addWidget(&toolbar->cbAlgorithm);
		hlButtonsBottom->addWidget(&toolbar->btnPractice);
		vlButtons->addWidget(buttonsTop);
		vlButtons->addWidget(buttonsBottom);

		// Style buttons
		hlButtonsTop->setContentsMargins(0, 0, 0, 0);
		hlButtonsBottom->setContentsMargins(0, 5, 0, 0);

		// Add top components.
		hlTop->addWidget(wLabels);
		hlTop->addStretch(1);
		hlTop->addWidget(buttons);
	}

	QWidget     *leftWidget = new QWidget();
	{ // Left side.

		QVBoxLayout *leftLayout = new QVBoxLayout();
		leftWidget->setLayout(leftLayout);

		model          = new QFileSystemModel();
		mdemList       = new QTreeView();
		mdemList->setModel(model);
		currentPath = settings->value(SETTING_MEMORYBASE).toString();
		if (currentPath.size() > 0) {
			pickDirectory(currentPath);
		}

		/*leftLayout->addWidget(leftTop);*/

		QObject::connect(
			mdemList,
			&QTreeView::doubleClicked,
			[](const QModelIndex &index) {
				trainWindow->close();
				auto fileInfo = model->fileInfo(index);
				reloadMdem(fileInfo.filePath().toStdString());
			}
		);

		for (int col = 1; col < model->columnCount(); ++col) {
			mdemList->hideColumn(col);
		}
		model->setHeaderData(0, Qt::Horizontal, QObject::tr("Custom Name"));
		leftLayout->addWidget(mdemList);
	}

	auto wMain = new QWidget;
	auto *rightWidget = new QWidget();
	auto *rightLayout = new QVBoxLayout();
	{ // Main layout.
		auto vlMain = new QVBoxLayout;
		wMain->setLayout(vlMain);

		rightWidget->setLayout(rightLayout);

		auto *hSplitter = new QSplitter();
		hSplitter->addWidget(leftWidget);
		hSplitter->addWidget(rightWidget);
		hSplitter->setStretchFactor(1, 3);
		hSplitter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

		vlMain->addWidget(wTop);
		vlMain->addWidget(hSplitter);
	}

	{ // Error.
		errorView = new ErrorView;
		errorView->box.setObjectName("error-box");
		errorView->box.setLayout(&errorView->layout);
		errorView->box.setMinimumHeight(30);
		errorView->box.setStyleSheet(
			QString(
				"QWidget#error-box {"
					"border: 1px solid red;"
					"background: %2;"
				"}"
			).arg(MDEM_BACKGROUND)
		);
		errorView->layout.addWidget(&errorView->label);
		errorView->label.setWordWrap(true);
	}

	{ // Mdems
		QScrollArea *mdemScroll    = new QScrollArea();
		QWidget     *mdemContainer = new QWidget();
		hMdemScroll                = new QVBoxLayout();
		mdemScroll->setWidget(mdemContainer);
		mdemScroll->setWidgetResizable(true);
		mdemContainer->setLayout(hMdemScroll);
		rightLayout->addWidget(mdemScroll);
		mdemSpacer = new QSpacerItem(
			50,
			50,
			QSizePolicy::Minimum,
			QSizePolicy::Expanding
		);
		/*hMdemScroll->addItem(mdemSpacer);*/
	}

	{ // Pagination 

		auto wPagination = new QWidget();
		auto hPagination = new QHBoxLayout();

		wPagination->setLayout(hPagination);

		pagination->firstButton.setText(QString::fromStdString("<<"));
		hPagination->addWidget(&pagination->firstButton);
		pagination->firstButton.hide();
		QObject::connect(&pagination->firstButton, &QToolButton::clicked, []() {
			if (pagination->pages.size() > 0) {
				switchPage(0);
			}
		});

		pagination->prevButton.setText(QString::fromStdString("<"));
		hPagination->addWidget(&pagination->prevButton);
		pagination->prevButton.hide();
		QObject::connect(&pagination->prevButton, &QToolButton::clicked, []() {
			if (pagination->pages.size() > 0) {
				switchPage(pagination->currentPage - 1);
			}
		});

		perPage = settings->value(SETTING_PER_PAGE).toInt();
		for (size_t i = 0; i < perPage; i++) {
			auto elButton = new QToolButton();
			elButton->setText(QString("%1").arg(i+1));
			hPagination->addWidget(elButton);
			elButton->hide();
			QObject::connect(elButton, &QToolButton::clicked, [elButton]() {
				auto pageNum = std::stoi(elButton->text().toStdString().c_str());
				auto pageIdx = pageNum - 1;
				if (pageIdx < pagination->pages.size()) {
					switchPage(pageIdx);
				}
			});
			pagination->paginationButtons.push_back(elButton);
		}
		
		pagination->nextButton.setText(QString::fromStdString(">"));
		hPagination->addWidget(&pagination->nextButton);
		pagination->nextButton.hide();
		QObject::connect(&pagination->nextButton, &QToolButton::clicked, []() {
			if (pagination->pages.size() > 0) {
				switchPage(pagination->currentPage + 1);
			}
		});

		pagination->lastButton.setText(QString::fromStdString(">>"));
		hPagination->addWidget(&pagination->lastButton);
		pagination->lastButton.hide();
		QObject::connect(&pagination->lastButton, &QToolButton::clicked, []() {
			if (pagination->pages.size() > 0) {
				switchPage(pagination->pages.size() - 1);
			}
		});
		hPagination->addStretch(1);
		hPagination->addWidget(&pagination->paginationLabel);
		rightLayout->addWidget(wPagination);
	}

	{ // Setup shortcuts.
		auto addShortcut = [window](QString key, std::function<void()> func) {
			QShortcut* shortcut = new QShortcut(QKeySequence(key), window);
			QObject::connect(shortcut, &QShortcut::activated, [func]() {
				func();
			});
		};

		addShortcut("Ctrl+,", [settingsWindow]() {
			settingsWindow->show();
		});
		addShortcut("Ctrl+S", [toolbar]() {
			toolbar->btnSave.click();
		});
		addShortcut("Ctrl+P", [toolbar]() {
			toolbar->btnPractice.click();
		});
		addShortcut("Ctrl+A", [toolbar]() {
			toolbar->btnAdd.click();
		});
		addShortcut("Ctrl+L", [toolbar]() {
			toolbar->btnLoad.click();
		});
	}

    window->setCentralWidget(wMain);
    window->show();
	return window;
}
