#include "PgProc.h"
#include "std_utils.h"
#include "SqlFormattingUtils.h"
#include "PgDatabaseCatalog.h"
#include "PgLanguageContainer.h"
#include "PgTypeContainer.h"
#include <QStringBuilder>
#include <boost/assert.hpp>

namespace {

	Arg::Mode CharToArgMode(char c)
	{
		switch (c) {
		case 'i': return Arg::In;
		case 'o': return Arg::Out;
		case 'b': return Arg::InOut;
		case 'v': return Arg::Variadic;
		case 't': return Arg::Table;
		}
		throw std::runtime_error("Unexpected value for pg_proc.proargmodes");
	}

	QString ArgModeToString(Arg::Mode m)
	{
		switch (m) {
		case Arg::In:       return "IN";
		case Arg::Out:      return "OUT";
		case Arg::InOut:    return "INOUT";
		case Arg::Variadic: return "VARIADIC";
		case Arg::Table:    return "TABLE";
		}
		throw std::runtime_error("Unexpected value for Arg::Mode");
	}



	std::vector<QString> getArrayFromCommaSeparatedList(const QString &str)
	{
		std::vector<QString> res;
		const int len = str.length();
		if (len == 0)
			return res;

		// setup start state for parsing
		int index = 0, brackets = 0, start_array = 0;
		bool single_quote = false, double_quote = false;

		for(; index < len; index++) {
			QChar ch = str[index];
			if (!double_quote && ch == L'\'')
				single_quote = !single_quote;
			else if (!single_quote && ch == L'"')
				double_quote = !double_quote;
			else if (!double_quote && !single_quote && ch == L'(')
				brackets++;
			else if (!double_quote && !single_quote && ch == L')')
				brackets--;
			else if (!double_quote && !single_quote && brackets == 0 && ch == L',')	{
				if (index != start_array)
					res.push_back(str.mid(start_array, index - 1).trimmed());
				else
					res.push_back(QString());
				start_array = index + 1;
			}
		}
		if (double_quote || single_quote || brackets != 0)
			throw std::runtime_error("Error parsing comma seperated array");


		// Add last value to array
		res.push_back(str.right(start_array).trimmed());
		return res;
	}

}

void operator<<(ProcKind &s, const Pgsql::Value &v)
{
	const char *c = v.c_str();
	switch (*c) {
	case 'f':
		s = ProcKind::Function;
		break;
	case 'p':
		s = ProcKind::Procedure;
		break;
	case 'a':
		s = ProcKind::Aggregate;
		break;
	case 'w':
		s = ProcKind::Window;
		break;
	default:
		throw std::runtime_error("Unexpected value for ProcKind");
	}
}

void operator<<(ParallelMode &s, const Pgsql::Value &v)
{
	const char *c = v.c_str();
	switch (*c) {
	case 'u':
		s = ParallelMode::Unsafe;
		break;
	case 'r':
		s = ParallelMode::Restricted;
		break;
	case 's':
		s = ParallelMode::Safe;
		break;
	default:
		throw std::runtime_error("Unexpected value for ParallelMode");
	}
}

QString parallelModeToSql(ParallelMode pm, bool explicit_unsafe)
{
	switch (pm) {
	case ParallelMode::Unsafe:
		return explicit_unsafe ? "PARALLEL UNSAFE" : "";
	case ParallelMode::Restricted:
		return "PARALLEL RESTRICTED";
	case ParallelMode::Safe:
		return "PARALLEL SAFE";
	default:
		throw std::runtime_error("Unexpected value for pm in parallelModeToSql");
	}
}


void PgProc::setArgs(
	std::vector<Oid> argtypes,
	std::vector<Oid> allargtypes,
	std::vector<char> argmodes,
	std::vector<QString> argnames,
	std::optional<QString> argdefaults
	)
{
	// When all args are of type IN allargtypes is empty and only argtypes is filled
	const std::vector<Oid> &types = allargtypes.empty() ? argtypes : allargtypes;
	const size_t count = types.size();
	m_args.clear();
	m_args.reserve(count);
	for (size_t index = 0; index < count; ++index) {
		// we "forget" the default here because it is easier
		// to apply them in reverse order when we have already
		// filled the list
		m_args.emplace_back(
			types[index],
			CharToArgMode(value_or(argmodes, index, 'i')),
			value_or(argnames, index, QString()),
			""
			);
	}
	auto defaults_array = getArrayFromCommaSeparatedList(argdefaults.value_or(QString()));

	// Apply defaults from end to start to IN en INOUT parameters (others can't have a default)
	size_t arg_idx = m_args.size() - 1;
	for (auto def_iter = defaults_array.rbegin(); def_iter != defaults_array.rend(); ++def_iter) {
		// skip arguments with wrong mode
		while (m_args[arg_idx].mode != Arg::In && m_args[arg_idx].mode != Arg::InOut)
			if (arg_idx == 0) throw std::runtime_error("Error processing defaults");
			else --arg_idx;
		m_args[arg_idx].def = *def_iter;
	}
}

const std::vector<Arg>& PgProc::args() const
{
	return m_args;
}

QString PgProc::argListWithNames(bool multiline) const
{
	QString args;
	size_t nArgs = 0;

	auto&& types = catalog().types();

	for (auto&& arg_elem : m_args) {

		// All Table arguments lies at the end of the list
		// Do not include them as the part of the argument list
		if (arg_elem.mode == Arg::Table)
			break;

		nArgs++;
		if (args.length() > 0)
			args += (multiline) ? ",\n    " : ", ";

		QString arg;
		if (arg_elem.mode != Arg::In)
			arg += ArgModeToString(arg_elem.mode);

		if (!arg_elem.name.isEmpty()) {
			if (!arg.isEmpty())
				arg += " ";
			arg += quoteIdent(arg_elem.name);
		}

		if (!arg.isEmpty())
			arg += " ";
		arg += types->getByKey(arg_elem.type)->objectName();

		if (!arg_elem.def.isEmpty())
			arg += " DEFAULT " + arg_elem.def;

		args += arg;
	}

	if (multiline && nArgs > 1)
		args = "\n    " + args;

	return args;
}

// Return the signature arguments list. If forScript = true, we format the list
// appropriately for use in a SELECT script.
QString PgProc::argSigList(const bool forScript) const
{
	QString args;

	auto&& types = catalog().types();

	for (auto&& arg_elem : m_args) {
		// OUT parameters are not considered part of the signature, except for EDB-SPL,
		// although this is not true for EDB AS90 onwards..
		if (arg_elem.mode != Arg::Out && arg_elem.mode != Arg::Table) {
			if (args.length() > 0) {
				if (forScript)
					args += ",\n";
				else
					args += ", ";
			}

			QString typname = types->getByKey(arg_elem.type)->objectName();
			if (forScript)
				args += "    <" + typname + ">";
			else
				args += typname;
		}
	}
	return args;
}


QString PgProc::createSql() const
{
	if (createSqlCache.isEmpty()) {
		QString sql;

		if (isAggregate()) {
			/// \todo Support for aggregates
			createSqlCache = "-- aggregates not supported yet\n";
			return createSqlCache;
		}
		QString quoted_name = QString("%1(%2)").arg(fullyQualifiedQuotedObjectName(), argListWithNames(true));
		QString quoted_sig = QString("%1(%2)").arg(fullyQualifiedQuotedObjectName(), argSigList());
		if (isProcedure()) {
			//		P  CREATE [ OR REPLACE ] PROCEDURE
			//			name ( [ [ argmode ] [ argname ] argtype [ { DEFAULT | = } default_expr ] [, ...] ] )
			sql = QString("-- Procedure: %1\n\n"
						  "-- DROP PROCEDURE %1;\n\n"
						  "CREATE OR REPLACE PROCEDURE %2\n"
						  ).arg(quoted_sig, quoted_name);
		}
		else {
			// all other kinds are functions
			//		F CREATE [ OR REPLACE ] FUNCTION
			//			name ( [ [ argmode ] [ argname ] argtype [ { DEFAULT | = } default_expr ] [, ...] ] )
			//		F    [ RETURNS rettype
			/// \todo	F      | RETURNS TABLE ( column_name column_type [, ...] ) ]
			auto&& types = catalog().types();
			QString return_type = types->getByKey(rettype)->objectName();
			sql = QString("-- Function: %1\n\n"
						  "-- DROP FUNCTION %1;\n\n"
						  "CREATE OR REPLACE FUNCTION %2\n"
						  "  RETURNS %3"
						  ).arg(quoted_sig, quoted_name, return_type);
		}

		sql += " AS\n";
		auto language = catalog().languages()->getByKey(lang);
		BOOST_ASSERT(language != nullptr);

		if (language->isC()) {
			//			| AS 'obj_file', 'link_symbol'
			sql += escapeLiteral(bin) % ", " % escapeLiteral(src);
		}
		else {
			//			| AS 'definition'
			sql += dollarQuoteString(src);
		}

		//		  { LANGUAGE lang_name
		sql += "\n  LANGUAGE " % language->quotedObjectName();

		/// \todo			| TRANSFORM { FOR TYPE type_name } [, ... ]

		//		    | WINDOW
		if (isWindow())
			sql += " WINDOW";

		if (!isProcedure()) {
			//		F    | IMMUTABLE | STABLE | VOLATILE | [ NOT ] LEAKPROOF
			sql += " " % volatility();
			if (leakproof)
				sql += " LEAKPROOF";
			//		F    | CALLED ON NULL INPUT | RETURNS NULL ON NULL INPUT | STRICT
			// CALLED ON NULL INPUT is the default so we never specify this
			// RETURNS NULL ON NULL INPUT is an alias for STRICT so we use STRICT
			if (isstrict)
				sql += " STRICT"
						;
			/// \todo		F    | [ EXTERNAL ] SECURITY INVOKER | [ EXTERNAL ] SECURITY DEFINER
			//			if (GetSecureDefiner())
			//				sql += wxT(" SECURITY DEFINER");
			//		F    | PARALLEL { UNSAFE | RESTRICTED | SAFE }
			sql += " " % parallelModeToSql(parallel);
			//		F    | COST execution_cost
			sql += QString("\n  COST %1").arg(static_cast<double>(cost));
			//		F    | ROWS result_rows
			if (retset)
				sql += QString("n  ROWS %1").arg(static_cast<double>(rows));

		}



		sql += ";";

		//			| SET configuration_parameter { TO value | = value | FROM CURRENT }
		for (auto&& cfg : config) {
			auto before = cfg.section('=', 0, 0); // before first =
			auto after  = cfg.section('=', 1, -1); // everything after first =
			if (before != "search_path" && before != "temp_tablespaces")
				sql += QString("\nALTER FUNCTION %1 SET %2 TO '%3'").arg(quoted_sig, before, after);
			else
				sql += QString("\nALTER FUNCTION %1 SET %2 TO %3").arg(quoted_sig, before, after);
		}

//		sql += wxT("\n")
//			   +  GetGrant(wxT("X"), wxT("FUNCTION ") + qtSig);

		sql += "\n";
		sql += alterOwnerSql("FUNCTION " + quoted_sig);
		sql += "\n";
		sql += grantSql();

//		if (!GetComment().IsNull())
//		{
//			sql += wxT("COMMENT ON FUNCTION ") + qtSig
//				   + wxT(" IS ") + qtDbString(GetComment()) + wxT(";\n");
//		}

//		if (GetConnection()->BackendMinimumVersion(9, 1))
//			sql += GetSeqLabelsSql();

		createSqlCache = std::move(sql);
	}

	return createSqlCache;
}


QString PgProc::volatility() const
{
	switch (provolatile) {
	case 'i': return "IMMUTABLE";
	case 's': return "STABLE";
	case 'v': return "VOLATILE";
	}
	return "<unknown>";
}

QString PgProc::typeName() const
{
	switch (kind) {
	case ProcKind::Function:
	case ProcKind::Window:
		return "FUNCTION";
	case ProcKind::Procedure:
		return "PROCEDURE";
	case ProcKind::Aggregate:
		return "AGGREGATE";
	}
	throw std::runtime_error("Unexpected kind in PgProc::typeName()");
}

QString PgProc::aclAllPattern() const
{
	// X = execute
	return "X";
}
