// SPDX-FileCopyrightText: 2012 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-2.0-or-later

#include "base/configwriter.hpp"
#include "base/exception.hpp"
#include <boost/regex.hpp>
#include <boost/algorithm/string/replace.hpp>
#include <set>
#include <iterator>

using namespace icinga;

void ConfigWriter::EmitBoolean(std::ostream& fp, bool val)
{
	fp << (val ? "true" : "false");
}

void ConfigWriter::EmitNumber(std::ostream& fp, double val)
{
	fp << std::fixed << val;
}

void ConfigWriter::EmitString(std::ostream& fp, const String& val)
{
	fp << "\"" << EscapeIcingaString(val) << "\"";
}

void ConfigWriter::EmitEmpty(std::ostream& fp)
{
	fp << "null";
}

void ConfigWriter::EmitArray(std::ostream& fp, int indentLevel, const Array::Ptr& val)
{
	fp << "[ ";
	EmitArrayItems(fp, indentLevel, val);
	if (val->GetLength() > 0)
		fp << " ";
	fp << "]";
}

void ConfigWriter::EmitArrayItems(std::ostream& fp, int indentLevel, const Array::Ptr& val)
{
	bool first = true;

	ObjectLock olock(val);
	for (const Value& item : val) {
		if (first)
			first = false;
		else
			fp << ", ";

		EmitValue(fp, indentLevel, item);
	}
}

void ConfigWriter::EmitScope(std::ostream& fp, int indentLevel, const Dictionary::Ptr& val,
	const Array::Ptr& imports, bool splitDot)
{
	fp << "{";

	if (imports && imports->GetLength() > 0) {
		ObjectLock xlock(imports);
		for (const Value& import : imports) {
			fp << "\n";
			EmitIndent(fp, indentLevel);
			fp << "import \"" << import << "\"";
		}

		fp << "\n";
	}

	if (val) {
		ObjectLock olock(val);
		for (const Dictionary::Pair& kv : val) {
			fp << "\n";
			EmitIndent(fp, indentLevel);

			if (splitDot) {
				std::vector<String> tokens = kv.first.Split(".");

				EmitIdentifier(fp, tokens[0], true);

				for (std::vector<String>::size_type i = 1; i < tokens.size(); i++) {
					fp << "[";
					EmitString(fp, tokens[i]);
					fp << "]";
				}
			} else
				EmitIdentifier(fp, kv.first, true);

			fp << " = ";
			EmitValue(fp, indentLevel + 1, kv.second);
		}
	}

	fp << "\n";
	EmitIndent(fp, indentLevel - 1);
	fp << "}";
}

void ConfigWriter::EmitValue(std::ostream& fp, int indentLevel, const Value& val)
{
	if (val.IsObjectType<Array>())
		EmitArray(fp, indentLevel, val);
	else if (val.IsObjectType<Dictionary>())
		EmitScope(fp, indentLevel, val);
	else if (val.IsObjectType<ConfigIdentifier>())
		EmitIdentifier(fp, static_cast<ConfigIdentifier::Ptr>(val)->GetName(), false);
	else if (val.IsString())
		EmitString(fp, val);
	else if (val.IsNumber())
		EmitNumber(fp, val);
	else if (val.IsBoolean())
		EmitBoolean(fp, val);
	else if (val.IsEmpty())
		EmitEmpty(fp);
}

void ConfigWriter::EmitRaw(std::ostream& fp, const String& val)
{
	fp << val;
}

void ConfigWriter::EmitIndent(std::ostream& fp, int indentLevel)
{
	for (int i = 0; i < indentLevel; i++)
		fp << "\t";
}

void ConfigWriter::EmitIdentifier(std::ostream& fp, const String& identifier, bool inAssignment)
{
	static std::set<String> keywords;
	static std::mutex mutex;

	{
		std::unique_lock<std::mutex> lock(mutex);
		if (keywords.empty()) {
			const std::vector<String>& vkeywords = GetKeywords();
			std::copy(vkeywords.begin(), vkeywords.end(), std::inserter(keywords, keywords.begin()));
		}
	}

	if (keywords.find(identifier) != keywords.end()) {
		fp << "@" << identifier;
		return;
	}

	boost::regex expr("^[a-zA-Z_][a-zA-Z0-9\\_]*$");
	boost::smatch what;
	if (boost::regex_search(identifier.GetData(), what, expr))
		fp << identifier;
	else if (inAssignment)
		EmitString(fp, identifier);
	else
		BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid identifier"));
}

void ConfigWriter::EmitConfigItem(std::ostream& fp, const String& type, const String& name, bool isTemplate,
	bool ignoreOnError, const Array::Ptr& imports, const Dictionary::Ptr& attrs)
{
	if (isTemplate)
		fp << "template ";
	else
		fp << "object ";

	EmitIdentifier(fp, type, false);
	fp << " ";
	EmitString(fp, name);

	if (ignoreOnError)
		fp << " ignore_on_error";

	fp << " ";
	EmitScope(fp, 1, attrs, imports, true);
}

void ConfigWriter::EmitComment(std::ostream& fp, const String& text)
{
	fp << "/* " << text << " */\n";
}

void ConfigWriter::EmitFunctionCall(std::ostream& fp, const String& name, const Array::Ptr& arguments)
{
	EmitIdentifier(fp, name, false);
	fp << "(";
	EmitArrayItems(fp, 0, arguments);
	fp << ")";
}

String ConfigWriter::EscapeIcingaString(const String& str)
{
	String result = str;
	boost::algorithm::replace_all(result, "\\", "\\\\");
	boost::algorithm::replace_all(result, "\n", "\\n");
	boost::algorithm::replace_all(result, "\t", "\\t");
	boost::algorithm::replace_all(result, "\r", "\\r");
	boost::algorithm::replace_all(result, "\b", "\\b");
	boost::algorithm::replace_all(result, "\f", "\\f");
	boost::algorithm::replace_all(result, "\"", "\\\"");
	return result;
}

const std::vector<String>& ConfigWriter::GetKeywords()
{
	static std::vector<String> keywords;
	static std::mutex mutex;
	std::unique_lock<std::mutex> lock(mutex);

	if (keywords.empty()) {
		keywords.emplace_back("object");
		keywords.emplace_back("template");
		keywords.emplace_back("include");
		keywords.emplace_back("include_recursive");
		keywords.emplace_back("include_zones");
		keywords.emplace_back("library");
		keywords.emplace_back("null");
		keywords.emplace_back("true");
		keywords.emplace_back("false");
		keywords.emplace_back("const");
		keywords.emplace_back("var");
		keywords.emplace_back("this");
		keywords.emplace_back("globals");
		keywords.emplace_back("locals");
		keywords.emplace_back("use");
		keywords.emplace_back("using");
		keywords.emplace_back("namespace");
		keywords.emplace_back("default");
		keywords.emplace_back("ignore_on_error");
		keywords.emplace_back("current_filename");
		keywords.emplace_back("current_line");
		keywords.emplace_back("apply");
		keywords.emplace_back("to");
		keywords.emplace_back("where");
		keywords.emplace_back("import");
		keywords.emplace_back("assign");
		keywords.emplace_back("ignore");
		keywords.emplace_back("function");
		keywords.emplace_back("return");
		keywords.emplace_back("break");
		keywords.emplace_back("continue");
		keywords.emplace_back("for");
		keywords.emplace_back("if");
		keywords.emplace_back("else");
		keywords.emplace_back("while");
		keywords.emplace_back("throw");
		keywords.emplace_back("try");
		keywords.emplace_back("except");
	}

	return keywords;
}

ConfigIdentifier::ConfigIdentifier(String identifier)
	: m_Name(std::move(identifier))
{ }

String ConfigIdentifier::GetName() const
{
	return m_Name;
}
