/*-------------------------------------------------------------------------
 *
 * deparse_type_stmts.c
 *	  All routines to deparse type statements.
 *	  This file contains all entry points specific for type statement deparsing as well as
 *	  functions that are currently only used for deparsing of the type statements.
 *
 *	  Functions that could move later are AppendColumnDef, AppendColumnDefList, etc. These
 *	  should be reused across multiple statements and should live in their own deparse
 *	  file.
 *
 * Copyright (c) Citus Data, Inc.
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "catalog/namespace.h"
#include "lib/stringinfo.h"
#include "nodes/makefuncs.h"
#include "nodes/parsenodes.h"
#include "parser/parse_type.h"
#include "utils/builtins.h"

#include "distributed/citus_ruleutils.h"
#include "distributed/commands.h"
#include "distributed/deparser.h"
#include "distributed/metadata/namespace.h"

#define AlterEnumIsRename(stmt) (stmt->oldVal != NULL)
#define AlterEnumIsAddValue(stmt) (stmt->oldVal == NULL)

/* forward declaration for deparse functions */
static void AppendCompositeTypeStmt(StringInfo str, CompositeTypeStmt *stmt);
static void AppendColumnDef(StringInfo str, ColumnDef *columnDef);
static void AppendColumnDefList(StringInfo str, List *columnDefs);

static void AppendCreateEnumStmt(StringInfo str, CreateEnumStmt *stmt);
static void AppendStringList(StringInfo str, List *strings);

static void AppendDropTypeStmt(StringInfo buf, DropStmt *stmt);
static void AppendTypeNameList(StringInfo buf, List *objects);

static void AppendAlterEnumStmt(StringInfo buf, AlterEnumStmt *stmt);

static void AppendAlterTypeStmt(StringInfo buf, AlterTableStmt *stmt);
static void AppendAlterTypeCmd(StringInfo buf, AlterTableCmd *alterTableCmd);
static void AppendAlterTypeCmdAddColumn(StringInfo buf, AlterTableCmd *alterTableCmd);
static void AppendAlterTypeCmdDropColumn(StringInfo buf, AlterTableCmd *alterTableCmd);
static void AppendAlterTypeCmdAlterColumnType(StringInfo buf,
											  AlterTableCmd *alterTableCmd);

static void AppendRenameTypeStmt(StringInfo buf, RenameStmt *stmt);
static void AppendRenameTypeAttributeStmt(StringInfo buf, RenameStmt *stmt);
static void AppendAlterTypeSchemaStmt(StringInfo buf, AlterObjectSchemaStmt *stmt);
static void AppendAlterTypeOwnerStmt(StringInfo buf, AlterOwnerStmt *stmt);


/*
 * DeparseCompositeTypeStmt builds and returns a string representing the
 * CompositeTypeStmt for application on a remote server.
 */
char *
DeparseCompositeTypeStmt(Node *node)
{
	CompositeTypeStmt *stmt = castNode(CompositeTypeStmt, node);
	StringInfoData sql = { 0 };
	initStringInfo(&sql);

	AppendCompositeTypeStmt(&sql, stmt);

	return sql.data;
}


char *
DeparseCreateEnumStmt(Node *node)
{
	CreateEnumStmt *stmt = castNode(CreateEnumStmt, node);
	StringInfoData sql = { 0 };
	initStringInfo(&sql);

	AppendCreateEnumStmt(&sql, stmt);

	return sql.data;
}


char *
DeparseAlterEnumStmt(Node *node)
{
	AlterEnumStmt *stmt = castNode(AlterEnumStmt, node);
	StringInfoData sql = { 0 };
	initStringInfo(&sql);

	AppendAlterEnumStmt(&sql, stmt);

	return sql.data;
}


char *
DeparseDropTypeStmt(Node *node)
{
	DropStmt *stmt = castNode(DropStmt, node);
	StringInfoData str = { 0 };
	initStringInfo(&str);

	Assert(stmt->removeType == OBJECT_TYPE);

	AppendDropTypeStmt(&str, stmt);

	return str.data;
}


char *
DeparseAlterTypeStmt(Node *node)
{
	AlterTableStmt *stmt = castNode(AlterTableStmt, node);
	StringInfoData str = { 0 };
	initStringInfo(&str);

	Assert(stmt->relkind == OBJECT_TYPE);

	AppendAlterTypeStmt(&str, stmt);

	return str.data;
}


static void
AppendAlterTypeStmt(StringInfo buf, AlterTableStmt *stmt)
{
	const char *identifier = quote_qualified_identifier(stmt->relation->schemaname,
														stmt->relation->relname);
	ListCell *cmdCell = NULL;

	Assert(stmt->relkind == OBJECT_TYPE);

	appendStringInfo(buf, "ALTER TYPE %s", identifier);
	foreach(cmdCell, stmt->cmds)
	{
		if (cmdCell != list_head(stmt->cmds))
		{
			appendStringInfoString(buf, ", ");
		}

		AlterTableCmd *alterTableCmd = castNode(AlterTableCmd, lfirst(cmdCell));
		AppendAlterTypeCmd(buf, alterTableCmd);
	}

	appendStringInfoString(buf, ";");
}


static void
AppendAlterTypeCmd(StringInfo buf, AlterTableCmd *alterTableCmd)
{
	switch (alterTableCmd->subtype)
	{
		case AT_AddColumn:
		{
			AppendAlterTypeCmdAddColumn(buf, alterTableCmd);
			break;
		}

		case AT_DropColumn:
		{
			AppendAlterTypeCmdDropColumn(buf, alterTableCmd);
			break;
		}

		case AT_AlterColumnType:
		{
			AppendAlterTypeCmdAlterColumnType(buf, alterTableCmd);
			break;
		}

		default:
		{
			ereport(ERROR, (errmsg("unsupported subtype for alter table command"),
							errdetail("sub command type: %d", alterTableCmd->subtype)));
		}
	}
}


static void
AppendAlterTypeCmdAddColumn(StringInfo buf, AlterTableCmd *alterTableCmd)
{
	Assert(alterTableCmd->subtype == AT_AddColumn);

	appendStringInfoString(buf, " ADD ATTRIBUTE ");
	AppendColumnDef(buf, castNode(ColumnDef, alterTableCmd->def));
}


static void
AppendAlterTypeCmdDropColumn(StringInfo buf, AlterTableCmd *alterTableCmd)
{
	Assert(alterTableCmd->subtype == AT_DropColumn);
	appendStringInfo(buf, " DROP ATTRIBUTE %s", quote_identifier(alterTableCmd->name));

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


static void
AppendAlterTypeCmdAlterColumnType(StringInfo buf, AlterTableCmd *alterTableCmd)
{
	Assert(alterTableCmd->subtype == AT_AlterColumnType);
	appendStringInfo(buf, " ALTER ATTRIBUTE %s SET DATA TYPE ", quote_identifier(
						 alterTableCmd->name));
	AppendColumnDef(buf, castNode(ColumnDef, alterTableCmd->def));

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


static void
AppendAlterEnumStmt(StringInfo buf, AlterEnumStmt *stmt)
{
	appendStringInfo(buf, "ALTER TYPE %s", NameListToQuotedString(stmt->typeName));

	if (AlterEnumIsRename(stmt))
	{
		/* Rename an existing label */
		appendStringInfo(buf, " RENAME VALUE %s TO %s;",
						 quote_literal_cstr(stmt->oldVal),
						 quote_literal_cstr(stmt->newVal));
	}
	else if (AlterEnumIsAddValue(stmt))
	{
		/* Add a new label */
		appendStringInfoString(buf, " ADD VALUE ");
		if (stmt->skipIfNewValExists)
		{
			appendStringInfoString(buf, "IF NOT EXISTS ");
		}
		appendStringInfoString(buf, quote_literal_cstr(stmt->newVal));

		if (stmt->newValNeighbor)
		{
			appendStringInfo(buf, " %s %s",
							 stmt->newValIsAfter ? "AFTER" : "BEFORE",
							 quote_literal_cstr(stmt->newValNeighbor));
		}

		appendStringInfoString(buf, ";");
	}
}


static void
AppendDropTypeStmt(StringInfo buf, DropStmt *stmt)
{
	/*
	 * already tested at call site, but for future it might be collapsed in a
	 * DeparseDropStmt so be safe and check again
	 */
	Assert(stmt->removeType == OBJECT_TYPE);

	appendStringInfo(buf, "DROP TYPE ");
	if (stmt->missing_ok)
	{
		appendStringInfoString(buf, "IF EXISTS ");
	}
	AppendTypeNameList(buf, stmt->objects);
	if (stmt->behavior == DROP_CASCADE)
	{
		appendStringInfoString(buf, " CASCADE");
	}
	appendStringInfoString(buf, ";");
}


static void
AppendTypeNameList(StringInfo buf, List *objects)
{
	ListCell *objectCell = NULL;
	foreach(objectCell, objects)
	{
		TypeName *typeName = castNode(TypeName, lfirst(objectCell));
		Oid typeOid = LookupTypeNameOid(NULL, typeName, false);
		const char *identifier = format_type_be_qualified(typeOid);

		if (objectCell != list_head(objects))
		{
			appendStringInfo(buf, ", ");
		}

		appendStringInfoString(buf, identifier);
	}
}


/*
 * AppendCompositeTypeStmt appends the sql string to recreate a CompositeTypeStmt to the
 * provided buffer, ending in a ; for concatination of multiple statements.
 */
static void
AppendCompositeTypeStmt(StringInfo str, CompositeTypeStmt *stmt)
{
	const char *identifier = quote_qualified_identifier(stmt->typevar->schemaname,
														stmt->typevar->relname);
	appendStringInfo(str, "CREATE TYPE %s AS (", identifier);
	AppendColumnDefList(str, stmt->coldeflist);
	appendStringInfo(str, ");");
}


static void
AppendCreateEnumStmt(StringInfo str, CreateEnumStmt *stmt)
{
	RangeVar *typevar = makeRangeVarFromNameList(stmt->typeName);

	/* create the identifier from the fully qualified rangevar */
	const char *identifier = quote_qualified_identifier(typevar->schemaname,
														typevar->relname);

	appendStringInfo(str, "CREATE TYPE %s AS ENUM (", identifier);
	AppendStringList(str, stmt->vals);
	appendStringInfo(str, ");");
}


static void
AppendStringList(StringInfo str, List *strings)
{
	ListCell *stringCell = NULL;
	foreach(stringCell, strings)
	{
		const char *string = strVal(lfirst(stringCell));
		if (stringCell != list_head(strings))
		{
			appendStringInfoString(str, ", ");
		}

		string = quote_literal_cstr(string);
		appendStringInfoString(str, string);
	}
}


/*
 * AppendColumnDefList appends the definition of a list of ColumnDef items to the provided
 * buffer, adding separators as necessary.
 */
static void
AppendColumnDefList(StringInfo str, List *columnDefs)
{
	ListCell *columnDefCell = NULL;
	foreach(columnDefCell, columnDefs)
	{
		if (columnDefCell != list_head(columnDefs))
		{
			appendStringInfoString(str, ", ");
		}
		AppendColumnDef(str, castNode(ColumnDef, lfirst(columnDefCell)));
	}
}


/*
 * AppendColumnDef appends the definition of one ColumnDef completely qualified to the
 * provided buffer.
 *
 * If the colname is not set that part is ommitted. This is the case in alter column type
 * statements.
 */
static void
AppendColumnDef(StringInfo str, ColumnDef *columnDef)
{
	Oid typeOid = LookupTypeNameOid(NULL, columnDef->typeName, false);
	Oid collationOid = GetColumnDefCollation(NULL, columnDef, typeOid);

	Assert(!columnDef->is_not_null); /* not null is not supported on composite types */

	if (columnDef->colname)
	{
		appendStringInfo(str, "%s ", quote_identifier(columnDef->colname));
	}

	appendStringInfo(str, "%s", format_type_be_qualified(typeOid));

	if (OidIsValid(collationOid))
	{
		const char *identifier = FormatCollateBEQualified(collationOid);
		appendStringInfo(str, " COLLATE %s", identifier);
	}
}


char *
DeparseRenameTypeStmt(Node *node)
{
	RenameStmt *stmt = castNode(RenameStmt, node);
	StringInfoData str = { 0 };
	initStringInfo(&str);

	Assert(stmt->renameType == OBJECT_TYPE);

	AppendRenameTypeStmt(&str, stmt);

	return str.data;
}


static void
AppendRenameTypeStmt(StringInfo buf, RenameStmt *stmt)
{
	List *names = (List *) stmt->object;

	appendStringInfo(buf, "ALTER TYPE %s RENAME TO %s;", NameListToQuotedString(names),
					 quote_identifier(stmt->newname));
}


char *
DeparseRenameTypeAttributeStmt(Node *node)
{
	RenameStmt *stmt = castNode(RenameStmt, node);
	StringInfoData str = { 0 };
	initStringInfo(&str);

	Assert(stmt->renameType == OBJECT_ATTRIBUTE);
	Assert(stmt->relationType == OBJECT_TYPE);

	AppendRenameTypeAttributeStmt(&str, stmt);

	return str.data;
}


static void
AppendRenameTypeAttributeStmt(StringInfo buf, RenameStmt *stmt)
{
	appendStringInfo(buf, "ALTER TYPE %s RENAME ATTRIBUTE %s TO %s",
					 quote_qualified_identifier(stmt->relation->schemaname,
												stmt->relation->relname),
					 quote_identifier(stmt->subname),
					 quote_identifier(stmt->newname));

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

	appendStringInfoString(buf, ";");
}


char *
DeparseAlterTypeSchemaStmt(Node *node)
{
	AlterObjectSchemaStmt *stmt = castNode(AlterObjectSchemaStmt, node);
	StringInfoData str = { 0 };
	initStringInfo(&str);

	Assert(stmt->objectType == OBJECT_TYPE);

	AppendAlterTypeSchemaStmt(&str, stmt);

	return str.data;
}


static void
AppendAlterTypeSchemaStmt(StringInfo buf, AlterObjectSchemaStmt *stmt)
{
	Assert(stmt->objectType == OBJECT_TYPE);

	List *names = (List *) stmt->object;
	appendStringInfo(buf, "ALTER TYPE %s SET SCHEMA %s;", NameListToQuotedString(names),
					 quote_identifier(stmt->newschema));
}


char *
DeparseAlterTypeOwnerStmt(Node *node)
{
	AlterOwnerStmt *stmt = castNode(AlterOwnerStmt, node);
	StringInfoData str = { 0 };
	initStringInfo(&str);

	Assert(stmt->objectType == OBJECT_TYPE);

	AppendAlterTypeOwnerStmt(&str, stmt);

	return str.data;
}


static void
AppendAlterTypeOwnerStmt(StringInfo buf, AlterOwnerStmt *stmt)
{
	Assert(stmt->objectType == OBJECT_TYPE);

	List *names = (List *) stmt->object;
	appendStringInfo(buf, "ALTER TYPE %s OWNER TO %s;", NameListToQuotedString(names),
					 RoleSpecString(stmt->newowner, true));
}
