/**
 * Function to generate a documentation from database comments as Markdown for
 * a given Schema.
 * The result is a text in Markdown format.
 *
 * Currently the following objects are supported:
 * 	- Database
 * 	- Schema
 * 	- Tables with columns
 * 	- Views with columns
 * 	- Materialized Views with columns
 * 	- Foreign Tables with columns
 * 	- Functions
 * 	- Procedures
 *
 *
 * @author: "Stefanie Janine Stölting"<stefanie@proopensource.eu>
 * @license: PostgreSQL https://opensource.org/licenses/postgresql
 */

CREATE OR REPLACE FUNCTION get_markdown_doku_by_schema(in_schema_name TEXT, time_zone TEXT DEFAULT 'Europe/Berlin')
RETURNS TEXT
LANGUAGE PLPGSQL
AS
$$
DECLARE
	res TEXT DEFAULT '';
	current_table_name TEXT DEFAULT '';
	current_table_type TEXT DEFAULT '';
	current_program_name TEXT DEFAULT '';
	current_mv_name TEXT DEFAULT '';
	table_header TEXT;
	time_zone_exists BOOLEAN;
	curs_tables CURSOR FOR
		WITH indexes AS
			(
				SELECT schemaname
					, tablename
					, array_agg(indexname) AS indexes
				FROM pg_indexes
				GROUP BY schemaname
					, tablename
			)
		SELECT t.table_type
			, c.table_catalog AS database_name
			, c.table_schema AS schema_name
			, c.table_name
			, c.column_name
			, COALESCE (array_to_string (i.indexes, ';'), '') AS indexes
			, COALESCE (c.column_default, '') AS column_default
			, c.is_nullable
			, c.data_type
			, COALESCE (c.character_maximum_length::TEXT, '') AS character_maximum_length
			, COALESCE (de.description, '') AS database_description
			, COALESCE (obj_description(('"' || c.table_schema || '"')::regnamespace), '') AS schema_description
			, COALESCE (OBJ_DESCRIPTION(('"' || c.table_schema || '"."' || c.table_name || '"')::regclass), '') AS table_description
			, COALESCE (COL_DESCRIPTION(('"' || c.table_schema || '"."' || c.table_name || '"')::regclass, c.ordinal_position), '') AS column_description
		FROM information_schema.columns AS c
		INNER JOIN information_schema."tables" AS t
			USING (table_catalog, table_schema, table_name)
		INNER JOIN pg_catalog.pg_database AS d
			ON c.table_catalog = d.datname
		LEFT OUTER JOIN pg_catalog.pg_shdescription AS de
			ON d."oid" = de.objoid		LEFT OUTER JOIN indexes AS i
			ON c.table_schema = i.schemaname
			AND c.table_name = i.tablename
		WHERE c.table_schema = in_schema_name
		ORDER BY t.table_type
			, c.table_name
			, c.ordinal_position
		;
	table_record RECORD;
	curs_programs CURSOR FOR
		SELECT (current_database())::information_schema.sql_identifier AS database_name
			, pg_namespace.nspname AS schema_name
			, pg_proc.proname AS program_name
			, COALESCE (pg_catalog.pg_get_function_arguments(pg_proc.oid), '') AS parameters
			, COALESCE (pg_catalog.pg_get_function_result(pg_proc.oid), '') AS returning_data_type
			, CASE
					WHEN pg_proc.prokind = 'a' THEN
						'AGGREGATE'
					WHEN pg_proc.prokind = 'w' THEN
						'WINDOW'
					WHEN pg_proc.prokind = 'f' THEN
						'FUNCTION'
					WHEN pg_proc.prorettype = 'pg_catalog.trigger'::pg_catalog.regtype THEN
						'TRIGGER'
					ELSE
						'UNKNOWN'
				END AS program_type
			, pg_language.lanname AS program_language
			, COALESCE (pg_description.description, '') AS program_description
		FROM pg_catalog.pg_proc
		INNER JOIN pg_catalog.pg_language
			ON pg_proc.prolang = pg_language.oid
		LEFT OUTER JOIN pg_catalog.pg_namespace
			ON pg_proc.pronamespace = pg_namespace.oid
		LEFT OUTER JOIN pg_catalog.pg_description
			ON pg_proc.oid = pg_description.objoid
		WHERE pg_catalog.pg_function_is_visible(pg_proc.oid)
			AND pg_namespace.nspname = in_schema_name
		ORDER BY schema_name
			, program_name
			, parameters
		;
	program_record RECORD;
	curs_mv CURSOR FOR
		WITH indexes AS
			(
				SELECT schemaname
					, tablename
					, array_agg(indexname) AS indexes
				FROM pg_indexes
				GROUP BY schemaname
					, tablename
			)
		SELECT (current_database())::information_schema.sql_identifier AS database_name
			  , n.nspname AS schema_name
		    , c.relname AS table_name
		    , col.attname AS column_name
			, COALESCE (array_to_string (i.indexes, ';'), '') AS indexes
		    , format_type (col.atttypid, col.atttypmod) AS data_type
			, COALESCE (obj_description (c."oid"), '') AS materialized_view_description
			, COALESCE (COL_DESCRIPTION(('"' || n.nspname || '"."' || c.relname || '"')::regclass, col.attnum), '') AS column_description
		FROM pg_catalog.pg_class AS c
		INNER JOIN pg_catalog.pg_namespace AS n
			ON c.relnamespace = n.oid
		INNER JOIN pg_catalog.pg_attribute AS col
			ON c.oid = col.attrelid
		LEFT OUTER JOIN indexes AS i
			ON n.nspname = i.schemaname
			AND c.relname = i.tablename
		WHERE c.relkind = 'm'
		    AND col.attnum >= 1
		    AND n.nspname = in_schema_name
		ORDER BY schema_name
		    , table_name
		    , column_name
		;
	mv_record RECORD;
BEGIN
	-- Check given time zone for existence
	EXECUTE 'SELECT count (*) = 1 AS exist
		FROM pg_catalog.pg_timezone_names
		WHERE name::TEXT = $1'
		USING time_zone
		INTO time_zone_exists
	;

	IF NOT time_zone_exists THEN
		RAISE EXCEPTION 'Time zone % does not exist!', time_zone;
	END IF;

	table_header = '| Column | Default | Is Nullable | Data Type | Maximum Length | Description |' || CHR(13) || '|--|--|--|--|--|--|'  || CHR(13);

	-- TABLES and VIEWS
	OPEN curs_tables;

	FETCH NEXT FROM curs_tables INTO table_record;
	WHILE FOUND LOOP
		IF current_table_name = '' THEN
			res = '# DATABASE ' ||
				table_record.database_name || ' SCHEMA ' || table_record.schema_name ||
				CHR(13) || CHR(13) ||
				'Generated: ' || TO_CHAR (now() AT time ZONE time_zone, 'YYYY-MM-DD HH24:MI') ||
				CHR(13) || CHR(13) ||
				'## Database Description' ||
				CHR(13) || CHR(13) ||
				table_record.database_description ||
				CHR(13) || CHR(13) ||
				'## Schema Description' ||
				CHR(13) || CHR(13) ||
				table_record.schema_description ||
				CHR(13) || CHR(13)
			;

		END IF;

		IF current_table_type <> table_record.table_type THEN
			current_table_type = table_record.table_type;
			CASE table_record.table_type
				WHEN 'BASE TABLE' THEN
					res = res ||
						'## Tables in Schema ' || table_record.schema_name ||
						CHR(13) || CHR(13)
					;

				WHEN 'VIEW' THEN
					res = res ||
						'## Views in Schema ' || table_record.schema_name ||
						CHR(13) || CHR(13)
					;

				WHEN 'FOREIGN' THEN
					res = res ||
						'## Foreign Tables in Schema ' || table_record.schema_name ||
						CHR(13) || CHR(13)
					;
				ELSE
					res = res;
			END CASE;
		END IF;

		IF current_table_name <> table_record.table_name THEN
			current_table_name = table_record.table_name;
			res = res || '### ' || table_record.table_name
				|| CHR(13) || CHR(13)
			;

			res = res || table_record.table_description ||
				CHR(13) || CHR(13)
			;

			IF table_record.indexes <> '' THEN
				res = res || 'Indexes: ' || table_record.indexes ||
					CHR(13) || CHR(13)
				;
			END IF;

			res = res || table_header;

		END IF;

		res = res || '| ' || table_record.column_name || ' | ' ||
			table_record.column_default || ' | ' ||
			table_record.is_nullable || ' | ' ||
			table_record.data_type || ' | ' ||
			table_record.character_maximum_length || ' | ' ||
			table_record.column_description || ' |' ||
			CHR(13)
		;

		FETCH NEXT FROM curs_tables INTO table_record;
	END LOOP;

	CLOSE curs_tables;

	-- MATERIALIZED VIEWS
	OPEN curs_mv;

	FETCH NEXT FROM curs_mv INTO mv_record;
	WHILE FOUND LOOP

			IF current_mv_name = '' THEN
			res = res || CHR(13) ||
				'## MATERIALIZED VIEWS in DATABASE ' ||
				mv_record.database_name ||
				' SCHEMA ' ||
				mv_record.schema_name ||
				CHR(13) || CHR(13)
			;
		END IF;

		IF current_mv_name <> mv_record.table_name THEN
			current_mv_name = mv_record.table_name;
			res = res || '### ' || mv_record.table_name ||
				CHR(13) || CHR(13) ||
				mv_record.materialized_view_description ||
				CHR(13) || CHR(13)
			;

			IF mv_record.indexes <> '' THEN
				res = res || 'Indexes: ' || mv_record.indexes ||
					CHR(13) || CHR(13)
				;
			END IF;

			res = res || '| Column | Data Type |  | Description |' || CHR(13) ||
				'|--|--|--|'  || CHR(13)
			;
		END IF;


		res = res || '| ' || mv_record.column_name || ' | ' ||
			mv_record.data_type || ' | ' ||
			mv_record.column_description || ' |' ||
			CHR(13)
		;

		FETCH NEXT FROM curs_mv INTO mv_record;
	END LOOP;

	CLOSE curs_mv;

	-- PROGRAMS AND FUNCTIONS
	OPEN curs_programs;
	FETCH NEXT FROM curs_programs INTO program_record;
	WHILE FOUND LOOP


		IF current_program_name = '' THEN
			current_program_name = program_record.program_name;
			res = res || CHR(13) || '## FUNCTIONS and PROCEDURES in DATABASE ' || program_record.database_name || ' SCHEMA ' || program_record.schema_name ||
				CHR(13) || CHR(13) ||
				'| Program Name | Type | Language | Parameters | Returning | Description |' || CHR(13) ||
				'|--|--|--|--|--|--|' || CHR(13)
			;
		END IF;
		res = res || '| ' || program_record.program_name || ' | ' ||
			program_record.program_type || ' | ' ||
			program_record.program_language || ' | ' ||
			program_record.parameters || ' | ' ||
			program_record.returning_data_type || ' |' ||
			program_record.program_description || ' | ' ||
			CHR(13)
		;

		FETCH NEXT FROM curs_programs INTO program_record;
	END LOOP;

	CLOSE curs_programs;

	RETURN res;
END;
$$
;

COMMENT ON FUNCTION get_markdown_doku_by_schema(TEXT, TEXT) IS 'Function to generate a documentation from database comments as Markdown. The result is a text in Markdown format.';
