#include "QueryTool.h"
#include "ui_QueryTab.h"
#include "util/SqlSyntaxHighlighter.h"
#include <QStandardPaths>
#include <QPushButton>
#include <QAction>
#include <QFileDialog>
#include <QMessageBox>
#include <QStatusBar>
#include <QStringConverter>
#include <QTabWidget>
#include <QTextDocumentFragment>
#include <QTextStream>
#include <QClipboard>
#include "ExplainTreeModelItem.h"
#include "json/json.h"
#include "OpenDatabase.h"
#include "catalog/PgDatabaseCatalog.h"
#include "Pgsql_PgException.h"
#include "QueryParamListController.h"
#include "util.h"
#include "UserConfiguration.h"
#include "IDatabaseWindow.h"


QueryTool::QueryTool(IDatabaseWindow *context, QWidget *parent)
	: ManagedPage(parent)
	, m_context(context)
	, ui(new Ui::QueryTab)
	, m_dbConnection()
{
	ui->setupUi(this);

	auto db = context->openDatabase();
	m_config = db->config();
	m_catalog = db->catalog();

	connect(&m_dbConnection, &ASyncDBConnection::onStateChanged, this, &QueryTool::connectionStateChanged);
	connect(&m_dbConnection, &ASyncDBConnection::onNotice, this, &QueryTool::receiveNotice);
  
	ui->queryEdit->setFont(UserConfiguration::instance()->codeFont());

	highlighter = new SqlSyntaxHighlighter(ui->queryEdit->document());
	auto types = m_catalog->types();
	if (types) {
		highlighter->setTypes(*types);
	}

	connect(ui->queryEdit, &QPlainTextEdit::textChanged, this, &QueryTool::queryTextChanged);
    ui->queryEdit->setAcceptDrops(false);

	m_queryParamListController = new QueryParamListController(ui->paramTableView, m_context->openDatabase(), this);
	connect(ui->addButton, &QPushButton::clicked, m_queryParamListController,
        &QueryParamListController::on_addParam);
	connect(ui->removeButton, &QPushButton::clicked, m_queryParamListController,
        &QueryParamListController::on_removeParam);

	startConnect();
}

QueryTool::~QueryTool()
{
	delete ui;
}

bool QueryTool::CanClose(bool prompt_user)
{
	bool can_close;
	if (m_queryTextChanged) {
		if (prompt_user)
			can_close = continueWithoutSavingWarning();
		else
			can_close = false;
	}
	else {
		can_close = true;
	}
	return can_close;
}

void QueryTool::newdoc()
{
	ui->queryEdit->clear();
	setFileName(tr("new"));
    setQueryTextChanged(false);
	m_new = true;
}

bool QueryTool::load(const QString &filename)
{
	bool result = false;
	QFile file(filename);
	if (file.open(QIODevice::ReadOnly)) {
		QByteArray ba = file.readAll();
        auto toUtf16 = QStringDecoder(QStringDecoder::Utf8);
        QString text = toUtf16(ba);
        if (toUtf16.hasError()) {
			file.reset();
			QTextStream stream(&file);
			text = stream.readAll();
		}

		ui->queryEdit->setPlainText(text);
        setQueryTextChanged(false);
		setFileName(filename);
		m_new = false;
		result = true;
	}
	return result;
}

bool QueryTool::save()
{
	bool result;
	if (m_fileName.isEmpty() || m_new) {
		result = saveAs();
	}
	else {
		result = saveSqlTo(m_fileName);
	}
	return result;
}

bool QueryTool::saveAs()
{
	bool result = false;
	QString filename = promptUserForSaveSqlFilename();
	if (!filename.isEmpty()) {
		result = saveSqlTo(filename);
		if (result) {
			setFileName(filename);
			m_new = false;
		}
	}
	return result;
}

void QueryTool::saveCopyAs()
{
	QString filename = promptUserForSaveSqlFilename();
	if (!filename.isEmpty()) {
		saveSqlTo(filename);
	}
}

void QueryTool::execute()
{
	if (m_dbConnection.state() == ASyncDBConnection::State::Connected) {
		addLog("Query clicked");

		clearResult();
		ui->messagesEdit->clear();

		std::string cmd = getCommandUtf8();
		m_stopwatch.start();
        
		auto cb = [this](Expected<std::shared_ptr<Pgsql::Result>> res, qint64 elapsedms)
		{
            try
            {
                auto dbres = res.get();
                QMetaObject::invokeMethod(this, "query_ready",
                            Q_ARG(std::shared_ptr<Pgsql::Result>, dbres),
                                      Q_ARG(qint64, elapsedms));
            }
            catch (Pgsql::PgException &ex)
            {
                //QMessageBox::critical(this, "pglab", QString::fromStdString(ex.what()));
                QMetaObject::invokeMethod(this, [this, ex] ()
                {
                    QMessageBox::critical(this, "pglab", QString::fromStdString(ex.what()));
                });
            }


		};

		try {
			if (m_queryParamListController->empty())
				m_dbConnection.send(cmd, cb);
			else
				m_dbConnection.send(cmd, m_queryParamListController->params(), cb);
		}
		catch (const std::exception &ex) {
			QMessageBox msgBox;
			msgBox.setIcon(QMessageBox::Critical);
			msgBox.setText(QString("Error executing query: %1").arg(QString::fromUtf8(ex.what())));
			msgBox.setStandardButtons(QMessageBox::Close);
			msgBox.setDefaultButton(QMessageBox::Close);
			msgBox.exec();
		}
	}
}

void QueryTool::explain(bool analyze)
{
	ui->explainTreeView->setModel(nullptr);
	explainModel.reset();
	ui->messagesEdit->clear();

	addLog("Explain clicked");

	std::string analyze_str;
	if (analyze) {
		analyze_str = "ANALYZE, BUFFERS, ";
	}
	m_stopwatch.start();
	std::string cmd = "EXPLAIN (" + analyze_str + "VERBOSE, FORMAT JSON) " + getCommandUtf8();

	auto cb = [this](Expected<std::shared_ptr<Pgsql::Result>> exp_res, qint64 )
	{
		if (exp_res.valid()) {
			// Process explain data seperately
			auto res = exp_res.get();
			if (res) {
				std::thread([this,res]()
				{
					std::shared_ptr<ExplainRoot> explain;
					if (res->cols() == 1 && res->rows() == 1) {
						std::string s = res->val(0, 0);
						Json::Value root;   // will contains the root value after parsing.
						Json::Reader reader;
						bool parsingSuccessful = reader.parse(s, root);
						if (parsingSuccessful) {
							explain = ExplainRoot::createFromJson(root);
						}
					}
					QMetaObject::invokeMethod(this, "explain_ready",
									Q_ARG(ExplainRoot::SPtr, explain));
				}).detach();
			}
		}
	};

	if (m_queryParamListController->empty())
		m_dbConnection.send(cmd, cb);
	else
		m_dbConnection.send(cmd, m_queryParamListController->params(), cb);
}

void QueryTool::cancel()
{
	m_dbConnection.cancel();
}

QString QueryTool::title() const
{
	QFileInfo fileInfo(m_fileName);
	QString fn(fileInfo.fileName());
	return fn;
}

void QueryTool::setFileName(const QString &filename)
{
	m_fileName = filename;
    updateTitle();
}

void QueryTool::updateTitle()
{
    QFileInfo fileInfo(m_fileName);
    QString title = fileInfo.fileName();

    if (m_queryTextChanged) {
        title = "*" + title;
    }

    m_context->setTitleForWidget(this, title, m_fileName);
}

bool QueryTool::continueWithoutSavingWarning()
{
	QMessageBox msgBox;
	msgBox.setIcon(QMessageBox::Warning);
	msgBox.setText(QString("Save changes in document \"%1\" before closing?").arg(m_fileName));
	msgBox.setInformativeText("The changes will be lost when you choose Discard.");
	msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
	msgBox.setDefaultButton(QMessageBox::Cancel);
	int ret = msgBox.exec();
	if (ret == QMessageBox::Save) {
		if (!save()) {
			// save failed or was a saveAs and was cancelled, don't close!
			ret = QMessageBox::Cancel;
		}
	}

	return ret != QMessageBox::Cancel;
}

bool QueryTool::saveSqlTo(const QString &filename)
{
	bool result = false;
	QFileInfo fileinfo(filename);


	QFile file(filename);
	if (file.open(QIODevice::WriteOnly)) {
		QTextStream stream(&file);
        //stream.setCodec("utf-8");
        stream.setEncoding(QStringConverter::Utf8);
        QString text = ui->queryEdit->toPlainText();
		stream << text;

		stream.flush();
		if (stream.status() == QTextStream::Ok) {
            setQueryTextChanged(false);
			result = true;
		}
	}
	return result;
}


QString QueryTool::promptUserForSaveSqlFilename()
{
	QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory);
	return QFileDialog::getSaveFileName(this, tr("Save query"), home_dir, tr("SQL file (*.sql)"));
}

void QueryTool::queryTextChanged()
{
    setQueryTextChanged(true);
}

void QueryTool::connectionStateChanged(ASyncDBConnection::StateData state)
{
	QString iconname;
    switch (state.State) {
	case ASyncDBConnection::State::NotConnected:
        QMessageBox::warning(this, "pglab", tr("Warning connection and any of its session state has been lost"));
		startConnect();
		iconname = "red.png";
		break;
	case ASyncDBConnection::State::Connecting:
		iconname = "red.png";
		break;
	case ASyncDBConnection::State::Connected:
		iconname = "green.png";
		break;
	case ASyncDBConnection::State::QuerySend:
	case ASyncDBConnection::State::CancelSend:
		iconname = "yellow.png";
		break;
	case ASyncDBConnection::State::Terminating:
		break;
	}
	m_context->setIconForWidget(this, QIcon(":/icons/16x16/document_" + iconname));
}

void QueryTool::addLog(QString s)
{
	QTextCursor text_cursor = QTextCursor(ui->edtLog->document());
	text_cursor.movePosition(QTextCursor::End);
	text_cursor.insertText(s + "\r\n");
}

void QueryTool::receiveNotice(Pgsql::ErrorDetails notice)
{
	ui->messagesEdit->append(QString::fromStdString(notice.errorMessage));

	ui->messagesEdit->append(QString::fromStdString(notice.severity));
	ui->messagesEdit->append(QString("At position: %1").arg(notice.statementPosition));
	ui->messagesEdit->append(QString::fromStdString("State: " + notice.state));
	ui->messagesEdit->append(QString::fromStdString("Primary: " + notice.messagePrimary));
	ui->messagesEdit->append(QString::fromStdString("Detail: " + notice.messageDetail));
	ui->messagesEdit->append(QString::fromStdString("Hint: " + notice.messageHint));
	ui->messagesEdit->append(QString::fromStdString("Context: " + notice.context));
//	std::string state; ///< PG_DIAG_SQLSTATE Error code as listed in	https://www.postgresql.org/docs/9.5/static/errcodes-appendix.html
//	std::string severity;
//	std::string messagePrimary;
//	std::string messageDetail;
//	std::string messageHint;
//	int statementPosition; ///< First character is one, measured in characters not bytes!
	//	std::string context;

//	int internalPosition;
//	std::string internalQuery;
//	std::string schemaName;
//	std::string tableName;
//	std::string columnName;
//	std::string datatypeName;
//	std::string constraintName;
//	std::string sourceFile;
//	std::string sourceLine;
//	std::string sourceFunction;

//	QTextCursor cursor = ui->messagesEdit->textCursor();
//	cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor);

//	QTextTable *table = cursor.insertTable(4, 2);
//	if (table) {
//		table->cellAt(1, 0).firstCursorPosition().insertText("State");
//		table->cellAt(1, 1).firstCursorPosition().insertText(QString::fromStdString(notice.state));
//		table->cellAt(2, 0).firstCursorPosition().insertText("Primary");
//		table->cellAt(2, 1).firstCursorPosition().insertText(QString::fromStdString(notice.messagePrimary));
//		table->cellAt(3, 0).firstCursorPosition().insertText("Detail");
//		table->cellAt(3, 1).firstCursorPosition().insertText(QString::fromStdString(notice.messageDetail));
//	}

	// syntax error at or near "limit
	// statementPosition
}

void QueryTool::startConnect()
{
	m_dbConnection.setupConnection(m_config);
}

void QueryTool::explain_ready(ExplainRoot::SPtr explain)
{
	m_stopwatch.stop();
	if (explain) {
		addLog("Explain ready");
		QString times_str;
		if (explain->totalRuntime > 0.f)
			times_str = QString("Total time: %1").arg(
					msfloatToHumanReadableString(explain->totalRuntime));
		else
			times_str = QString("Execution time: %1, Planning time: %2").arg(
					msfloatToHumanReadableString(explain->executionTime)
					, msfloatToHumanReadableString(explain->planningTime));
		ui->lblTimes->setText(times_str);
		explainModel.reset(new QueryExplainModel(nullptr, explain));
		ui->explainTreeView->setModel(explainModel.get());
		ui->explainTreeView->expandAll();
		ui->explainTreeView->setColumnWidth(0, 200);
		ui->explainTreeView->setColumnWidth(1, 80);
		ui->explainTreeView->setColumnWidth(2, 80);
		ui->explainTreeView->setColumnWidth(3, 80);
		ui->explainTreeView->setColumnWidth(4, 80);
		ui->explainTreeView->setColumnWidth(5, 80);
		ui->explainTreeView->setColumnWidth(6, 600);
		ui->tabWidget->setCurrentWidget(ui->explainTab);

		m_context->showStatusBarMessage(tr("Explain ready."));

	}
	else {
		addLog(tr("Explain no result"));
		ui->tabWidget->setCurrentWidget(ui->messageTab);
		m_context->showStatusBarMessage(tr("Explain no result"));
	}
}

QString QueryTool::getCommand() const
{
	QString command;
	QTextCursor cursor = ui->queryEdit->textCursor();
	if (cursor.hasSelection()) {
		command = cursor.selection().toPlainText();
	}
	else {
		command = ui->queryEdit->toPlainText();
	}
	return command;
}

std::string QueryTool::getCommandUtf8() const
{
	return getCommand().toUtf8().data();
}

void QueryTool::query_ready(std::shared_ptr<Pgsql::Result> dbres, qint64 elapsedms)
{
    if (!dbres)
    {
        m_stopwatch.stop();
        addLog("query_ready with NO result");
        return;
    }

    addLog("query_ready with result");
    auto st = dbres->resultStatus();
    if (st == PGRES_TUPLES_OK) {
        //int n_rows = dbres->getRows();
        //QString rowcount_str = QString("rows: %1").arg(dbres->getRows());

        auto result_model = std::make_shared<QueryResultModel>(nullptr , dbres,
                                                    m_catalog);
        TuplesResultWidget *trw = new TuplesResultWidget;
        trw->setResult(result_model, elapsedms);
        resultList.push_back(trw);
        ui->tabWidget->addTab(trw, "Data");
        if (resultList.size() == 1)
            ui->tabWidget->setCurrentWidget(trw);

    }
    else {
        if (st == PGRES_COMMAND_OK) {
            int tuples_affected = dbres->tuplesAffected();
            QString msg;
            if (tuples_affected >= 0)
                msg = tr("Query returned succesfully: %1 rows affected, execution time %2")
                    .arg(QString::number(tuples_affected))
                    .arg(msfloatToHumanReadableString(elapsedms));
            else
                msg = tr("Query returned succesfully, execution time %1")
                    .arg(msfloatToHumanReadableString(elapsedms));

            ui->messagesEdit->append(msg);

            ui->tabWidget->setCurrentWidget(ui->messageTab);
        }
        else {
            ui->tabWidget->setCurrentWidget(ui->messageTab);
            auto details = dbres->diagDetails();
            markError(details);
            receiveNotice(details);
        }
    }
}

void QueryTool::markError(const Pgsql::ErrorDetails &details)
{
	if (details.statementPosition > 0) {
		QTextCursor cursor = ui->queryEdit->textCursor();

		// Following finds out the start of the current selection
		// theoreticallly the selection might have changed however
		// theoretically all the text might have changed also so we ignore
		// both issues for know and we solve both when we decide it is to much of
		// a problem but in practice syntax errors come back very quickly...
		int position_offset = 0;
		if (cursor.hasSelection()) {
			position_offset = cursor.selectionStart();
		}

		cursor.setPosition(details.statementPosition - 1 + position_offset);
		ui->queryEdit->setTextCursor(cursor);

		int length = 0;
		if (details.state == "42703") {
			std::size_t pos = details.messagePrimary.find('"');
			if (pos != std::string::npos) {
				std::size_t pos2 = details.messagePrimary.find('"', pos+1);
				if (pos2 != std::string::npos) {
					length = static_cast<int>(pos2 - pos);
				}
			}
		}
		else if (details.state == "42P01") {
			std::size_t pos = details.messagePrimary.find('"');
			if (pos != std::string::npos) {
				std::size_t pos2 = details.messagePrimary.find('"', pos+1);
				if (pos2 != std::string::npos) {
					length = static_cast<int>(pos2 - pos);
				}
			}
		}
		ui->queryEdit->addErrorMarker(details.statementPosition - 1 + position_offset, length);
	}
}

void QueryTool::clearResult()
{
	for (auto e : resultList)
		delete e;
	resultList.clear();
}

void QueryTool::copyQueryAsCString()
{
	QString command = getCommand();
	QString cs = ConvertToMultiLineCString(command);
	QApplication::clipboard()->setText(cs);
}

#include <codebuilder/CodeBuilder.h>
#include <codebuilder/DefaultConfigs.h>

void QueryTool::copyQueryAsRawCppString()
{
	QString command = getCommand();
	QString cs = ConvertToMultiLineRawCppString(command);
	QApplication::clipboard()->setText(cs);
}

void QueryTool::pasteLangString()
{
	QString s = QApplication::clipboard()->text();
	s = ConvertLangToSqlString(s);
	ui->queryEdit->insertPlainText(s);
}

void QueryTool::generateCode()
{
	QString command = getCommand();
	if (resultList.empty()) {
        QMessageBox::question(this, "pglab", tr("Please execute the query first"),
                    QMessageBox::StandardButtons(QMessageBox::Ok),
                    QMessageBox::Ok);
	}
	if (resultList.size() == 1) {
		std::shared_ptr<const Pgsql::Result> dbres = resultList[0]->GetPgsqlResult();
		m_context->newCodeGenPage(command, dbres);
	}
}

void QueryTool::exportDataToFilename(const QString &file_name)
{
	auto widget = ui->tabWidget->currentWidget();
	auto fi = std::find(resultList.begin(), resultList.end(), widget);
	if (fi != resultList.end()) {
		TuplesResultWidget* rw = *fi;
		rw->exportData(file_name);
	}
}

void QueryTool::focusEditor()
{
	ui->queryEdit->setFocus();
}

void QueryTool::exportData()
{
	QString home_dir = QStandardPaths::locate(QStandardPaths::HomeLocation, "", QStandardPaths::LocateDirectory);
	QString file_name = QFileDialog::getSaveFileName(this,
		tr("Export data"), home_dir, tr("CSV file (*.csv)"));
	exportDataToFilename(file_name);
}

void QueryTool::setQueryTextChanged(bool queryTextChanged)
{
    if (m_queryTextChanged != queryTextChanged) {
        m_queryTextChanged = queryTextChanged;
        updateTitle();
    }
}
