using System;
using System.Data;
using System.Data.Common;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Npgsql.PostgresTypes;
using NpgsqlTypes;

namespace Npgsql;

/// <summary>
/// Provides the underlying mechanism for reading schema information.
/// </summary>
static class NpgsqlSchema
{
    public static Task<DataTable> GetSchema(NpgsqlConnection conn, string? collectionName, string?[]? restrictions, bool async, CancellationToken cancellationToken = default)
    {
        if (collectionName is null)
            throw new ArgumentNullException(nameof(collectionName));
        if (collectionName.Length == 0)
            throw new ArgumentException("Collection name cannot be empty.", nameof(collectionName));

        return collectionName.ToUpperInvariant() switch
        {
            "METADATACOLLECTIONS"   => Task.FromResult(GetMetaDataCollections()),
            "RESTRICTIONS"          => Task.FromResult(GetRestrictions()),
            "DATASOURCEINFORMATION" => Task.FromResult(GetDataSourceInformation(conn)),
            "DATATYPES"             => Task.FromResult(GetDataTypes(conn)),
            "RESERVEDWORDS"         => Task.FromResult(GetReservedWords()),
            // custom collections for npgsql
            "DATABASES"             => GetDatabases(conn, restrictions, async, cancellationToken),
            "SCHEMATA"              => GetSchemata(conn, restrictions, async, cancellationToken),
            "TABLES"                => GetTables(conn, restrictions, async, cancellationToken),
            "COLUMNS"               => GetColumns(conn, restrictions, async, cancellationToken),
            "VIEWS"                 => GetViews(conn, restrictions, async, cancellationToken),
            "USERS"                 => GetUsers(conn, restrictions, async, cancellationToken),
            "INDEXES"               => GetIndexes(conn, restrictions, async, cancellationToken),
            "INDEXCOLUMNS"          => GetIndexColumns(conn, restrictions, async, cancellationToken),
            "CONSTRAINTS"           => GetConstraints(conn, restrictions, collectionName, async, cancellationToken),
            "PRIMARYKEY"            => GetConstraints(conn, restrictions, collectionName, async, cancellationToken),
            "UNIQUEKEYS"            => GetConstraints(conn, restrictions, collectionName, async, cancellationToken),
            "FOREIGNKEYS"           => GetConstraints(conn, restrictions, collectionName, async, cancellationToken),
            "CONSTRAINTCOLUMNS"     => GetConstraintColumns(conn, restrictions, async, cancellationToken),
            _ => throw new ArgumentOutOfRangeException(nameof(collectionName), collectionName, "Invalid collection name.")
        };
    }

    /// <summary>
    /// Returns the MetaDataCollections that lists all possible collections.
    /// </summary>
    /// <returns>The MetaDataCollections</returns>
    static DataTable GetMetaDataCollections()
    {
        var table = new DataTable("MetaDataCollections");
        table.Columns.Add("CollectionName", typeof(string));
        table.Columns.Add("NumberOfRestrictions", typeof(int));
        table.Columns.Add("NumberOfIdentifierParts", typeof(int));

        table.Rows.Add("MetaDataCollections", 0, 0);
        table.Rows.Add("DataSourceInformation", 0, 0);
        table.Rows.Add("Restrictions", 0, 0);
        table.Rows.Add("DataTypes", 0, 0);  // TODO: Support type name restriction
        table.Rows.Add("Databases", 1, 1);
        table.Rows.Add("Tables", 4, 3);
        table.Rows.Add("Columns", 4, 4);
        table.Rows.Add("Views", 3, 3);
        table.Rows.Add("Users", 1, 1);
        table.Rows.Add("Indexes", 4, 4);
        table.Rows.Add("IndexColumns", 5, 5);

        return table;
    }

    /// <summary>
    /// Returns the Restrictions that contains the meaning and position of the values in the restrictions array.
    /// </summary>
    /// <returns>The Restrictions</returns>
    static DataTable GetRestrictions()
    {
        var table = new DataTable("Restrictions");

        table.Columns.Add("CollectionName", typeof(string));
        table.Columns.Add("RestrictionName", typeof(string));
        table.Columns.Add("RestrictionDefault", typeof(string));
        table.Columns.Add("RestrictionNumber", typeof(int));

        table.Rows.Add("Database", "Name", "Name", 1);
        table.Rows.Add("Tables", "Catalog", "table_catalog", 1);
        table.Rows.Add("Tables", "Schema", "schema_catalog", 2);
        table.Rows.Add("Tables", "Table", "table_name", 3);
        table.Rows.Add("Tables", "TableType", "table_type", 4);
        table.Rows.Add("Columns", "Catalog", "table_catalog", 1);
        table.Rows.Add("Columns", "Schema", "table_schema", 2);
        table.Rows.Add("Columns", "TableName", "table_name", 3);
        table.Rows.Add("Columns", "Column", "column_name", 4);
        table.Rows.Add("Views", "Catalog", "table_catalog", 1);
        table.Rows.Add("Views", "Schema", "table_schema", 2);
        table.Rows.Add("Views", "Table", "table_name", 3);
        table.Rows.Add("Users", "User", "user_name", 1);
        table.Rows.Add("Indexes", "Catalog", "table_catalog", 1);
        table.Rows.Add("Indexes", "Schema", "table_schema", 2);
        table.Rows.Add("Indexes", "Table", "table_name", 3);
        table.Rows.Add("Indexes", "Index", "index_name", 4);
        table.Rows.Add("IndexColumns", "Catalog", "table_catalog", 1);
        table.Rows.Add("IndexColumns", "Schema", "table_schema", 2);
        table.Rows.Add("IndexColumns", "Table", "table_name", 3);
        table.Rows.Add("IndexColumns", "Index", "index_name", 4);
        table.Rows.Add("IndexColumns", "Column", "column_name", 5);

        return table;
    }

    static NpgsqlCommand BuildCommand(NpgsqlConnection conn, StringBuilder query, string?[]? restrictions, params string[]? names)
        => BuildCommand(conn, query, restrictions, true, names);

    static NpgsqlCommand BuildCommand(NpgsqlConnection conn, StringBuilder query, string?[]? restrictions, bool addWhere, params string[]? names)
    {
        var command = new NpgsqlCommand();

        if (restrictions != null && names != null)
        {
            for (var i = 0; i < restrictions.Length && i < names.Length; ++i)
            {
                if (restrictions[i] is string restriction && restriction.Length != 0)
                {
                    if (addWhere)
                    {
                        query.Append(" WHERE ");
                        addWhere = false;
                    }
                    else
                    {
                        query.Append(" AND ");
                    }

                    var paramName = RemoveSpecialChars(names[i]);

                    query.AppendFormat("{0} = :{1}", names[i], paramName);

                    command.Parameters.Add(new NpgsqlParameter(paramName, restriction));
                }
            }
        }
        command.CommandText = query.ToString();
        command.Connection = conn;

        return command;
    }

    static string RemoveSpecialChars(string paramName)
        => paramName.Replace("(", "").Replace(")", "").Replace(".", "");

    static async Task<DataTable> GetDatabases(NpgsqlConnection conn, string?[]? restrictions, bool async, CancellationToken cancellationToken = default)
    {
        var databases = new DataTable("Databases") { Locale = CultureInfo.InvariantCulture };

        databases.Columns.AddRange(new[] {
            new DataColumn("database_name"),
            new DataColumn("owner"),
            new DataColumn("encoding")
        });

        var getDatabases = new StringBuilder();

        getDatabases.Append("SELECT d.datname AS database_name, u.usename AS owner, pg_catalog.pg_encoding_to_char(d.encoding) AS encoding FROM pg_catalog.pg_database d LEFT JOIN pg_catalog.pg_user u ON d.datdba = u.usesysid");

        using var command = BuildCommand(conn, getDatabases, restrictions, "datname");
        using var adapter = new NpgsqlDataAdapter(command);
        await adapter.Fill(databases, async, cancellationToken);

        return databases;
    }

    static async Task<DataTable> GetSchemata(NpgsqlConnection conn, string?[]? restrictions, bool async, CancellationToken cancellationToken = default)
    {
        var schemata = new DataTable("Schemata") { Locale = CultureInfo.InvariantCulture };

        schemata.Columns.AddRange(new[] {
            new DataColumn("catalog_name"),
            new DataColumn("schema_name"),
            new DataColumn("schema_owner")
        });

        var getSchemata = new StringBuilder(@"
SELECT * FROM (
    SELECT current_database() AS catalog_name,
        nspname AS schema_name,
        r.rolname AS schema_owner
    FROM
        pg_catalog.pg_namespace LEFT JOIN pg_catalog.pg_roles r ON r.oid = nspowner
    ) tmp");

        using var command = BuildCommand(conn, getSchemata, restrictions, "catalog_name", "schema_name", "schema_owner");
        using var adapter = new NpgsqlDataAdapter(command);
        await adapter.Fill(schemata, async, cancellationToken);

        return schemata;
    }


    static async Task<DataTable> GetTables(NpgsqlConnection conn, string?[]? restrictions, bool async, CancellationToken cancellationToken = default)
    {
        var tables = new DataTable("Tables") { Locale = CultureInfo.InvariantCulture };

        tables.Columns.AddRange(new[] {
            new DataColumn("table_catalog"),
            new DataColumn("table_schema"),
            new DataColumn("table_name"),
            new DataColumn("table_type")
        });

        var getTables = new StringBuilder();

        getTables.Append(@"
SELECT table_catalog, table_schema, table_name, table_type
FROM information_schema.tables
WHERE
    table_type IN ('BASE TABLE', 'FOREIGN', 'FOREIGN TABLE') AND
    table_schema NOT IN ('pg_catalog', 'information_schema')");

        using var command = BuildCommand(conn, getTables, restrictions, false, "table_catalog", "table_schema", "table_name", "table_type");
        using var adapter = new NpgsqlDataAdapter(command);
        await adapter.Fill(tables, async, cancellationToken);

        return tables;
    }

    static async Task<DataTable> GetColumns(NpgsqlConnection conn, string?[]? restrictions, bool async, CancellationToken cancellationToken = default)
    {
        var columns = new DataTable("Columns") { Locale = CultureInfo.InvariantCulture };

        columns.Columns.AddRange(new DataColumn[] {
            new("table_catalog"), new("table_schema"), new("table_name"), new("column_name"),
            new("ordinal_position", typeof(int)),
            new("column_default"),
            new("is_nullable"),
            new("data_type"),
            new("character_maximum_length", typeof(int)), new("character_octet_length", typeof(int)),
            new("numeric_precision", typeof(int)), new("numeric_precision_radix", typeof(int)), new("numeric_scale", typeof(int)),
            new("datetime_precision", typeof(int)),
            new("character_set_catalog"), new("character_set_schema"), new("character_set_name"),
            new("collation_catalog")
        });

        var getColumns = new StringBuilder(@"
SELECT
    table_catalog, table_schema, table_name, column_name,
    ordinal_position,
    column_default,
    is_nullable,
    CASE WHEN udt_schema is NULL THEN udt_name ELSE format_type(typ.oid, NULL) END AS data_type,
    character_maximum_length, character_octet_length,
    numeric_precision, numeric_precision_radix, numeric_scale,
    datetime_precision,
    character_set_catalog, character_set_schema, character_set_name,
    collation_catalog
FROM information_schema.columns
JOIN pg_namespace AS ns ON ns.nspname = udt_schema
JOIN pg_type AS typ ON typnamespace = ns.oid AND typname = udt_name");

        using var command = BuildCommand(conn, getColumns, restrictions, "table_catalog", "table_schema", "table_name", "column_name");
        using var adapter = new NpgsqlDataAdapter(command);
        await adapter.Fill(columns, async, cancellationToken);

        return columns;
    }

    static async Task<DataTable> GetViews(NpgsqlConnection conn, string?[]? restrictions, bool async, CancellationToken cancellationToken = default)
    {
        var views = new DataTable("Views") { Locale = CultureInfo.InvariantCulture };

        views.Columns.AddRange(new[] {
            new DataColumn("table_catalog"), new DataColumn("table_schema"), new DataColumn("table_name"),
            new DataColumn("check_option"), new DataColumn("is_updatable")
        });

        var getViews = new StringBuilder(@"
SELECT table_catalog, table_schema, table_name, check_option, is_updatable
FROM information_schema.views
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')");

        using var command = BuildCommand(conn, getViews, restrictions, false, "table_catalog", "table_schema", "table_name");
        using var adapter = new NpgsqlDataAdapter(command);
        await adapter.Fill(views, async, cancellationToken);

        return views;
    }

    static async Task<DataTable> GetUsers(NpgsqlConnection conn, string?[]? restrictions, bool async, CancellationToken cancellationToken = default)
    {
        var users = new DataTable("Users") { Locale = CultureInfo.InvariantCulture };

        users.Columns.AddRange(new[] { new DataColumn("user_name"), new DataColumn("user_sysid", typeof(uint)) });

        var getUsers = new StringBuilder();

        getUsers.Append("SELECT usename as user_name, usesysid as user_sysid FROM pg_catalog.pg_user");

        using var command = BuildCommand(conn, getUsers, restrictions, "usename");
        using var adapter = new NpgsqlDataAdapter(command);
        await adapter.Fill(users, async, cancellationToken);

        return users;
    }

    static async Task<DataTable> GetIndexes(NpgsqlConnection conn, string?[]? restrictions, bool async, CancellationToken cancellationToken = default)
    {
        var indexes = new DataTable("Indexes") { Locale = CultureInfo.InvariantCulture };

        indexes.Columns.AddRange(new[] {
            new DataColumn("table_catalog"), new DataColumn("table_schema"), new DataColumn("table_name"),
            new DataColumn("index_name"), new DataColumn("type_desc")
        });

        var getIndexes = new StringBuilder(@"
SELECT current_database() AS table_catalog,
    n.nspname AS table_schema,
    t.relname AS table_name,
    i.relname AS index_name,
    '' AS type_desc
FROM
    pg_catalog.pg_class i
    JOIN pg_catalog.pg_index ix ON ix.indexrelid = i.oid
    JOIN pg_catalog.pg_class t ON ix.indrelid = t.oid
    LEFT JOIN pg_catalog.pg_user u ON u.usesysid = i.relowner
    LEFT JOIN pg_catalog.pg_namespace n ON n.oid = i.relnamespace
WHERE
    i.relkind = 'i' AND
    n.nspname NOT IN ('pg_catalog', 'pg_toast') AND
    t.relkind = 'r'");

        using var command = BuildCommand(conn, getIndexes, restrictions, false, "current_database()", "n.nspname", "t.relname", "i.relname");
        using var adapter = new NpgsqlDataAdapter(command);
        await adapter.Fill(indexes, async, cancellationToken);

        return indexes;
    }

    static async Task<DataTable> GetIndexColumns(NpgsqlConnection conn, string?[]? restrictions, bool async, CancellationToken cancellationToken = default)
    {
        var indexColumns = new DataTable("IndexColumns") { Locale = CultureInfo.InvariantCulture };

        indexColumns.Columns.AddRange(new[] {
            new DataColumn("constraint_catalog"), new DataColumn("constraint_schema"), new DataColumn("constraint_name"),
            new DataColumn("table_catalog"), new DataColumn("table_schema"), new DataColumn("table_name"),
            new DataColumn("column_name"), new DataColumn("index_name")
        });

        var getIndexColumns = new StringBuilder(@"
SELECT
    current_database() AS constraint_catalog,
    t_ns.nspname AS constraint_schema,
    ix_cls.relname AS constraint_name,
    current_database() AS table_catalog,
    ix_ns.nspname AS table_schema,
    t.relname AS table_name,
    a.attname AS column_name,
    ix_cls.relname AS index_name
FROM
    pg_class t
    JOIN pg_index ix ON t.oid = ix.indrelid
    JOIN pg_class ix_cls ON ix.indexrelid = ix_cls.oid
    JOIN pg_attribute a ON t.oid = a.attrelid
    LEFT JOIN pg_namespace t_ns ON t.relnamespace = t_ns.oid
    LEFT JOIN pg_namespace ix_ns ON ix_cls.relnamespace = ix_ns.oid
WHERE
    ix_cls.relkind = 'i' AND
    t_ns.nspname NOT IN ('pg_catalog', 'pg_toast') AND
    a.attnum = ANY(ix.indkey) AND
    t.relkind = 'r'");

        using var command = BuildCommand(conn, getIndexColumns, restrictions, false, "current_database()", "t_ns.nspname", "t.relname", "ix_cls.relname", "a.attname");
        using var adapter = new NpgsqlDataAdapter(command);
        await adapter.Fill(indexColumns, async, cancellationToken);

        return indexColumns;
    }

    static async Task<DataTable> GetConstraints(NpgsqlConnection conn, string?[]? restrictions, string? constraintType, bool async, CancellationToken cancellationToken = default)
    {
        var getConstraints = new StringBuilder(@"
SELECT
    current_database() AS ""CONSTRAINT_CATALOG"",
    pgn.nspname AS ""CONSTRAINT_SCHEMA"",
    pgc.conname AS ""CONSTRAINT_NAME"",
    current_database() AS ""TABLE_CATALOG"",
    pgtn.nspname AS ""TABLE_SCHEMA"",
    pgt.relname AS ""TABLE_NAME"",
    ""CONSTRAINT_TYPE"",
    pgc.condeferrable AS ""IS_DEFERRABLE"",
    pgc.condeferred AS ""INITIALLY_DEFERRED""
FROM
    pg_catalog.pg_constraint pgc
    JOIN pg_catalog.pg_namespace pgn ON pgc.connamespace = pgn.oid
    JOIN pg_catalog.pg_class pgt ON pgc.conrelid = pgt.oid
    JOIN pg_catalog.pg_namespace pgtn ON pgt.relnamespace = pgtn.oid
    JOIN (
        SELECT 'PRIMARY KEY' AS ""CONSTRAINT_TYPE"", 'p' AS ""contype""
        UNION ALL
        SELECT 'FOREIGN KEY' AS ""CONSTRAINT_TYPE"", 'f' AS ""contype""
        UNION ALL
        SELECT 'UNIQUE KEY' AS ""CONSTRAINT_TYPE"", 'u' AS ""contype""
) mapping_table ON mapping_table.contype = pgc.contype");
        if ("ForeignKeys".Equals(constraintType))
            getConstraints.Append(" and pgc.contype='f'");
        else if ("PrimaryKey".Equals(constraintType))
            getConstraints.Append(" and pgc.contype='p'");
        else if ("UniqueKeys".Equals(constraintType))
            getConstraints.Append(" and pgc.contype='u'");
        else
            constraintType = "Constraints";

        using var command = BuildCommand(conn, getConstraints, restrictions, false, "current_database()", "pgtn.nspname", "pgt.relname", "pgc.conname");
        using var adapter = new NpgsqlDataAdapter(command);
        var table = new DataTable(constraintType) { Locale = CultureInfo.InvariantCulture };

        await adapter.Fill(table, async, cancellationToken);

        return table;
    }

    static async Task<DataTable> GetConstraintColumns(NpgsqlConnection conn, string?[]? restrictions, bool async, CancellationToken cancellationToken = default)
    {
        var getConstraintColumns = new StringBuilder(@"
SELECT current_database() AS constraint_catalog,
    n.nspname AS constraint_schema,
    c.conname AS constraint_name,
    current_database() AS table_catalog,
    n.nspname AS table_schema,
    t.relname AS table_name,
    a.attname AS column_name,
    a.attnum AS ordinal_number,
    mapping_table.constraint_type
FROM pg_constraint c
    JOIN pg_namespace n on n.oid = c.connamespace
    JOIN pg_class t on t.oid = c.conrelid AND t.relkind = 'r'
    JOIN pg_attribute a on t.oid = a.attrelid AND a.attnum = ANY(c.conkey)
    JOIN (
        SELECT 'PRIMARY KEY' AS constraint_type, 'p' AS contype
        UNION ALL
        SELECT 'FOREIGN KEY' AS constraint_type, 'f' AS contype
        UNION ALL
        SELECT 'UNIQUE KEY' AS constraint_type, 'u' AS contype
) mapping_table ON
    mapping_table.contype = c.contype
    AND n.nspname NOT IN ('pg_catalog', 'pg_toast')");

        using var command = BuildCommand(conn, getConstraintColumns, restrictions, false, "current_database()", "n.nspname", "t.relname", "c.conname", "a.attname");
        using var adapter = new NpgsqlDataAdapter(command);
        var table = new DataTable("ConstraintColumns") { Locale = CultureInfo.InvariantCulture };

        await adapter.Fill(table, async, cancellationToken);

        return table;
    }

    static DataTable GetDataSourceInformation(NpgsqlConnection conn)
    {
        var table = new DataTable("DataSourceInformation");
        var row = table.Rows.Add();

        table.Columns.Add("CompositeIdentifierSeparatorPattern", typeof(string));
        // TODO: DefaultCatalog? Was in XML (unfilled) but isn't in docs
        table.Columns.Add("DataSourceProductName", typeof(string));
        table.Columns.Add("DataSourceProductVersion", typeof(string));
        table.Columns.Add("DataSourceProductVersionNormalized", typeof(string));
        table.Columns.Add("GroupByBehavior", typeof(GroupByBehavior));
        table.Columns.Add("IdentifierPattern", typeof(string));
        table.Columns.Add("IdentifierCase", typeof(IdentifierCase));
        table.Columns.Add("OrderByColumnsInSelect", typeof(bool));
        table.Columns.Add("ParameterMarkerFormat", typeof(string));
        table.Columns.Add("ParameterMarkerPattern", typeof(string));
        table.Columns.Add("ParameterNameMaxLength", typeof(int));
        table.Columns.Add("QuotedIdentifierPattern", typeof(string));
        table.Columns.Add("QuotedIdentifierCase", typeof(IdentifierCase));
        table.Columns.Add("ParameterNamePattern", typeof(string));
        table.Columns.Add("StatementSeparatorPattern", typeof(string));
        table.Columns.Add("StringLiteralPattern", typeof(string));
        table.Columns.Add("SupportedJoinOperators", typeof(SupportedJoinOperators));

        var version = conn.PostgreSqlVersion;
        var normalizedVersion = $"{version.Major:00}.{version.Minor:00}";
        if (version.Build >= 0)
            normalizedVersion += $".{version.Build:00}";

        row["CompositeIdentifierSeparatorPattern"] = @"\.";
        row["DataSourceProductName"] = "Npgsql";
        row["DataSourceProductVersion"] = version.ToString();
        row["DataSourceProductVersionNormalized"] = normalizedVersion;
        row["GroupByBehavior"] = GroupByBehavior.Unrelated;
        row["IdentifierPattern"] = @"(^\[\p{Lo}\p{Lu}\p{Ll}_@#][\p{Lo}\p{Lu}\p{Ll}\p{Nd}@$#_]*$)|(^\[[^\]\0]|\]\]+\]$)|(^\""[^\""\0]|\""\""+\""$)";
        row["IdentifierCase"] = IdentifierCase.Insensitive;
        row["OrderByColumnsInSelect"] = false;
        row["QuotedIdentifierPattern"] = @"""(([^\""]|\""\"")*)""";
        row["QuotedIdentifierCase"] = IdentifierCase.Sensitive;
        row["StatementSeparatorPattern"] = ";";
        row["StringLiteralPattern"] = @"'(([^']|'')*)'";
        row["SupportedJoinOperators"] =
            SupportedJoinOperators.FullOuter |
            SupportedJoinOperators.Inner |
            SupportedJoinOperators.LeftOuter |
            SupportedJoinOperators.RightOuter;

        row["ParameterNameMaxLength"] = 63; // For function out parameters
        row["ParameterMarkerFormat"] = @"{0}";  // TODO: Not sure

        if (NpgsqlCommand.EnableSqlRewriting)
        {
            row["ParameterMarkerPattern"] = @"@[\p{Lo}\p{Lu}\p{Ll}\p{Lm}_@#][\p{Lo}\p{Lu}\p{Ll}\p{Lm}\p{Nd}\uff3f_@#\$]*(?=\s+|$)";
            row["ParameterNamePattern"] = @"^[\p{Lo}\p{Lu}\p{Ll}\p{Lm}_@#][\p{Lo}\p{Lu}\p{Ll}\p{Lm}\p{Nd}\uff3f_@#\$]*(?=\s+|$)";
        }
        else
        {
            row["ParameterMarkerPattern"] = @"$\d+";
            row["ParameterNamePattern"] = @"\d+";
        }

        return table;
    }

    #region DataTypes

    static DataTable GetDataTypes(NpgsqlConnection conn)
    {
        using var _ = conn.StartTemporaryBindingScope(out var connector);

        var table = new DataTable("DataTypes");

        table.Columns.Add("TypeName", typeof(string));
        table.Columns.Add("ColumnSize", typeof(long));
        table.Columns.Add("CreateFormat", typeof(string));
        table.Columns.Add("CreateParameters", typeof(string));
        table.Columns.Add("DataType", typeof(string));
        table.Columns.Add("IsAutoIncrementable", typeof(bool));
        table.Columns.Add("IsBestMatch", typeof(bool));
        table.Columns.Add("IsCaseSensitive", typeof(bool));
        table.Columns.Add("IsConcurrencyType", typeof(bool));
        table.Columns.Add("IsFixedLength", typeof(bool));
        table.Columns.Add("IsFixedPrecisionAndScale", typeof(bool));
        table.Columns.Add("IsLiteralSupported", typeof(bool));
        table.Columns.Add("IsLong", typeof(bool));
        table.Columns.Add("IsNullable", typeof(bool));
        table.Columns.Add("IsSearchable", typeof(bool));
        table.Columns.Add("IsSearchableWithLike", typeof(bool));
        table.Columns.Add("IsUnsigned", typeof(bool));
        table.Columns.Add("LiteralPrefix", typeof(string));
        table.Columns.Add("LiteralSuffix", typeof(string));
        table.Columns.Add("MaximumScale", typeof(short));
        table.Columns.Add("MinimumScale", typeof(short));
        table.Columns.Add("NativeDataType", typeof(string));
        table.Columns.Add("ProviderDbType", typeof(int));

        // Npgsql-specific
        table.Columns.Add("OID", typeof(uint));

        // TODO: Support type name restriction

        foreach (var baseType in connector.DatabaseInfo.BaseTypes.Cast<PostgresType>()
                     .Concat(connector.DatabaseInfo.EnumTypes)
                     .Concat(connector.DatabaseInfo.CompositeTypes))
        {
            if (!connector.TypeMapper.TryGetMapping(baseType, out var mapping))
                continue;

            var row = table.Rows.Add();

            PopulateDefaultDataTypeInfo(row, baseType);
            PopulateHardcodedDataTypeInfo(row, baseType);

            if (mapping.ClrTypes.Length > 0)
                row["DataType"] = mapping.ClrTypes[0].FullName;
            if (mapping.NpgsqlDbType.HasValue)
                row["ProviderDbType"] = (int)mapping.NpgsqlDbType.Value;
        }

        foreach (var arrayType in connector.DatabaseInfo.ArrayTypes)
        {
            if (!connector.TypeMapper.TryGetMapping(arrayType.Element, out var elementMapping))
                continue;

            var row = table.Rows.Add();

            PopulateDefaultDataTypeInfo(row, arrayType.Element);
            // Populate hardcoded values based on the element type (e.g. citext[] is case-insensitive).
            PopulateHardcodedDataTypeInfo(row, arrayType.Element);

            row["TypeName"] = arrayType.DisplayName;
            row["OID"] = arrayType.OID;
            row["CreateFormat"] += "[]";
            if (elementMapping.ClrTypes.Length > 0)
                row["DataType"] = elementMapping.ClrTypes[0].MakeArrayType().FullName;
            if (elementMapping.NpgsqlDbType.HasValue)
                row["ProviderDbType"] = (int)(elementMapping.NpgsqlDbType.Value | NpgsqlDbType.Array);
        }

        foreach (var rangeType in connector.DatabaseInfo.RangeTypes)
        {
            if (!connector.TypeMapper.TryGetMapping(rangeType.Subtype, out var subtypeMapping))
                continue;

            var row = table.Rows.Add();

            PopulateDefaultDataTypeInfo(row, rangeType.Subtype);
            // Populate hardcoded values based on the subtype type (e.g. citext[] is case-insensitive).
            PopulateHardcodedDataTypeInfo(row, rangeType.Subtype);

            row["TypeName"] = rangeType.DisplayName;
            row["OID"] = rangeType.OID;
            row["CreateFormat"] = rangeType.DisplayName.ToUpperInvariant();
            if (subtypeMapping.ClrTypes.Length > 0)
                row["DataType"] = typeof(NpgsqlRange<>).MakeGenericType(subtypeMapping.ClrTypes[0]).FullName;
            if (subtypeMapping.NpgsqlDbType.HasValue)
                row["ProviderDbType"] = (int)(subtypeMapping.NpgsqlDbType.Value | NpgsqlDbType.Range);
        }

        foreach (var multirangeType in connector.DatabaseInfo.MultirangeTypes)
        {
            var subtypeType = multirangeType.Subrange.Subtype;
            if (!connector.TypeMapper.TryGetMapping(subtypeType, out var subtypeMapping))
                continue;

            var row = table.Rows.Add();

            PopulateDefaultDataTypeInfo(row, subtypeType);
            // Populate hardcoded values based on the subtype type (e.g. citext[] is case-insensitive).
            PopulateHardcodedDataTypeInfo(row, subtypeType);

            row["TypeName"] = multirangeType.DisplayName;
            row["OID"] = multirangeType.OID;
            row["CreateFormat"] = multirangeType.DisplayName.ToUpperInvariant();
            if (subtypeMapping.ClrTypes.Length > 0)
                row["DataType"] = typeof(NpgsqlRange<>).MakeGenericType(subtypeMapping.ClrTypes[0]).FullName;
            if (subtypeMapping.NpgsqlDbType.HasValue)
                row["ProviderDbType"] = (int)(subtypeMapping.NpgsqlDbType.Value | NpgsqlDbType.Range);
        }

        foreach (var domainType in connector.DatabaseInfo.DomainTypes)
        {
            if (!connector.TypeMapper.TryGetMapping(domainType, out var baseMapping))
                continue;

            var row = table.Rows.Add();

            PopulateDefaultDataTypeInfo(row, domainType.BaseType);
            // Populate hardcoded values based on the element type (e.g. citext[] is case-insensitive).
            PopulateHardcodedDataTypeInfo(row, domainType.BaseType);
            row["TypeName"] = domainType.DisplayName;
            row["OID"] = domainType.OID;
            // A domain is never the best match, since its underlying base type is
            row["IsBestMatch"] = false;

            if (baseMapping.ClrTypes.Length > 0)
                row["DataType"] = baseMapping.ClrTypes[0].FullName;
            if (baseMapping.NpgsqlDbType.HasValue)
                row["ProviderDbType"] = (int)baseMapping.NpgsqlDbType.Value;
        }

        return table;
    }

    /// <summary>
    /// Populates some generic type information that is common for base types, arrays, enums, etc. Some will
    /// be overridden later.
    /// </summary>
    static void PopulateDefaultDataTypeInfo(DataRow row, PostgresType type)
    {
        row["TypeName"] = type.DisplayName;
        // Skipping ColumnSize at least for now, not very meaningful
        row["CreateFormat"] = type.DisplayName.ToUpperInvariant();
        row["CreateParameters"] = "";
        row["IsAutoIncrementable"] = false;
        // We populate the DataType above from mapping.ClrTypes, which means we take the .NET type from
        // which we *infer* the PostgreSQL type. Since only a single PostgreSQL type gets inferred from a given
        // .NET type, we never have the same DataType in more than one row - so the mapping is always the
        // best match. See the hardcoding override  below for some exceptions.
        row["IsBestMatch"] = true;
        row["IsCaseSensitive"] = true;
        row["IsConcurrencyType"] = false;
        row["IsFixedLength"] = false;
        row["IsFixedPrecisionAndScale"] = false;
        row["IsLiteralSupported"] = false;  // See hardcoding override below
        row["IsLong"] = false;
        row["IsNullable"] = true;
        row["IsSearchable"] = true;
        row["IsSearchableWithLike"] = false;
        row["IsUnsigned"] = DBNull.Value; // See hardcoding override below
        // LiteralPrefix/Suffix: no literal for now except for strings, see hardcoding override below
        row["MaximumScale"] = DBNull.Value;
        row["MinimumScale"] = DBNull.Value;
        // NativeDataType is unset
        row["OID"] = type.OID;
    }

    /// <summary>
    /// Sets some custom, hardcoded info on a DataType row that cannot be loaded/inferred from PostgreSQL
    /// </summary>
    static void PopulateHardcodedDataTypeInfo(DataRow row, PostgresType type)
    {
        switch (type.Name)
        {
        case "varchar":
        case "char":
            row["DataType"] = "String";
            row["IsBestMatch"] = false;
            goto case "text";
        case "text":
            row["CreateFormat"] += "({0})";
            row["CreateParameters"] = "size";
            row["IsSearchableWithLike"] = true;
            row["IsLiteralSupported"] = true;
            row["LiteralPrefix"] = "'";
            row["LiteralSuffix"] = "'";
            return;
        case "numeric":
            row["CreateFormat"] += "({0},{1})";
            row["CreateParameters"] = "precision, scale";
            row["MaximumScale"] = 16383;
            row["MinimumScale"] = 16383;
            row["IsUnsigned"] = false;
            return;
        case "bytea":
            row["IsLong"] = true;
            return;
        case "citext":
            row["IsCaseSensitive"] = false;
            return;
        case "integer":
        case "smallint":
        case "bigint":
        case "double precision":
        case "real":
        case "money":
            row["IsUnsigned"] = false;
            return;
        case "oid":
        case "cid":
        case "regtype":
        case "regconfig":
            row["IsUnsigned"] = true;
            return;
        case "xid":
            row["IsUnsigned"] = true;
            row["IsConcurrencyType"] = true;
            return;
        }
    }

    #endregion DataTypes

    #region Reserved Keywords

    static DataTable GetReservedWords()
    {
        var table = new DataTable("ReservedWords") { Locale = CultureInfo.InvariantCulture };
        table.Columns.Add("ReservedWord", typeof(string));
        foreach (var keyword in ReservedKeywords)
            table.Rows.Add(keyword);
        return table;
    }

    /// <summary>
    /// List of keywords taken from PostgreSQL 9.0 reserved words documentation.
    /// </summary>
    static readonly string[] ReservedKeywords =
    {
        "ALL",
        "ANALYSE",
        "ANALYZE",
        "AND",
        "ANY",
        "ARRAY",
        "AS",
        "ASC",
        "ASYMMETRIC",
        "AUTHORIZATION",
        "BINARY",
        "BOTH",
        "CASE",
        "CAST",
        "CHECK",
        "COLLATE",
        "COLUMN",
        "CONCURRENTLY",
        "CONSTRAINT",
        "CREATE",
        "CROSS",
        "CURRENT_CATALOG",
        "CURRENT_DATE",
        "CURRENT_ROLE",
        "CURRENT_SCHEMA",
        "CURRENT_TIME",
        "CURRENT_TIMESTAMP",
        "CURRENT_USER",
        "DEFAULT",
        "DEFERRABLE",
        "DESC",
        "DISTINCT",
        "DO",
        "ELSE",
        "END",
        "EXCEPT",
        "FALSE",
        "FETCH",
        "FOR",
        "FOREIGN",
        "FREEZE",
        "FROM",
        "FULL",
        "GRANT",
        "GROUP",
        "HAVING",
        "ILIKE",
        "IN",
        "INITIALLY",
        "INNER",
        "INTERSECT",
        "INTO",
        "IS",
        "ISNULL",
        "JOIN",
        "LATERAL",
        "LEADING",
        "LEFT",
        "LIKE",
        "LIMIT",
        "LOCALTIME",
        "LOCALTIMESTAMP",
        "NATURAL",
        "NOT",
        "NOTNULL",
        "NULL",
        "OFFSET",
        "ON",
        "ONLY",
        "OR",
        "ORDER",
        "OUTER",
        "OVER",
        "OVERLAPS",
        "PLACING",
        "PRIMARY",
        "REFERENCES",
        "RETURNING",
        "RIGHT",
        "SELECT",
        "SESSION_USER",
        "SIMILAR",
        "SOME",
        "SYMMETRIC",
        "TABLE",
        "THEN",
        "TO",
        "TRAILING",
        "TRUE",
        "UNION",
        "UNIQUE",
        "USER",
        "USING",
        "VARIADIC",
        "VERBOSE",
        "WHEN",
        "WHERE",
        "WINDOW",
        "WITH"
    };

    #endregion Reserved Keywords
}
