/*
* OpenClonk, http://www.openclonk.org
*
* Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
* Copyright (c) 2013, The OpenClonk Team and contributors
*
* Distributed under the terms of the ISC license; see accompanying file
* "COPYING" for details.
*
* "Clonk" is a registered trademark of Matthes Bender, used with permission.
* See accompanying file "TRADEMARK" for details.
*
* To redistribute this file separately, substitute the full license texts
* for the above references.
*/

/* "New scenario" editor dialogue */

#include "C4Include.h"
#include "script/C4Value.h"
#include "config/C4Config.h"
#include "editor/C4ConsoleQtNewScenario.h"


/* Definition file list model for new scenario definition selection */

C4ConsoleQtDefinitionFileListModel::DefFileInfo::DefFileInfo(C4ConsoleQtDefinitionFileListModel::DefFileInfo *parent, const char *filename, const char *root_path)
	: parent(parent), filename(filename), root_path(root_path), was_opened(false), is_root(false), user_selected(parent->IsUserSelected()), force_selected(parent->IsForceSelected())
{
	// Delay opening of groups until information is actually requested
	// Full names into child groups in C4S always delimeted with backslashes
	if (parent->full_filename.getLength())
		full_filename = parent->full_filename + R"(\)" + filename;
	else
		full_filename = filename;
}

C4ConsoleQtDefinitionFileListModel::DefFileInfo::DefFileInfo()
{
	// Init as root: List definitions in root paths
	// Objects.ocd is always there (even if not actually found) and always first
	DefFileInfo *main_objects_def = new DefFileInfo(this, C4CFN_Objects, "");
	children.emplace_back(main_objects_def);
	bool has_default_objects_found = false;
	for (auto & root_iter : ::Reloc)
	{
		const char *root = root_iter.strBuf.getData();
		for (DirectoryIterator def_file_iter(root); *def_file_iter; ++def_file_iter)
		{
			const char *def_file = ::GetFilename(*def_file_iter);
			if (WildcardMatch(C4CFN_DefFiles, def_file))
			{
				// Set path of main objects if found
				if (!has_default_objects_found && !strcmp(C4CFN_Objects, def_file))
				{
					main_objects_def->root_path.Copy(root);
					continue;
				}
				// Avoid duplicates on top level
				bool dup = false;
				for (auto & child : children)
					if (!strcmp(child->GetName(), def_file))
					{
						dup = true; break;
					}
				if (dup) continue;
				children.emplace_back(new DefFileInfo(this, def_file, root));
			}
		}
	}
}

void C4ConsoleQtDefinitionFileListModel::DefFileInfo::SetSelected(bool to_val, bool forced)
{
	if (forced)
		force_selected = to_val;
	else
		user_selected = to_val;
	// Selection propagates to children
	for (auto & child : children)
	{
		child->SetSelected(to_val, forced);
	}
}

bool C4ConsoleQtDefinitionFileListModel::DefFileInfo::OpenGroup()
{
	children.clear();
	was_opened = true; // mark as opened even if fails to prevent permanent re-loading of broken groups
	if (parent->IsRoot())
	{
		if (!grp.Open((root_path + DirectorySeparator + filename).getData())) return false;
	}
	else
	{
		if (!grp.OpenAsChild(&parent->grp, filename.getData())) return false;
	}
	// Init child array (without loading full groups)
	StdStrBuf child_filename;
	children.reserve(grp.EntryCount(C4CFN_DefFiles));
	grp.ResetSearch();
	while (grp.FindNextEntry(C4CFN_DefFiles, &child_filename))
		children.emplace_back(new DefFileInfo(this, child_filename.getData(), nullptr));
	return true;
}

int32_t C4ConsoleQtDefinitionFileListModel::DefFileInfo::GetChildCount()
{
	if (!was_opened) OpenGroup();
	return children.size();
}

C4ConsoleQtDefinitionFileListModel::DefFileInfo *C4ConsoleQtDefinitionFileListModel::DefFileInfo::GetChild(int32_t index)
{
	if (!was_opened) OpenGroup();
	if (index >= children.size()) return nullptr;
	return children[index].get();
}

int32_t C4ConsoleQtDefinitionFileListModel::DefFileInfo::GetChildIndex(const DefFileInfo *child)
{
	auto iter = std::find_if(children.begin(), children.end(),
		[child](std::unique_ptr<DefFileInfo> & item)->bool { return item.get() == child; });
	if (iter == children.end()) return -1; // not found
	return int32_t(iter - children.begin());
}

void C4ConsoleQtDefinitionFileListModel::DefFileInfo::AddUserSelectedDefinitions(std::list<const char *> *result) const
{
	// Add parent-most selected
	// Ignore any forced selection even if also selected by user.
	// It may have been selected first and then forced by the scenario preset after the template has been switched)
	if (!IsForceSelected())
	{
		if (IsUserSelected())
			result->push_back(full_filename.getData());
		else
			for (auto &iter : children) iter->AddUserSelectedDefinitions(result);
	}
}

void C4ConsoleQtDefinitionFileListModel::DefFileInfo::AddSelectedDefinitions(std::list<const char *> *result) const
{
	// Add parent-most selected
	if (IsSelected())
		result->push_back(full_filename.getData());
	else
		for (auto &iter : children) iter->AddSelectedDefinitions(result);
}

void C4ConsoleQtDefinitionFileListModel::DefFileInfo::SetForcedSelection(const char *selected_def_filepath)
{
	// Filenames are assumed to be case insensitive for the Windows client
	if (SEqualNoCase(selected_def_filepath, full_filename.getData()))
	{
		// This is the def to be force-selected
		SetSelected(true, true);
	}
	else if (is_root || (SEqual2NoCase(selected_def_filepath, full_filename.getData()) && selected_def_filepath[full_filename.getLength()] == '\\'))
	{
		// One of the child definitions should be force-selected
		if (!was_opened) OpenGroup();
		for (auto &iter : children) iter->SetForcedSelection(selected_def_filepath);
	}
}

void C4ConsoleQtDefinitionFileListModel::DefFileInfo::AddExtraDef(const char *def)
{
	assert(is_root);
	// Ignore if it was already added
	// Could also avoid adding child definitions if they are already in the list.
	// E.g. do not add both foo.ocs\bar.ocd and foo.ocs\bar.ocd\baz.ocd, but keep only the parent path.
	// But it's overkill for a case that will probably never happen and would pose just a minor nuisance if it does.
	for (auto &iter : children)
	{
		if (SEqualNoCase(iter->full_filename.getData(), def))
		{
			return;
		}
	}
	// Add using user path as root (extra defs will always be in the user path because they are not used by our main system templates)
	children.emplace_back(new DefFileInfo(this, def, ::Config.General.UserDataPath));
}


C4ConsoleQtDefinitionFileListModel::C4ConsoleQtDefinitionFileListModel() = default;

C4ConsoleQtDefinitionFileListModel::~C4ConsoleQtDefinitionFileListModel() = default;

void C4ConsoleQtDefinitionFileListModel::AddExtraDef(const char *def)
{
	root.AddExtraDef(def);
}

std::list<const char *> C4ConsoleQtDefinitionFileListModel::GetUserSelectedDefinitions() const
{
	std::list<const char *> result;
	root.AddUserSelectedDefinitions(&result);
	return result;
}

std::list<const char *> C4ConsoleQtDefinitionFileListModel::GetSelectedDefinitions() const
{
	std::list<const char *> result;
	root.AddSelectedDefinitions(&result);
	return result;
}

void C4ConsoleQtDefinitionFileListModel::SetForcedSelection(const std::list<const char *> &defs)
{
	beginResetModel();
	// Unselect previous
	root.SetSelected(false, true);
	// Force new selection
	for (const char *def : defs)
	{
		root.SetForcedSelection(def);
	}
	endResetModel();
}

int C4ConsoleQtDefinitionFileListModel::rowCount(const QModelIndex & parent) const
{
	if (!parent.isValid()) return root.GetChildCount();
	DefFileInfo *parent_def = static_cast<DefFileInfo *>(parent.internalPointer());
	if (!parent_def) return 0;
	return parent_def->GetChildCount();
}

int C4ConsoleQtDefinitionFileListModel::columnCount(const QModelIndex & parent) const
{
	return 1; // Name
}

QVariant C4ConsoleQtDefinitionFileListModel::data(const QModelIndex & index, int role) const
{
	DefFileInfo *def = static_cast<DefFileInfo *>(index.internalPointer());
	if (!def) return QVariant();
	// Query latest data from prop list
	if (role == Qt::DisplayRole)
	{
		return QString(def->GetName());
	}
	else if (role == Qt::CheckStateRole)
	{
		return def->IsSelected() ? Qt::Checked : Qt::Unchecked;
	}
	// Nothing to show
	return QVariant();
}

QModelIndex C4ConsoleQtDefinitionFileListModel::index(int row, int column, const QModelIndex &parent) const
{
	if (column) return QModelIndex();
	DefFileInfo *parent_def = &root;
	if (parent.isValid()) parent_def = static_cast<DefFileInfo *>(parent.internalPointer());
	if (!parent_def) return QModelIndex();
	return createIndex(row, column, parent_def->GetChild(row));
}

QModelIndex C4ConsoleQtDefinitionFileListModel::parent(const QModelIndex &index) const
{
	DefFileInfo *def = static_cast<DefFileInfo *>(index.internalPointer());
	if (!def) return QModelIndex();
	DefFileInfo *parent_def = def->GetParent();
	if (!parent_def) return QModelIndex();
	int32_t def_index = parent_def->GetChildIndex(def);
	if (def_index < 0) return QModelIndex(); // can't happen
	return createIndex(def_index, 0, parent_def);
}

Qt::ItemFlags C4ConsoleQtDefinitionFileListModel::flags(const QModelIndex &index) const
{
	Qt::ItemFlags flags = Qt::ItemIsSelectable | Qt::ItemIsUserCheckable;
	DefFileInfo *def = static_cast<DefFileInfo *>(index.internalPointer());
	if (def && !def->IsDisabled()) flags |= Qt::ItemIsEnabled;
	return flags;
}

bool C4ConsoleQtDefinitionFileListModel::setData(const QModelIndex& index, const QVariant& value, int role)
{
	// Adjust check-state
	if (role == Qt::CheckStateRole)
	{
		DefFileInfo *def = static_cast<DefFileInfo *>(index.internalPointer());
		if (def && !def->IsDisabled())
		{
			def->SetSelected(value.toBool(), false);
			// Update changed index and all children
			int32_t child_count = def->GetChildCount();
			QModelIndex end_changed = index;
			if (child_count) end_changed = createIndex(child_count - 1, 0, def->GetChild(child_count - 1));
			emit dataChanged(index, end_changed);
		}
	}
	return true;
}


/* New scenario dialogue */

C4ConsoleQtNewScenarioDlg::C4ConsoleQtNewScenarioDlg(class QMainWindow *parent_window)
	: QDialog(parent_window, Qt::WindowSystemMenuHint | Qt::WindowTitleHint | Qt::WindowCloseButtonHint)
	, has_custom_filename(false)
{
	ui.setupUi(this);
	adjustSize();
	setMinimumSize(size());
	// Create scenario at user path by default
	ui.filenameEdit->setText(::Config.General.UserDataPath);
	// Fill definition file model
	QItemSelectionModel *m = ui.definitionTreeView->selectionModel();
	ui.definitionTreeView->setModel(&def_file_model);
	delete m;
	// Init scenario template list
	InitScenarioTemplateList();
}

void C4ConsoleQtNewScenarioDlg::InitScenarioTemplateList()
{
	// Init template scenarios from user and system folder
	// Clear previous
	ui.templateComboBox->clear();
	C4Group system_templates, user_templates;
	system_templates.OpenAsChild(&::Application.SystemGroup, C4CFN_Template);
	user_templates.Open(Config.AtUserDataPath(C4CFN_Template));
	for (C4Group *template_group : { &system_templates, &user_templates })
	{
		if (template_group->IsOpen()) // open may have failed (e.g. if it doesn't exist)
		{
			// All scenarios within the template group are possible scenario templates
			template_group->ResetSearch();
			StdStrBuf template_filename;
			while (template_group->FindNextEntry(C4CFN_ScenarioFiles, &template_filename))
			{
				bool is_default = (template_group == &system_templates) && (template_filename == C4CFN_DefaultScenarioTemplate);
				AddScenarioTemplate(*template_group, template_filename.getData(), is_default);
			}
		}
	}
	// TODO could sort elements. But should usually be sorted within the packed groups anyway.
}

void C4ConsoleQtNewScenarioDlg::AddScenarioTemplate(C4Group &parent, const char *filename, bool is_default)
{
	// Load scenario information from group and add as template
	C4Group grp;
	if (!grp.OpenAsChild(&parent, filename)) return;
	C4Scenario template_c4s;
	if (!template_c4s.Load(grp)) return;
	// Title from file or scenario core
	C4ComponentHost title_file;
	StdCopyStrBuf title(template_c4s.Head.Title);
	C4Language::LoadComponentHost(&title_file, grp, C4CFN_Title, Config.General.LanguageEx);
	title_file.GetLanguageString(Config.General.LanguageEx, title);
	// Add it; remember full path as user data
	StdStrBuf template_path(grp.GetFullName());
	ui.templateComboBox->addItem(QString(title.getData()), QString(template_path.getData()));
	all_template_c4s.push_back(template_c4s);
	// Add any extra definition (e.g. pointing into a scenario) to selection model
	// "extra" definitions are those that use non-ocd-files anywhere in their path
	auto c4s_defs = template_c4s.Definitions.GetModulesAsList();
	for (const char *c4s_def : c4s_defs)
	{
		char c4s_def_component[_MAX_PATH + 1];
		int32_t i = 0;
		bool is_extra_def = false;
		while (SCopySegment(c4s_def, i++, c4s_def_component, '\\', _MAX_PATH))
		{
			if (!WildcardMatch(C4CFN_DefFiles, c4s_def_component))
			{
				is_extra_def = true;
				break;
			}
		}
		if (is_extra_def)
		{
			def_file_model.AddExtraDef(c4s_def);
		}
	}
	// Default selection
	if (is_default) ui.templateComboBox->setCurrentIndex(ui.templateComboBox->count()-1);
}

bool C4ConsoleQtNewScenarioDlg::IsHostAsNetwork() const
{
	return ui.startInNetworkCheckbox->isChecked();
}

void C4ConsoleQtNewScenarioDlg::SelectedTemplateChanged(int new_selection)
{
	// Update forced definition selection for template
	if (new_selection >= 0 && new_selection < all_template_c4s.size())
	{
		const C4Scenario &template_c4s = all_template_c4s[new_selection];
		def_file_model.SetForcedSelection(template_c4s.Definitions.GetModulesAsList());
	}
	else
	{
		def_file_model.SetForcedSelection(std::list<const char *>());
	}
}

bool C4ConsoleQtNewScenarioDlg::CreateScenario()
{
	// Try to create scenario from template. Unpack if necessery.
	QVariant tmpl_data = ui.templateComboBox->currentData();
	LogF(tmpl_data.toString().toUtf8());
	StdStrBuf template_filename;
	template_filename.Copy(tmpl_data.toString().toUtf8());
	if (DirectoryExists(template_filename.getData()))
	{
		if (!CopyDirectory(template_filename.getData(), filename.getData(), true))
		{
			return false;
		}
	}
	else
	{
		if (!C4Group_CopyItem(template_filename.getData(), filename.getData(), true, true))
		{
			return false;
		}
		if (!C4Group_UnpackDirectory(filename.getData()))
		{
			return false;
		}
	}
	C4Group grp;
	if (!grp.Open(filename.getData()))
	{
		return false;
	}
	// Remove localized title file to ensure it's loaded from the scenario core
	grp.DeleteEntry(C4CFN_WriteTitle);
	// Update scenario core with settings from dialogue
	C4Scenario c4s;
	if (!c4s.Load(grp)) return false;
	// Take over settings
	c4s.Landscape.MapWdt.SetConstant(ui.mapWidthSpinBox->value());
	c4s.Landscape.MapHgt.SetConstant(ui.mapHeightSpinBox->value());
	c4s.Landscape.MapZoom.SetConstant(ui.mapZoomSpinBox->value());
	c4s.Head.Title = ui.titleEdit->text().toStdString();
	c4s.Game.Mode.Copy(ui.gameModeComboBox->currentText().toUtf8());
	if (c4s.Game.Mode == "Undefined") c4s.Game.Mode.Clear();
	filename.Copy(ui.filenameEdit->text().toUtf8());
	std::list<const char *> definitions = def_file_model.GetUserSelectedDefinitions();
	StdStrBuf forced_definitions;
	c4s.Definitions.GetModules(&forced_definitions);
	const char *forced_definitions_c = forced_definitions.getData();
	std::ostringstream definitions_join(forced_definitions_c ? forced_definitions_c : nullptr, std::ostringstream::ate);
	if (definitions.size())
	{
		// definitions_join = definitions.join(";")
		if (forced_definitions.getLength())
		{
			// Combine both forced and user-selected definitions
			definitions_join << ";";
		}
		auto iter_end = definitions.end();
		std::copy(definitions.begin(), --iter_end, std::ostream_iterator<std::string>(definitions_join, ";"));
		definitions_join << *iter_end;
	}
	c4s.Definitions.SetModules(definitions_join.str().c_str());
	if (!c4s.Save(grp))
	{
		return false;
	}
	// Group saving not needed because it's unpacked.
	//if (!grp.Save()) return false;
	return true;
}

void C4ConsoleQtNewScenarioDlg::CreatePressed()
{
	// Check validity of settings
	if (!ui.titleEdit->text().length())
	{
		DoError(::LoadResStr("IDS_ERR_ENTERTITLE"));
		ui.titleEdit->setFocus();
		return;
	}
	if (ItemExists(filename.getData()))
	{
		DoError(::LoadResStr("IDS_ERR_NEWSCENARIOFILEEXISTS"));
		ui.titleEdit->setFocus();
		return;
	}
	std::list<const char *> definitions = def_file_model.GetSelectedDefinitions();
	if (definitions.size() > C4S_MaxDefinitions)
	{
		DoError(FormatString(::LoadResStr("IDS_ERR_TOOMANYDEFINITIONS"), (int)definitions.size(), (int)C4S_MaxDefinitions).getData());
		ui.definitionTreeView->setFocus();
		return;
	}
	if (!CreateScenario())
	{
		EraseItem(filename.getData());
		DoError(::LoadResStr("IDS_ERR_CREATESCENARIO"));
		ui.titleEdit->setFocus();
		return;
	}
	// Close dialogue with OK
	accept();
}

// Filter for allowed characters in filename
// (Also replace space, because spaces in filenames suk)
static char ReplaceSpecialFilenameChars(char c)
{
	const char *special_chars = R"(\/:<>|$?" )";
	return strchr(special_chars, c) ? '_' : c;
}

void C4ConsoleQtNewScenarioDlg::TitleChanged(const QString &new_title)
{
	if (!has_custom_filename)
	{
		// Default filename by title
		std::string filename = new_title.toStdString();
		std::transform(filename.begin(), filename.end(), filename.begin(), ReplaceSpecialFilenameChars);
		filename += (C4CFN_ScenarioFiles+1);
		const char *filename_full = Config.AtUserDataPath(filename.c_str());
		ui.filenameEdit->setText(filename_full);
		this->filename.Copy(filename_full);

	}
}

void C4ConsoleQtNewScenarioDlg::DoError(const char *msg)
{
	QMessageBox::critical(this, ::LoadResStr("IDS_ERR_TITLE"), QString(msg));
}

void C4ConsoleQtNewScenarioDlg::BrowsePressed()
{
	// Browse for new filename to be used instead of the filename generated from the title
	QString new_file;
	for (;;)
	{
		new_file = QFileDialog::getSaveFileName(this, LoadResStr("IDS_CNS_NEWSCENARIO"), Config.General.UserDataPath, QString("%1 (%2)").arg(LoadResStr("IDS_CNS_SCENARIOFILE")).arg(C4CFN_ScenarioFiles), nullptr, QFileDialog::DontConfirmOverwrite);
		if (!new_file.size()) return;
		// Extension must be .ocs
		if (!new_file.endsWith(C4CFN_ScenarioFiles + 1)) new_file += (C4CFN_ScenarioFiles + 1);
		if (!ItemExists(new_file.toUtf8())) break;
		// Overwriting of existing scenarios not supported
		QMessageBox::critical(this, ::LoadResStr("IDS_ERR_TITLE"), ::LoadResStr("IDS_ERR_NEWSCENARIOFILEEXISTS"));
	}
	filename.Copy(new_file.toUtf8());
	ui.filenameEdit->setText(filename.getData()); // set from converted filename just in case weird stuff happened in toUtf8
	// After setting a new filename, it no longer changes when changing the title
	has_custom_filename = true;
}
