#include "view_controller.hpp"

#include <stdexcept>
#include <iostream>

#include <QApplication>
#include <QDesktopServices>
#include <QUrl>

#include "../gui/call_tab.hpp"
#include "../gui/call_window.hpp"
#include "../gui/overview_panel.hpp"
#include "../gui/main_call_window.hpp"
#include "../gui/filter_call_tab.hpp"
#include "../gui/match_call_tab.hpp"
#include "../gui/image_call_tab.hpp"
#include "../impl/init.hpp"
#include "../impl/filter_call.hpp"
#include "../impl/match_call.hpp"
#include "../impl/single_image_call.hpp"
#include "../impl/data_controller.hpp"
#include "../qtutil/util.hpp"

namespace cvv
{
namespace controller
{

// It's only used for instatiating a QApplication.
// static char *emptyArray[] = {""};
static char *parameterSystemV[] = { new char[1]{ 0 }, nullptr };
static int parameterSystemC = 1;

ViewController::ViewController()
{
	impl::initializeFilterAndViews();
	if (!QApplication::instance())
	{
		auto tmp =
		    new QApplication{ parameterSystemC, parameterSystemV };
		ownsQApplication = true;
		(void)tmp;
	}
	ovPanel = new gui::OverviewPanel{ util::makeRef(*this) };
	mainWindow = new gui::MainCallWindow(util::makeRef(*this), 0, ovPanel);
	windowMap[0] = std::unique_ptr<gui::CallWindow>(mainWindow);
	max_window_id = 0;
	mainWindow->show();
}

ViewController::~ViewController()
{
	callTabMap.clear();
	windowMap.clear();
	windowMap.clear();
	if (ownsQApplication)
	{
		delete QApplication::instance();
	}
}

void ViewController::addCallType(const QString typeName, TabFactory constr)
{
	ViewController::callTabType[typeName] = constr;
}

std::unique_ptr<cvv::gui::FilterCallTab>
makeFilterCallTab(cvv::util::Reference<cvv::impl::Call> call)
{
	return cvv::util::make_unique<cvv::gui::FilterCallTab>(
	    *call.castTo<cvv::impl::FilterCall>());
}

std::unique_ptr<cvv::gui::MatchCallTab>
makeMatchCallTab(cvv::util::Reference<cvv::impl::Call> call)
{
	return cvv::util::make_unique<cvv::gui::MatchCallTab>(
	    *call.castTo<cvv::impl::MatchCall>());
}

std::unique_ptr<cvv::gui::ImageCallTab>
makeImageCallTab(cvv::util::Reference<cvv::impl::Call> call)
{
	return cvv::util::make_unique<cvv::gui::ImageCallTab>(
	    *call.castTo<cvv::impl::SingleImageCall>());
}

std::map<QString, TabFactory> ViewController::callTabType{
	{ "filter", makeFilterCallTab }, { "match", makeMatchCallTab },
	{ "singleImage", makeImageCallTab }
};

void ViewController::addCall(util::Reference<impl::Call> data)
{
	updateMode();
	if (mode == Mode::NORMAL)
	{
		ovPanel->addElement(*data);
		mainWindow->showOverviewTab();
	} 
	else if (mode == Mode::FAST_FORWARD)
	{
		ovPanel->addElementBuffered(*data);
	} 
}

void ViewController::exec()
{
	updateMode();
	if (mode == Mode::NORMAL)
	{
		QApplication::instance()->exec();
	}
}

impl::Call &ViewController::getCall(size_t id)
{
	return impl::dataController().getCall(id);
}

QString ViewController::getSetting(const QString &scope, const QString &key)
{
	return qtutil::getSetting(scope, key);
}

std::vector<util::Reference<gui::CallWindow>> ViewController::getTabWindows()
{
	std::vector<util::Reference<gui::CallWindow>> windows{};
	for (auto &it : windowMap)
	{
		windows.push_back(util::makeRef(*(it.second)));
	}
	return windows;
}

util::Reference<gui::MainCallWindow> ViewController::getMainWindow()
{
	return util::makeRef(*mainWindow);
}

void ViewController::moveCallTabToNewWindow(size_t tabId)
{
	if (!hasCall(tabId))
		return;
	auto newWindow = util::make_unique<gui::CallWindow>(
	    util::makeRef<ViewController>(*this), ++max_window_id);
	removeCallTab(tabId);
	newWindow->addTab(getCallTab(tabId));
	newWindow->show();
	if (doesShowExitProgramButton)
	{
		newWindow->showExitProgramButton();
	}
	windowMap[max_window_id] = std::move(newWindow);
	removeEmptyWindowsWithDelay();
}

void ViewController::moveCallTabToWindow(size_t tabId, size_t windowId)
{
	if (!hasCall(tabId))
		return;
	removeCallTab(tabId);
	auto tab = getCallTab(tabId);
	windowMap[windowId]->addTab(tab);
	removeEmptyWindowsWithDelay();
}

void ViewController::removeCallTab(size_t tabId, bool deleteIt, bool deleteCall, bool updateUI)
{
	auto *curWindow = getCurrentWindowOfTab(tabId);
	if (curWindow->hasTab(tabId))
	{
		getCurrentWindowOfTab(tabId)->removeTab(tabId);
		if (deleteIt)
		{
			callTabMap.erase(tabId);
		}
	}
	if (deleteCall && hasCall(tabId))
	{
		if (updateUI)
		{
			ovPanel->removeElement(tabId);
		}
		impl::dataController().removeCall(tabId);
	}
	removeEmptyWindowsWithDelay();
}

void ViewController::openHelpBrowser(const QString &topic)
{
	qtutil::openHelpBrowser(topic);
}

void ViewController::resumeProgramExecution()
{
	QApplication::instance()->exit();
}

void ViewController::setDefaultSetting(const QString &scope, const QString &key,
                                       const QString &value)
{
	qtutil::setDefaultSetting(scope, key, value);
}

void ViewController::setSetting(const QString &scope, const QString &key,
                                const QString &value)
{
	qtutil::setSetting(scope, key, value);
}

void ViewController::showCallTab(size_t tabId)
{
	auto *window = getCurrentWindowOfTab(tabId);
	window->showTab(tabId);
	window->setWindowState((window->windowState() & ~Qt::WindowMinimized) |
	                       Qt::WindowActive);
	window->raise();
}

void ViewController::showAndOpenCallTab(size_t tabId)
{
	auto curWindow = getCurrentWindowOfTab(tabId);
	if (!curWindow->hasTab(tabId))
	{
		moveCallTabToWindow(tabId, 0);
		curWindow = mainWindow;
	}
	curWindow->showTab(tabId);
}

void ViewController::openCallTab(size_t tabId)
{
	auto curWindow = getCurrentWindowOfTab(tabId);
	if (!curWindow->hasTab(tabId))
	{
		moveCallTabToWindow(tabId, 0);
		curWindow = mainWindow;
	}
}

void ViewController::showOverview()
{
	mainWindow->setWindowState(
	    (mainWindow->windowState() & ~Qt::WindowMinimized) |
	    Qt::WindowActive);
	mainWindow->raise();
	mainWindow->showOverviewTab();
}

gui::CallWindow *ViewController::getCurrentWindowOfTab(size_t tabId)
{
	for (auto &elem : windowMap)
	{
		if (elem.second->hasTab(tabId))
		{
			return elem.second.get();
		}
	}
	return mainWindow;
}

gui::CallTab *ViewController::getCallTab(size_t tabId)
{
	if (callTabMap.count(tabId) == 0)
	{
		auto *call = &(getCall(tabId));
		if (callTabType.count(call->type()) == 0)
		{
			throw std::invalid_argument{
				"no such type '" + call->type().toStdString() +
				"'"
			};
		}
		callTabMap[tabId] =
		    callTabType[call->type()](util::makeRef(*call));
	}
	return callTabMap[tabId].get();
}

void ViewController::removeWindowFromMaps(size_t windowId)
{
	if (windowMap.count(windowId) > 0)
	{
		windowMap[windowId].release();
		windowMap.erase(windowId);
	}
}

void ViewController::removeEmptyWindows()
{
	std::vector<size_t> remIds{};
	for (auto &elem : windowMap)
	{
		if (elem.second->tabCount() == 0 && elem.second->getId() != 0)
		{
			remIds.push_back(elem.first);
		}
	}
	for (auto windowId : remIds)
	{
		auto window = windowMap[windowId].release();
		windowMap.erase(windowId);
		window->deleteLater();
	}
	shouldRunRemoveEmptyWindows_ = false;
}

void ViewController::removeEmptyWindowsWithDelay()
{
	shouldRunRemoveEmptyWindows_ = true;
}

bool ViewController::shouldRunRemoveEmptyWindows()
{
	return shouldRunRemoveEmptyWindows_;
}

void ViewController::showExitProgramButton()
{
	for (auto &elem : windowMap)
	{
		elem.second->showExitProgramButton();
	}
	doesShowExitProgramButton = true;
}

bool ViewController::hasCall(size_t id)
{
	return impl::dataController().hasCall(id);
}

void ViewController::setMode(Mode newMode)
{
	mode = newMode;
	switch (newMode)
	{
	case Mode::NORMAL:
		break;
	case Mode::HIDE:
		hideAll();
		QApplication::instance()->exit();
		break;
	case Mode::FAST_FORWARD:
		if (!doesShowExitProgramButton)
		{
			QApplication::instance()->exit();
		}
		else
		{
			mode = Mode::NORMAL;
		}
		break;
	}
}

Mode ViewController::getMode()
{
	return mode;
}

void ViewController::updateMode()
{
	if (mode == Mode::FAST_FORWARD && hasFinalCall())
	{
		mode = Mode::NORMAL;
		ovPanel->flushElementBuffer();
	}
}

void ViewController::hideAll()
{
	for (auto &window : windowMap)
	{
		window.second->hide();
	}
}

bool ViewController::hasFinalCall()
{
	return doesShowExitProgramButton;
}
}
}