#include <cstdio>
#include <format>
#include <fstream>
#include <iostream>
#include <qabstractbutton.h>
#include <qboxlayout.h>
#include <qlabel.h>
#include <qlayoutitem.h>
#include <qmainwindow.h>
#include <qnamespace.h>
#include <qobjectdefs.h>
#include <qsizepolicy.h>
#include <qtoolbutton.h>
#include <qwidget.h>
#include <qwindow.h>
#include <qwindowdefs.h>
#include <regex>
#include <sstream>
#include <string>

#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 "api.h"
#include "parser.h"
#include "trainWindow.h"

#define TEXT_LG =        20
#define ERROR_POOL_CHUNK 50
#define DISTANCE         2
#define PER_PAGE         8
#define MDEM_BACKGROUND  "#F7F7F7"
#define WRAP_WIDTH 50

struct Mdem {
    QWidget          wMdem;
    QVBoxLayout      vMdem;
    QLabel           wFrontText;
    QWidget          wFront;
    QHBoxLayout      hFront;
    QWidget          wBack;
    QVBoxLayout      hBack;
    QToolButton      editButton;
    QToolButton      showButton;
    int              labelCount;
    QVector<QLabel*> backLabels;
	Question        *question;
};

struct ErrorView {
	QWidget     box;
	QVBoxLayout layout;
	QLabel      label;
};

struct Page {
    int start;
    int end;
};

// @Debug: set the default directory for convenience. It should be configured by user
// or set to "".
std::string              currentPath   = "/home/jorenchik/Code/mdemory/memorybase";
std::string              currentMdem   = "";
/*std::string              currentPath   = "";*/
QFileSystemModel        *model;
QLabel                  *mdemLabel;
QTreeView               *mdemList;

// Mdem list
QLabel                  *deckListLabel;
QVBoxLayout             *hMdemScroll;
QList<Mdem*>             mdems         = QList<Mdem*>();
std::vector<Question*>   questions     = std::vector<Question*>();
QSpacerItem             *mdemSpacer;
std::vector<ErrorView*>  errorPool;
std::vector<ErrorView*>  errorViews;

// Pagination 
int                      currentPage   = -1;
std::vector<Page>        pages;
QList<QToolButton*>      paginationButtons;
QToolButton*             prevButton;
QToolButton*             firstButton;
QToolButton*             lastButton;
QToolButton*             nextButton; 
QLabel*                  paginationLabel;

// Mdem actions
QToolButton             *save;
QToolButton             *load;
QToolButton             *practice;

QsciScintilla *editor;
QMainWindow* editorWindow;

const std::regex         lastPathElementExp = std::regex("(.+\\/)*(.+)"); 

void showBacklabels(Mdem *mdem) {
	for (int 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) {
	std::stringstream ss;
	for (auto question: questions) {
		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);
				}
			}
		}
		ss << std::endl;
	}
	return ss.str();
}

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");
	mdem->hFront.addWidget(&mdem->editButton);

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

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

	// Add Back Content
	for (int i = 0; i < 20; ++i) {
		// @Fix: back label pooling
		QLabel *elBackText = new QLabel();
		mdem->hBack.addWidget(elBackText);
		mdem->backLabels.append(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->showButton, &QToolButton::clicked, [mdem]() {
		if (mdem->wBack.isVisible()) {
			mdem->wBack.hide();
			mdem->showButton.setText("Show");
		} else {
			mdem->wBack.show();
			mdem->showButton.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();
        }
    }

	if (questions.size() > mdems.size()) {
		auto amount = questions.size() - mdems.size(); 
		for (size_t i = 0; i < amount; ++i) {
			auto mdem = makeMdem();
			mdems.append(mdem);
			hMdemScroll->addWidget(&mdem->wMdem);
		}
	}


	for (size_t i = 0; i < questions.size(); ++i) {
		mdems[i]->question = questions[i];
		if (MultiElementQuestion* mw = dynamic_cast<MultiElementQuestion*>(questions[i])) {
			mdems[i]->wFrontText.setText(
				QString::fromStdString(mw->QuestionText)	
			);
			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 < mdems[i]->backLabels.size()) {
					mdems[i]->backLabels[k]->setText(QString::fromStdString(answer));
				} else {
					auto label = new QLabel();
					label->setText(QString::fromStdString(answer));
					mdems[i]->backLabels.push_back(label);
					mdems[i]->wBack.layout()->addWidget(label);
				}
			}
			mdems[i]->labelCount = choices.size();
		} else if (GroupQuestion* mw = dynamic_cast<GroupQuestion*>(questions[i])) {
			mdems[i]->wFrontText.setText(
				QString::fromStdString(mw->QuestionText)	
			);
			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 < mdems[i]->backLabels.size()) {
					mdems[i]->backLabels[k]->setText(QString::fromStdString(elements[k]));
				} else {
					auto label = new QLabel();
					label->setText(QString::fromStdString(elements[k]));
					mdems[i]->backLabels.push_back(label);
					mdems[i]->wBack.layout()->addWidget(label);
				}
			}
			mdems[i]->labelCount = elements.size();
		}
		if (!mdems[i]->wMdem.isVisible()) {
		   mdems[i]->wMdem.show();
    	}
	}

	hMdemScroll->addItem(mdemSpacer);
}

void makePages() {
    pages.clear();
	auto len = questions.size();
	for (int i = 0; i < (len / PER_PAGE) + 1; i++) {
		auto startingIndex = PER_PAGE * i ;
		auto amount = PER_PAGE;
		if (i == questions.size() / PER_PAGE) {
            amount = questions.size() % PER_PAGE;
        }
        pages.push_back(Page{startingIndex, startingIndex + amount});
    }
}

void SwitchPage(int pageIdx) {
    currentPage = pageIdx;

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

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

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

    // Update pagination buttons
    for (int k = -DISTANCE; k <= DISTANCE; ++k) {
        if (pageIdx + k >= 0 && pageIdx + k < pages.size()) {
            auto button = 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 && pages.size() > 1) {
        firstButton->show();
    } else {
        firstButton->hide();
    }

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

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

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

    // Handle page slice
    const Page& page = pages[pageIdx];

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

ErrorView *makeErrorView() {
	auto 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);
	return errorView; 
};

ErrorView* acquireError() {
	if (errorPool.size() <= 0) {
		for (int i = 0; i < ERROR_POOL_CHUNK; ++i) {
			auto errorView = makeErrorView();
			errorPool.push_back(errorView);
			hMdemScroll->addWidget(&errorView->box);
			errorView->box.hide();
		}
	}
	auto item = errorPool.back();
	errorPool.pop_back();
	errorViews.push_back(item);
	std::cout << std::format("Acquired, current pool size: {}\n", errorPool.size());
	return item;
}

void releaseError(ErrorView** item) {
	errorPool.push_back(*item);
	(*item) = nullptr;
	std::cout << std::format("Released, current pool size: {}\n", errorPool.size());
}

std::string getFilename(std::string path) {
	std::smatch matches;
	auto filenameMatched = std::regex_search(path, matches, lastPathElementExp); 
	return matches[2].str();
}

void reloadMdem() {
	auto file = std::ifstream(currentMdem);
	std::string content;

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

	// Get filename.
	auto filename = getFilename(currentMdem);
	if (filename.length() > 0) {
		deckListLabel->setText(
			QString::fromStdString(std::format("mdem: {}", filename))
		);
	} else {
		deckListLabel->setText("");
	}

	if (file) {
		std::stringstream buffer;
		buffer << file.rdbuf();
		content = buffer.str();
		auto res = transpile(content, true);

		while (errorViews.size() > 0) {
			auto errorView = errorViews.back();
			errorViews.pop_back();
			errorView->box.hide();
			releaseError(&errorView);
		}

		if (res.error == "") {
			time_t trainedAt;
			if (res.value.lastTrainedAt == 0) {
				trainedAt = 0;
			} else {
				trainedAt = res.value.lastTrainedAt + 3600 * 2;
			}
			std::cout << std::format("Last trained at: {}", trainedAt) << std::endl;
			questions = res.value.questions;
			makePages();
			SwitchPage(0);
		} else {
			std::cout << std::format("Compilation error: {}", res.error) << std::endl;

			// Show errors.
			hMdemScroll->removeItem(mdemSpacer);
			auto errorView = acquireError();
			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);
		}
	} else {
		std::cout << std::format("Could not open the file: {}", currentPath) << std::endl;
	}
}

void pickDirectory(QString directory) {
	currentPath = directory.toStdString();

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

	currentMdem = "";
	reloadMdem();
}


int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    QMainWindow window;

	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
		);
	});
	menuBar->addMenu(menu);

	window.setMenuBar(menuBar);

	{ // Setup editor.
		editorWindow = new QMainWindow;

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

		QHBoxLayout *buttonLayout = new QHBoxLayout;
		QWidget *editorButtons = new QWidget;
		auto save = new QToolButton;
		editorButtons->setLayout(buttonLayout);
		save->setText(QString::fromStdString("Save"));
		buttonLayout->addWidget(save);

		vlEditor->addWidget(editor);
		vlEditor->addWidget(editorButtons);
		editorWindow->setCentralWidget(wEditor);
	}

    QSplitter *hSplitter = new QSplitter();

    // LeftSide
    QWidget     *leftWidget = new QWidget();
    QVBoxLayout *leftLayout = new QVBoxLayout();
    QWidget     *leftTop    = new QWidget();
    QVBoxLayout *vLeftTop   = new QVBoxLayout();
    mdemList                = new QTreeView();
    mdemLabel               = new QLabel();
    model                   = new QFileSystemModel();
	mdemLabel->setStyleSheet(
		"font-size: 17px;"
		"font-weight: 400;"
	);
	mdemSpacer = new QSpacerItem(50, 50, QSizePolicy::Minimum, QSizePolicy::Expanding);
    
    leftWidget->setLayout(leftLayout);
    leftLayout->addWidget(leftTop);
	leftTop->setLayout(vLeftTop);
	leftTop->setMinimumSize(0, 40);
	vLeftTop->addWidget(mdemLabel);
	// Hide all columns except the first one
    mdemList->setModel(model);

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

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


    // RightSide
    QWidget     *rightWidget = new QWidget();
    QVBoxLayout *rightLayout = new QVBoxLayout();
    rightWidget->setLayout(rightLayout);

    QWidget *top      = new QWidget();
    QHBoxLayout *hTop = new QHBoxLayout();
    deckListLabel     = new QLabel();
	deckListLabel->setStyleSheet(
		"font-size: 17px;"
		"font-weight: 400;"
	);
	top->setMinimumSize(0, 40);
    top->setLayout(hTop);
    rightLayout->addWidget(top);

    hTop->addWidget(deckListLabel);
    hTop->addStretch(1);

	save = new QToolButton;
    load  = new QToolButton;
    practice = new QToolButton;

    hTop->addWidget(save);
    hTop->addWidget(load);
    hTop->addWidget(practice);

    // Buttons
    save->setText("Save");
    load->setText("Load");
	practice->setText("Practice");
	QObject::connect(save, &QToolButton::clicked, []() {
		auto filename = getFilename(currentMdem);
		std::ofstream out("generated.mdem");
		out << outputMdem(questions);;
	});
	QObject::connect(load, &QToolButton::clicked, &reloadMdem);

    // Mdems
    QScrollArea *mdemScroll    = new QScrollArea();
    QWidget     *mdemContainer = new QWidget();
    hMdemScroll = new QVBoxLayout();
    mdemScroll->setWidget(mdemContainer);
    mdemScroll->setWidgetResizable(true);
    mdemContainer->setLayout(hMdemScroll);
    rightLayout->addWidget(mdemScroll);
	hMdemScroll->addItem(mdemSpacer);

    // Pagination 
    hSplitter->addWidget(leftWidget);
    hSplitter->addWidget(rightWidget);
    hSplitter->setStretchFactor(0, 1);
    hSplitter->setStretchFactor(1, 3);


	auto pagination = new QWidget();
	auto hPagination = new QHBoxLayout();
	pagination->setLayout(hPagination);

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

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

	for (int i = 0; i < PER_PAGE; i++) {
		auto elButton = new QToolButton();
		elButton->setText(QString("%1").arg(i+1));
		hPagination->addWidget(elButton);
        elButton->hide();
		QObject::connect(elButton, &QToolButton::clicked, [elButton](bool checked) {
            auto pageNum = std::stoi(elButton->text().toStdString().c_str());
            auto pageIdx = pageNum - 1;
            if (pageIdx < pages.size()) {
                SwitchPage(pageIdx);
            }
		});
        paginationButtons.push_back(elButton);
	}
    
	nextButton = new QToolButton();
	nextButton->setText(QString::fromStdString(">"));
	hPagination->addWidget(nextButton);
    nextButton->hide();
	QObject::connect(nextButton, &QToolButton::clicked, [](bool checked) {
        if (pages.size() > 0) {
            SwitchPage(currentPage + 1);
        }
    });

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

	initTrainWindow();
	QObject::connect(
		practice,
		&QToolButton::clicked,
		[](bool checked) {
			trainWindow->show();
			trainWindow->resize(600, 300);
			setQuestions(questions);
		}
	);

    window.setCentralWidget(hSplitter);
    window.show();

	if (currentPath.length() > 0) {
		pickDirectory(QString::fromStdString(currentPath));
	}

    return app.exec();
}
