#include "SqlFormattingUtils.h"
#include <QStringBuilder>

#include <cassert>
#include "catalog/PgKeywordList.h"

#include "catalog/PgConstraint.h"
#include "catalog/PgAttributeContainer.h"
#include "catalog/PgClass.h"
#include "catalog/PgClassContainer.h"
#include "catalog/PgIndex.h"
#include "catalog/PgNamespace.h"
#include "catalog/PgNamespaceContainer.h"
#include "catalog/PgDatabaseCatalog.h"

namespace {

    QString escapeInternal(const QString &input, QChar	quote_char)
	{
        int	num_quotes = 0; /* single or double, depending on as_ident */
		const int len = input.length();
        for (int idx = 0; idx < len; ++idx)
            if (input[idx] == quote_char)
				++num_quotes;

		int output_size = len + num_quotes + 2; // + 2 for the quotes

        QString output;
		output.reserve(output_size);
		output += quote_char;
        if (num_quotes == 0) {
			output += input;
		}
		else {
			for (int idx = 0; idx < len; ++idx) {
				QChar c = input[idx];
				output += c;
                if (c == quote_char)
					output += c;
			}
		}
		output += quote_char;
		return output;
	}

}

QString escapeIdent(const QString &input)
{
    return escapeInternal(input, '"');
}

QString escapeLiteral(const QString &input)
{
    return escapeInternal(input, '\'');
}

//char *
//PQescapeInternal(PGconn *conn, const char *str, size_t len, bool as_ident)
//{
//	const char *s;
//	char	   *result;
//	char	   *rp;
//	int			num_quotes = 0; /* single or double, depending on as_ident */
//	int			num_backslashes = 0;
//	int			input_len;
//	int			result_size;
//	char		quote_char = as_ident ? '"' : '\'';

////	/* We must have a connection, else fail immediately. */
////	if (!conn)
////		return NULL;

//	/* Scan the string for characters that must be escaped. */
//	for (s = str; (s - str) < len && *s != '\0'; ++s)
//	{
//		if (*s == quote_char)
//			++num_quotes;
//		else if (*s == '\\')
//			++num_backslashes;
//		else if (IS_HIGHBIT_SET(*s))
//		{
//			int			charlen;

//			/* Slow path for possible multibyte characters */
//			charlen = pg_encoding_mblen(conn->client_encoding, s);

//			/* Multibyte character overruns allowable length. */
//			if ((s - str) + charlen > len || memchr(s, 0, charlen) != NULL)
//			{
//				printfPQExpBuffer(&conn->errorMessage,
//						  libpq_gettext("incomplete multibyte character\n"));
//				return NULL;
//			}

//			/* Adjust s, bearing in mind that for loop will increment it. */
//			s += charlen - 1;
//		}
//	}

//	/* Allocate output buffer. */
//	input_len = s - str;
//	result_size = input_len + num_quotes + 3;	/* two quotes, plus a NUL */
//	if (!as_ident && num_backslashes > 0)
//		result_size += num_backslashes + 2;
//	result = rp = (char *) malloc(result_size);
//	if (rp == NULL)
//	{
//		printfPQExpBuffer(&conn->errorMessage,
//						  libpq_gettext("out of memory\n"));
//		return NULL;
//	}

//	/*
//	 * If we are escaping a literal that contains backslashes, we use the
//	 * escape string syntax so that the result is correct under either value
//	 * of standard_conforming_strings.  We also emit a leading space in this
//	 * case, to guard against the possibility that the result might be
//	 * interpolated immediately following an identifier.
//	 */
//	if (!as_ident && num_backslashes > 0)
//	{
//		*rp++ = ' ';
//		*rp++ = 'E';1
//	}

//	/* Opening quote. */
//	*rp++ = quote_char;

//	/*
//	 * Use fast path if possible.
//	 *
//	 * We've already verified that the input string is well-formed in the
//	 * current encoding.  If it contains no quotes and, in the case of
//	 * literal-escaping, no backslashes, then we can just copy it directly to
//	 * the output buffer, adding the necessary quotes.
//	 *
//	 * If not, we must rescan the input and process each character
//	 * individually.
//	 */
//	if (num_quotes == 0 && (num_backslashes == 0 || as_ident))
//	{
//		memcpy(rp, str, input_len);
//		rp += input_len;
//	}
//	else
//	{
//		for (s = str; s - str < input_len; ++s)
//		{
//			if (*s == quote_char || (!as_ident && *s == '\\'))
//			{
//				*rp++ = *s;
//				*rp++ = *s;
//			}
//			else if (!IS_HIGHBIT_SET(*s))
//				*rp++ = *s;
//			else
//			{
//				int			i = pg_encoding_mblen(conn->client_encoding, s);

//				while (1)
//				{
//					*rp++ = *s;
//					if (--i == 0)
//						break;
//					++s;		/* for loop will provide the final increment */
//				}
//			}
//		}
//	}

//	/* Closing quote and terminating NUL. */
//	*rp++ = quote_char;
//	*rp = '\0';

//	return result;
//}


bool identNeedsQuotes(QString ident)
{
	if (ident[0].isDigit())
		return true;
	for (auto c : ident)
		if ((c < 'a' || c > 'z') && c != '_' && (c < '0' || c > '9'))
			return true;

	auto kw = getPgsqlKeyword(ident);
	if (kw == nullptr)
		return false;
	else if (kw->getCategory() == UNRESERVED_KEYWORD)
		return false;
//	if (forTypes && sk->category == COL_NAME_KEYWORD)
//		return false;

	return true;
}

QString quoteIdent(QString ident)
{
	assert(ident.length() > 0);

	static const wchar_t dquote = L'"';

	if (identNeedsQuotes(ident)) {
		QString out;
		out += dquote;
		out += ident.replace("\"", "\"\"");
		out += dquote;
		return out;
	}
	else
		return ident;
}


QString dollarQuoteString(const QString &value)
{
	QString def_tag = "BODY";
	QString tag = QString("$%1$").arg(def_tag);

	int counter = 1;
	while (value.indexOf(tag) >= 0)
		tag = QString("$%1%2$").arg(def_tag, counter++);


	return tag
		   +  value
		   +  tag;
}



//QString genSchemaPrefix(const PgNamespace &ns)
//{
//	QString str;
//	if (!ns.name.isEmpty()) {
//		str = quoteIdent(ns.name) % QString::fromUtf16(u".");
//	}
//	return str;
//}


//QString genFQTableName(const PgDatabaseCatalog &catalog, const PgClass &cls)
//{
//	auto ns = catalog.namespaces()->getByKey(cls.relnamespace);
////cls.fullyQualifiedQuotedObjectName()
//	return ns->quotedObjectName() % "." % cls.quotedObjectName();
//}

QString genAlterTable(const PgDatabaseCatalog &, const PgClass &cls)
{
	return "ALTER TABLE " % cls.fullyQualifiedQuotedObjectName(); // genFQTableName(catalog, cls);
}

QString getDropConstraintDefinition(const PgDatabaseCatalog &catalog, const PgConstraint &constraint)
{
	const PgClass *cls = catalog.classes()->getByKey(constraint.relid);
	return genAlterTable(catalog, *cls) % " DROP CONSTRAINT " % quoteIdent(constraint.objectName()) % ";";
}


/// Helper class that builds comma seperated identifier list
class IdentListString {
public:
	void add(QString ident)
	{
		if (!m_str.isEmpty())
			m_str += ",";
		m_str += quoteIdent(ident);
	}
	const QString& str() const { return m_str; }
private:
	QString m_str;
};

QString getColumnNameList(const PgDatabaseCatalog &catalog, Oid relid, const SmallAttNumVec<5> &attnums)
{
	IdentListString result;

	const auto ac = catalog.attributes();
	for (auto an : attnums) {
		result.add(ac->getByKey({ relid, an })->name);
	}
	return result.str();
}

QString getForeignKeyConstraintDefinition(const PgDatabaseCatalog &catalog, const PgConstraint &constraint, const QString &indent)
{
	//PgClass cls = catalog.classes()->getByKey(constraint.relid);
	const PgClass *fcls = catalog.classes()->getByKey(constraint.frelid);
	QString deferrable;
	QString validated;
	if (!constraint.validated)
		validated += " NOT VALID";
	if (constraint.deferrable) {
		deferrable = QLatin1String(" DEFERRABLE INITIALLY ") % (constraint.deferred ? "DEFERRED" : "IMMEDIATE");
	}

	return "\n" + indent + "FOREIGN KEY ("
			% getColumnNameList(catalog, constraint.relid, constraint.key) % ")\n" + indent + "REFERENCES "
			% fcls->fullyQualifiedQuotedObjectName() % " ("
			% getColumnNameList(catalog, constraint.frelid, constraint.fkey) % ")\n" + indent + "MATCH "
			% ForeignKeyMatchToString(constraint.fmatchtype)
			%  " ON UPDATE " % ForeignKeyActionToString(constraint.fupdtype)
			%  " ON DELETE " % ForeignKeyActionToString(constraint.fdeltype)
			% deferrable % validated;
}

QString getForeignKeyConstraintReferences(const PgDatabaseCatalog &catalog, const PgConstraint &constraint)
{
	const PgClass *fcls = catalog.classes()->getByKey(constraint.frelid);
	QString deferrable;
	QString validated;
	if (!constraint.validated)
		validated += " NOT VALID";
	if (constraint.deferrable) {
		deferrable = QLatin1String(" DEFERRABLE INITIALLY ") % (constraint.deferred ? "DEFERRED" : "IMMEDIATE");
	}

	return "REFERENCES "
			% fcls->fullyQualifiedQuotedObjectName() % " ("
			% getColumnNameList(catalog, constraint.frelid, constraint.fkey) % ") MATCH "
			% ForeignKeyMatchToString(constraint.fmatchtype)
			%  " ON UPDATE " % ForeignKeyActionToString(constraint.fupdtype)
			%  " ON DELETE " % ForeignKeyActionToString(constraint.fdeltype)
			% deferrable % validated;

}


QString getForeignKeyConstraintReferencesShort(const PgDatabaseCatalog &catalog, const PgConstraint &constraint)
{
	const PgClass *fcls = catalog.classes()->getByKey(constraint.frelid);
	QString deferrable;
	QString validated;
	if (!constraint.validated)
		validated += " NOT VALID";
	if (constraint.deferrable) {
		deferrable = QLatin1String(" DEFERRABLE") % (constraint.deferred ? " INITIALLY DEFERRED" : "");
	}
	QString on_update = constraint.fupdtype == ForeignKeyAction::NoAction ? QString() : " ON UPDATE " % ForeignKeyActionToString(constraint.fupdtype);
	QString on_delete = constraint.fdeltype == ForeignKeyAction::NoAction ? QString() : " ON DELETE " % ForeignKeyActionToString(constraint.fdeltype);
	QString match_type = constraint.fmatchtype == ForeignKeyMatch::Simple ? QString() : " MATCH " % ForeignKeyMatchToString(constraint.fmatchtype);

	return fcls->fullyQualifiedQuotedObjectName() % " ("
			% getColumnNameList(catalog, constraint.frelid, constraint.fkey) % ")"
			% match_type
			% on_update
			% on_delete
			% deferrable % validated;

}

QString getPrimaryKeyConstraintDefinition(const PgDatabaseCatalog &catalog, const PgConstraint &constraint)
{
	QString ddl = " PRIMARY KEY ("
			% getColumnNameList(catalog, constraint.relid, constraint.key) % ")";

	return ddl;
}

QString getUniqueConstraintDefinition(const PgDatabaseCatalog &catalog, const PgConstraint &constraint)
{
	QString ddl = " UNIQUE ("
			% getColumnNameList(catalog, constraint.relid, constraint.key) % ")";

	return ddl;
}

QString getAlterTableConstraintDefinition(const PgDatabaseCatalog &catalog, const PgConstraint &constraint)
{
	const PgClass *cls = catalog.classes()->getByKey(constraint.relid);
	QString result = genAlterTable(catalog, *cls) % "\n  ADD ";
	result += getConstraintDefinition(catalog, constraint, "    ") % ";";
	return result;
}

QString getConstraintDefinition(const PgDatabaseCatalog &catalog, const PgConstraint &constraint, const QString &indent)
{
	QString result = "CONSTRAINT " % quoteIdent(constraint.objectName());
	switch (constraint.type) {
	case ConstraintType::ForeignKey:
		result += getForeignKeyConstraintDefinition(catalog, constraint, indent);
		break;
	case ConstraintType::PrimaryKey:
		result += getPrimaryKeyConstraintDefinition(catalog, constraint);
		break;
	case ConstraintType::Unique:
		result += getUniqueConstraintDefinition(catalog, constraint);
		break;
	case ConstraintType::Check:
	default:
		result = result % " " % constraint.definition;
		break;
	}
	return result;
}

