/*-------------------------------------------------------------------------
 *
 * deparse_text_search.c
 *	  All routines to deparse text search statements.
 *	  This file contains all entry points specific for text search statement deparsing.
 *
 * Copyright (c) Citus Data, Inc.
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "catalog/namespace.h"
#include "commands/defrem.h"
#include "utils/builtins.h"

#include "distributed/citus_ruleutils.h"
#include "distributed/deparser.h"
#include "distributed/listutils.h"

static void AppendDefElemList(StringInfo buf, List *defelems, char *objectName);

static void AppendStringInfoTokentypeList(StringInfo buf, List *tokentypes);
static void AppendStringInfoDictnames(StringInfo buf, List *dicts);


/*
 * DeparseCreateTextSearchConfigurationStmt returns the sql for a DefineStmt defining a
 * TEXT SEARCH CONFIGURATION
 *
 * Although the syntax is mutually exclusive on the two arguments that can be passed in
 * the deparser will syntactically correct multiple definitions if provided. *
 */
char *
DeparseCreateTextSearchConfigurationStmt(Node *node)
{
	DefineStmt *stmt = castNode(DefineStmt, node);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	const char *identifier = NameListToQuotedString(stmt->defnames);
	appendStringInfo(&buf, "CREATE TEXT SEARCH CONFIGURATION %s ", identifier);
	appendStringInfoString(&buf, "(");
	AppendDefElemList(&buf, stmt->definition, "CONFIGURATION");
	appendStringInfoString(&buf, ");");

	return buf.data;
}


/*
 * DeparseCreateTextSearchDictionaryStmt returns the sql for a DefineStmt defining a
 * TEXT SEARCH DICTIONARY
 *
 * Although the syntax is mutually exclusive on the two arguments that can be passed in
 * the deparser will syntactically correct multiple definitions if provided. *
 */
char *
DeparseCreateTextSearchDictionaryStmt(Node *node)
{
	DefineStmt *stmt = castNode(DefineStmt, node);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	const char *identifier = NameListToQuotedString(stmt->defnames);
	appendStringInfo(&buf, "CREATE TEXT SEARCH DICTIONARY %s ", identifier);
	appendStringInfoString(&buf, "(");
	AppendDefElemList(&buf, stmt->definition, "DICTIONARY");
	appendStringInfoString(&buf, ");");

	return buf.data;
}


/*
 * AppendDefElemList is a helper to append a comma separated list of definitions to a
 * define statement.
 *
 * The extra objectName parameter is used to create meaningful error messages.
 */
static void
AppendDefElemList(StringInfo buf, List *defelems, char *objectName)
{
	DefElem *defelem = NULL;
	bool first = true;
	foreach_declared_ptr(defelem, defelems)
	{
		if (!first)
		{
			appendStringInfoString(buf, ", ");
		}
		first = false;

		/*
		 * There are some operations that can omit the argument. In that case, we only use
		 * the defname.
		 *
		 * For example, omitting [ = value ] in the next query results in resetting the
		 * option to defaults:
		 * ALTER TEXT SEARCH DICTIONARY name ( option [ = value ] );
		 */
		if (defelem->arg == NULL)
		{
			appendStringInfo(buf, "%s", defelem->defname);
			continue;
		}

		/* extract value from defelem */
		const char *value = defGetString(defelem);

		/* stringify */
		appendStringInfo(buf, "%s = %s", defelem->defname, value);
	}
}


/*
 * DeparseDropTextSearchConfigurationStmt returns the sql representation for a DROP TEXT
 * SEARCH CONFIGURATION ... statment. Supports dropping multiple configurations at once.
 */
char *
DeparseDropTextSearchConfigurationStmt(Node *node)
{
	DropStmt *stmt = castNode(DropStmt, node);
	Assert(stmt->removeType == OBJECT_TSCONFIGURATION);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	appendStringInfoString(&buf, "DROP TEXT SEARCH CONFIGURATION ");
	List *nameList = NIL;
	bool first = true;
	foreach_declared_ptr(nameList, stmt->objects)
	{
		if (!first)
		{
			appendStringInfoString(&buf, ", ");
		}
		first = false;

		appendStringInfoString(&buf, NameListToQuotedString(nameList));
	}

	if (stmt->behavior == DROP_CASCADE)
	{
		appendStringInfoString(&buf, " CASCADE");
	}

	appendStringInfoString(&buf, ";");

	return buf.data;
}


/*
 * DeparseDropTextSearchDictionaryStmt returns the sql representation for a DROP TEXT SEARCH
 * DICTIONARY ... statment. Supports dropping multiple dictionaries at once.
 */
char *
DeparseDropTextSearchDictionaryStmt(Node *node)
{
	DropStmt *stmt = castNode(DropStmt, node);
	Assert(stmt->removeType == OBJECT_TSDICTIONARY);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	appendStringInfoString(&buf, "DROP TEXT SEARCH DICTIONARY ");
	List *nameList = NIL;
	bool first = true;
	foreach_declared_ptr(nameList, stmt->objects)
	{
		if (!first)
		{
			appendStringInfoString(&buf, ", ");
		}
		first = false;

		appendStringInfoString(&buf, NameListToQuotedString(nameList));
	}

	if (stmt->behavior == DROP_CASCADE)
	{
		appendStringInfoString(&buf, " CASCADE");
	}

	appendStringInfoString(&buf, ";");

	return buf.data;
}


/*
 * DeparseRenameTextSearchConfigurationStmt returns the sql representation of a ALTER TEXT
 * SEARCH CONFIGURATION ... RENAME TO ... statement.
 */
char *
DeparseRenameTextSearchConfigurationStmt(Node *node)
{
	RenameStmt *stmt = castNode(RenameStmt, node);
	Assert(stmt->renameType == OBJECT_TSCONFIGURATION);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	char *identifier = NameListToQuotedString(castNode(List, stmt->object));
	appendStringInfo(&buf, "ALTER TEXT SEARCH CONFIGURATION %s RENAME TO %s;",
					 identifier, quote_identifier(stmt->newname));

	return buf.data;
}


/*
 * DeparseRenameTextSearchDictionaryStmt returns the sql representation of a ALTER TEXT SEARCH
 * DICTIONARY ... RENAME TO ... statement.
 */
char *
DeparseRenameTextSearchDictionaryStmt(Node *node)
{
	RenameStmt *stmt = castNode(RenameStmt, node);
	Assert(stmt->renameType == OBJECT_TSDICTIONARY);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	char *identifier = NameListToQuotedString(castNode(List, stmt->object));
	appendStringInfo(&buf, "ALTER TEXT SEARCH DICTIONARY %s RENAME TO %s;",
					 identifier, quote_identifier(stmt->newname));

	return buf.data;
}


/*
 * DeparseAlterTextSearchConfigurationStmt returns the sql representation of any generic
 * ALTER TEXT SEARCH CONFIGURATION .... statement. The statements supported include:
 *  - ALTER TEXT SEARCH CONFIGURATIONS ... ADD MAPPING FOR [, ...] WITH [, ...]
 *  - ALTER TEXT SEARCH CONFIGURATIONS ... ALTER MAPPING FOR [, ...] WITH [, ...]
 *  - ALTER TEXT SEARCH CONFIGURATIONS ... ALTER MAPPING REPLACE ... WITH ...
 *  - ALTER TEXT SEARCH CONFIGURATIONS ... ALTER MAPPING FOR [, ...] REPLACE ... WITH ...
 *  - ALTER TEXT SEARCH CONFIGURATIONS ... DROP MAPPING [ IF EXISTS ] FOR ...
 */
char *
DeparseAlterTextSearchConfigurationStmt(Node *node)
{
	AlterTSConfigurationStmt *stmt = castNode(AlterTSConfigurationStmt, node);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	char *identifier = NameListToQuotedString(castNode(List, stmt->cfgname));
	appendStringInfo(&buf, "ALTER TEXT SEARCH CONFIGURATION %s", identifier);

	switch (stmt->kind)
	{
		case ALTER_TSCONFIG_ADD_MAPPING:
		{
			appendStringInfoString(&buf, " ADD MAPPING FOR ");
			AppendStringInfoTokentypeList(&buf, stmt->tokentype);

			appendStringInfoString(&buf, " WITH ");
			AppendStringInfoDictnames(&buf, stmt->dicts);

			break;
		}

		case ALTER_TSCONFIG_ALTER_MAPPING_FOR_TOKEN:
		{
			appendStringInfoString(&buf, " ALTER MAPPING FOR ");
			AppendStringInfoTokentypeList(&buf, stmt->tokentype);

			appendStringInfoString(&buf, " WITH ");
			AppendStringInfoDictnames(&buf, stmt->dicts);

			break;
		}

		case ALTER_TSCONFIG_REPLACE_DICT:
		case ALTER_TSCONFIG_REPLACE_DICT_FOR_TOKEN:
		{
			appendStringInfoString(&buf, " ALTER MAPPING");
			if (list_length(stmt->tokentype) > 0)
			{
				appendStringInfoString(&buf, " FOR ");
				AppendStringInfoTokentypeList(&buf, stmt->tokentype);
			}

			if (list_length(stmt->dicts) != 2)
			{
				elog(ERROR, "unexpected number of dictionaries while deparsing ALTER "
							"TEXT SEARCH CONFIGURATION ... ALTER MAPPING [FOR ...] REPLACE "
							"statement.");
			}

			appendStringInfo(&buf, " REPLACE %s",
							 NameListToQuotedString(linitial(stmt->dicts)));

			appendStringInfo(&buf, " WITH %s",
							 NameListToQuotedString(lsecond(stmt->dicts)));

			break;
		}

		case ALTER_TSCONFIG_DROP_MAPPING:
		{
			appendStringInfoString(&buf, " DROP MAPPING");

			if (stmt->missing_ok)
			{
				appendStringInfoString(&buf, " IF EXISTS");
			}

			appendStringInfoString(&buf, " FOR ");
			AppendStringInfoTokentypeList(&buf, stmt->tokentype);
			break;
		}

		default:
		{
			elog(ERROR, "unable to deparse unsupported ALTER TEXT SEARCH STATEMENT");
		}
	}

	appendStringInfoString(&buf, ";");

	return buf.data;
}


/*
 * DeparseAlterTextSearchConfigurationStmt returns the sql representation of any generic
 * ALTER TEXT SEARCH DICTIONARY .... statement. The statements supported include
 * - ALTER TEXT SEARCH DICTIONARY name ( option [ = value ] [, ... ] )
 */
char *
DeparseAlterTextSearchDictionaryStmt(Node *node)
{
	AlterTSDictionaryStmt *stmt = castNode(AlterTSDictionaryStmt, node);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	char *identifier = NameListToQuotedString(castNode(List, stmt->dictname));
	appendStringInfo(&buf, "ALTER TEXT SEARCH DICTIONARY %s ( ", identifier);

	AppendDefElemList(&buf, stmt->options, "DICTIONARY");
	appendStringInfoString(&buf, " );");
	return buf.data;
}


/*
 * DeparseAlterTextSearchConfigurationSchemaStmt returns the sql statement representing
 * ALTER TEXT SEARCH CONFIGURATION ... SET SCHEMA ... statements.
 */
char *
DeparseAlterTextSearchConfigurationSchemaStmt(Node *node)
{
	AlterObjectSchemaStmt *stmt = castNode(AlterObjectSchemaStmt, node);
	Assert(stmt->objectType == OBJECT_TSCONFIGURATION);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	appendStringInfo(&buf, "ALTER TEXT SEARCH CONFIGURATION %s SET SCHEMA %s;",
					 NameListToQuotedString(castNode(List, stmt->object)),
					 quote_identifier(stmt->newschema));

	return buf.data;
}


/*
 * DeparseAlterTextSearchDictionarySchemaStmt returns the sql statement representing ALTER TEXT
 * SEARCH DICTIONARY ... SET SCHEMA ... statements.
 */
char *
DeparseAlterTextSearchDictionarySchemaStmt(Node *node)
{
	AlterObjectSchemaStmt *stmt = castNode(AlterObjectSchemaStmt, node);
	Assert(stmt->objectType == OBJECT_TSDICTIONARY);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	appendStringInfo(&buf, "ALTER TEXT SEARCH DICTIONARY %s SET SCHEMA %s;",
					 NameListToQuotedString(castNode(List, stmt->object)),
					 quote_identifier(stmt->newschema));

	return buf.data;
}


/*
 * DeparseTextSearchConfigurationCommentStmt returns the sql statement representing
 * COMMENT ON TEXT SEARCH CONFIGURATION ... IS ...
 */
char *
DeparseTextSearchConfigurationCommentStmt(Node *node)
{
	CommentStmt *stmt = castNode(CommentStmt, node);
	Assert(stmt->objtype == OBJECT_TSCONFIGURATION);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	appendStringInfo(&buf, "COMMENT ON TEXT SEARCH CONFIGURATION %s IS ",
					 NameListToQuotedString(castNode(List, stmt->object)));

	if (stmt->comment == NULL)
	{
		appendStringInfoString(&buf, "NULL");
	}
	else
	{
		appendStringInfoString(&buf, quote_literal_cstr(stmt->comment));
	}

	appendStringInfoString(&buf, ";");

	return buf.data;
}


/*
 * DeparseTextSearchDictionaryCommentStmt returns the sql statement representing
 * COMMENT ON TEXT SEARCH DICTIONARY ... IS ...
 */
char *
DeparseTextSearchDictionaryCommentStmt(Node *node)
{
	CommentStmt *stmt = castNode(CommentStmt, node);
	Assert(stmt->objtype == OBJECT_TSDICTIONARY);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	appendStringInfo(&buf, "COMMENT ON TEXT SEARCH DICTIONARY %s IS ",
					 NameListToQuotedString(castNode(List, stmt->object)));

	if (stmt->comment == NULL)
	{
		appendStringInfoString(&buf, "NULL");
	}
	else
	{
		appendStringInfoString(&buf, quote_literal_cstr(stmt->comment));
	}

	appendStringInfoString(&buf, ";");

	return buf.data;
}


/*
 * AppendStringInfoTokentypeList specializes in adding a comma separated list of
 * token_tyoe's to TEXT SEARCH CONFIGURATION commands
 */
static void
AppendStringInfoTokentypeList(StringInfo buf, List *tokentypes)
{
	String *tokentype = NULL;
	bool first = true;
	foreach_declared_ptr(tokentype, tokentypes)
	{
		if (nodeTag(tokentype) != T_String)
		{
			elog(ERROR,
				 "unexpected tokentype for deparsing in text search configuration");
		}

		if (!first)
		{
			appendStringInfoString(buf, ", ");
		}
		first = false;

		appendStringInfoString(buf, strVal(tokentype));
	}
}


/*
 * AppendStringInfoDictnames specializes in appending a comma separated list of
 * dictionaries to TEXT SEARCH CONFIGURATION commands.
 */
static void
AppendStringInfoDictnames(StringInfo buf, List *dicts)
{
	List *dictNames = NIL;
	bool first = true;
	foreach_declared_ptr(dictNames, dicts)
	{
		if (!first)
		{
			appendStringInfoString(buf, ", ");
		}
		first = false;

		char *dictIdentifier = NameListToQuotedString(dictNames);
		appendStringInfoString(buf, dictIdentifier);
	}
}


/*
 * DeparseAlterTextSearchConfigurationOwnerStmt returns the sql statement representing
 * ALTER TEXT SEARCH CONFIGURATION ... ONWER TO ... commands.
 */
char *
DeparseAlterTextSearchConfigurationOwnerStmt(Node *node)
{
	AlterOwnerStmt *stmt = castNode(AlterOwnerStmt, node);
	Assert(stmt->objectType == OBJECT_TSCONFIGURATION);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	appendStringInfo(&buf, "ALTER TEXT SEARCH CONFIGURATION %s OWNER TO %s;",
					 NameListToQuotedString(castNode(List, stmt->object)),
					 RoleSpecString(stmt->newowner, true));

	return buf.data;
}


/*
 * DeparseAlterTextSearchDictionaryOwnerStmt returns the sql statement representing ALTER TEXT
 * SEARCH DICTIONARY ... ONWER TO ... commands.
 */
char *
DeparseAlterTextSearchDictionaryOwnerStmt(Node *node)
{
	AlterOwnerStmt *stmt = castNode(AlterOwnerStmt, node);
	Assert(stmt->objectType == OBJECT_TSDICTIONARY);

	StringInfoData buf = { 0 };
	initStringInfo(&buf);

	appendStringInfo(&buf, "ALTER TEXT SEARCH DICTIONARY %s OWNER TO %s;",
					 NameListToQuotedString(castNode(List, stmt->object)),
					 RoleSpecString(stmt->newowner, true));

	return buf.data;
}
