using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Threading;
using Npgsql.Internal.TypeHandling;
using Npgsql.Internal.TypeMapping;
using Npgsql.NameTranslation;
using NpgsqlTypes;
using static Npgsql.Util.Statics;

namespace Npgsql.TypeMapping;

sealed class GlobalTypeMapper : TypeMapperBase
{
    public static GlobalTypeMapper Instance { get; }

    internal List<TypeHandlerResolverFactory> ResolverFactories { get; } = new();
    public ConcurrentDictionary<string, IUserTypeMapping> UserTypeMappings { get; } = new();

    readonly ConcurrentDictionary<Type, TypeMappingInfo> _mappingsByClrType = new();

    /// <summary>
    /// A counter that is incremented whenever a global mapping change occurs.
    /// Used to invalidate bound type mappers.
    /// </summary>
    internal int ChangeCounter => _changeCounter;

    internal ReaderWriterLockSlim Lock { get; }
        = new(LockRecursionPolicy.SupportsRecursion);

    int _changeCounter;

    static GlobalTypeMapper()
        => Instance = new GlobalTypeMapper();

    GlobalTypeMapper() : base(new NpgsqlSnakeCaseNameTranslator())
        => Reset();

    #region Mapping management

    public override INpgsqlTypeMapper MapEnum<TEnum>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
    {
        if (pgName != null && pgName.Trim() == "")
            throw new ArgumentException("pgName can't be empty", nameof(pgName));

        nameTranslator ??= DefaultNameTranslator;
        pgName ??= GetPgName(typeof(TEnum), nameTranslator);

        Lock.EnterWriteLock();
        try
        {
            UserTypeMappings[pgName] = new UserEnumTypeMapping<TEnum>(pgName, nameTranslator);
            RecordChange();
            return this;
        }
        finally
        {
            Lock.ExitWriteLock();
        }
    }

    public override bool UnmapEnum<TEnum>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
    {
        if (pgName != null && pgName.Trim() == "")
            throw new ArgumentException("pgName can't be empty", nameof(pgName));

        nameTranslator ??= DefaultNameTranslator;
        pgName ??= GetPgName(typeof(TEnum), nameTranslator);

        Lock.EnterWriteLock();
        try
        {
            if (UserTypeMappings.TryRemove(pgName, out _))
            {
                RecordChange();
                return true;
            }

            return false;
        }
        finally
        {
            Lock.ExitWriteLock();
        }
    }

    public override INpgsqlTypeMapper MapComposite<T>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
    {
        if (pgName != null && pgName.Trim() == "")
            throw new ArgumentException("pgName can't be empty", nameof(pgName));

        nameTranslator ??= DefaultNameTranslator;
        pgName ??= GetPgName(typeof(T), nameTranslator);

        Lock.EnterWriteLock();
        try
        {
            UserTypeMappings[pgName] = new UserCompositeTypeMapping<T>(pgName, nameTranslator);
            RecordChange();
            return this;
        }
        finally
        {
            Lock.ExitWriteLock();
        }
    }

    public override INpgsqlTypeMapper MapComposite(Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
    {
        var openMethod = typeof(GlobalTypeMapper).GetMethod(nameof(MapComposite), new[] { typeof(string), typeof(INpgsqlNameTranslator) })!;
        var method = openMethod.MakeGenericMethod(clrType);
        method.Invoke(this, new object?[] { pgName, nameTranslator });

        return this;
    }

    public override bool UnmapComposite<T>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
        => UnmapComposite(typeof(T), pgName, nameTranslator);

    public override bool UnmapComposite(Type clrType, string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
    {
        if (pgName != null && pgName.Trim() == "")
            throw new ArgumentException("pgName can't be empty", nameof(pgName));

        nameTranslator ??= DefaultNameTranslator;
        pgName ??= GetPgName(clrType, nameTranslator);

        Lock.EnterWriteLock();
        try
        {
            if (UserTypeMappings.TryRemove(pgName, out _))
            {
                RecordChange();
                return true;
            }

            return false;
        }
        finally
        {
            Lock.ExitWriteLock();
        }
    }

    public override void AddTypeResolverFactory(TypeHandlerResolverFactory resolverFactory)
    {
        Lock.EnterWriteLock();
        try
        {
            // Since EFCore.PG plugins (and possibly other users) repeatedly call NpgsqlConnection.GlobalTypeMapped.UseNodaTime,
            // we replace an existing resolver of the same CLR type.
            var type = resolverFactory.GetType();

            if (ResolverFactories[0].GetType() == type)
                ResolverFactories[0] = resolverFactory;
            else
            {
                for (var i = 0; i < ResolverFactories.Count; i++)
                    if (ResolverFactories[i].GetType() == type)
                        ResolverFactories.RemoveAt(i);

                ResolverFactories.Insert(0, resolverFactory);
            }

            RecordChange();
        }
        finally
        {
            Lock.ExitWriteLock();
        }
    }

    public override void Reset()
    {
        Lock.EnterWriteLock();
        try
        {
            ResolverFactories.Clear();
            ResolverFactories.Add(new BuiltInTypeHandlerResolverFactory());

            UserTypeMappings.Clear();

            RecordChange();
        }
        finally
        {
            Lock.ExitWriteLock();
        }
    }

    internal void RecordChange()
    {
        _mappingsByClrType.Clear();
        Interlocked.Increment(ref _changeCounter);
    }

    #endregion Mapping management

    #region NpgsqlDbType/DbType inference for NpgsqlParameter

    [RequiresUnreferencedCode("ToNpgsqlDbType uses interface-based reflection and isn't trimming-safe")]
    internal bool TryResolveMappingByValue(object value, [NotNullWhen(true)] out TypeMappingInfo? typeMapping)
    {
        Lock.EnterReadLock();
        try
        {
            // We resolve as follows:
            // 1. Cached by-type lookup (fast path). This will work for almost all types after the very first resolution.
            // 2. Value-dependent type lookup (e.g. DateTime by Kind) via the resolvers. This includes complex types (e.g. array/range
            //    over DateTime), and the results cannot be cached.
            // 3. Uncached by-type lookup (for the very first resolution of a given type)

            var type = value.GetType();
            if (_mappingsByClrType.TryGetValue(type, out typeMapping))
                return true;

            foreach (var resolverFactory in ResolverFactories)
                if ((typeMapping = resolverFactory.GetMappingByValueDependentValue(value)) is not null)
                    return true;

            return TryResolveMappingByClrType(value.GetType(), out typeMapping);
        }
        finally
        {
            Lock.ExitReadLock();
        }

        bool TryResolveMappingByClrType(Type clrType, [NotNullWhen(true)] out TypeMappingInfo? typeMapping)
        {
            if (_mappingsByClrType.TryGetValue(clrType, out typeMapping))
                return true;

            foreach (var resolverFactory in ResolverFactories)
            {
                if ((typeMapping = resolverFactory.GetMappingByClrType(clrType)) is not null)
                {
                    _mappingsByClrType[clrType] = typeMapping;
                    return true;
                }
            }

            if (clrType.IsArray)
            {
                if (TryResolveMappingByClrType(clrType.GetElementType()!, out var elementMapping))
                {
                    _mappingsByClrType[clrType] = typeMapping = new(
                        NpgsqlDbType.Array | elementMapping.NpgsqlDbType,
                        elementMapping.DataTypeName + "[]");
                    return true;
                }

                typeMapping = null;
                return false;
            }

            var typeInfo = clrType.GetTypeInfo();

            var ilist = typeInfo.ImplementedInterfaces.FirstOrDefault(x =>
                x.GetTypeInfo().IsGenericType && x.GetGenericTypeDefinition() == typeof(IList<>));
            if (ilist != null)
            {
                if (TryResolveMappingByClrType(ilist.GetGenericArguments()[0], out var elementMapping))
                {
                    _mappingsByClrType[clrType] = typeMapping = new(
                        NpgsqlDbType.Array | elementMapping.NpgsqlDbType,
                        elementMapping.DataTypeName + "[]");
                    return true;
                }

                typeMapping = null;
                return false;
            }

            if (typeInfo.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(NpgsqlRange<>))
            {
                if (TryResolveMappingByClrType(clrType.GetGenericArguments()[0], out var elementMapping))
                {
                    _mappingsByClrType[clrType] = typeMapping = new(
                        NpgsqlDbType.Range | elementMapping.NpgsqlDbType,
                        dataTypeName: null);
                    return true;
                }

                typeMapping = null;
                return false;
            }

            typeMapping = null;
            return false;
        }
    }

    #endregion NpgsqlDbType/DbType inference for NpgsqlParameter

    #region Static translation tables

    public static string? NpgsqlDbTypeToDataTypeName(NpgsqlDbType npgsqlDbType)
        => npgsqlDbType switch
        {
            // Numeric types
            NpgsqlDbType.Smallint => "smallint",
            NpgsqlDbType.Integer  => "integer",
            NpgsqlDbType.Bigint   => "bigint",
            NpgsqlDbType.Real     => "real",
            NpgsqlDbType.Double   => "double precision",
            NpgsqlDbType.Numeric  => "numeric",
            NpgsqlDbType.Money    => "money",

            // Text types
            NpgsqlDbType.Text      => "text",
            NpgsqlDbType.Xml       => "xml",
            NpgsqlDbType.Varchar   => "character varying",
            NpgsqlDbType.Char      => "character",
            NpgsqlDbType.Name      => "name",
            NpgsqlDbType.Refcursor => "refcursor",
            NpgsqlDbType.Citext    => "citext",
            NpgsqlDbType.Jsonb     => "jsonb",
            NpgsqlDbType.Json      => "json",
            NpgsqlDbType.JsonPath  => "jsonpath",

            // Date/time types
            NpgsqlDbType.Timestamp   => "timestamp without time zone",
            NpgsqlDbType.TimestampTz => "timestamp with time zone",
            NpgsqlDbType.Date        => "date",
            NpgsqlDbType.Time        => "time without time zone",
            NpgsqlDbType.TimeTz      => "time with time zone",
            NpgsqlDbType.Interval    => "interval",

            // Network types
            NpgsqlDbType.Cidr     => "cidr",
            NpgsqlDbType.Inet     => "inet",
            NpgsqlDbType.MacAddr  => "macaddr",
            NpgsqlDbType.MacAddr8 => "macaddr8",

            // Full-text search types
            NpgsqlDbType.TsQuery   => "tsquery",
            NpgsqlDbType.TsVector  => "tsvector",

            // Geometry types
            NpgsqlDbType.Box     => "box",
            NpgsqlDbType.Circle  => "circle",
            NpgsqlDbType.Line    => "line",
            NpgsqlDbType.LSeg    => "lseg",
            NpgsqlDbType.Path    => "path",
            NpgsqlDbType.Point   => "point",
            NpgsqlDbType.Polygon => "polygon",

            // LTree types
            NpgsqlDbType.LQuery    => "lquery",
            NpgsqlDbType.LTree     => "ltree",
            NpgsqlDbType.LTxtQuery => "ltxtquery",

            // UInt types
            NpgsqlDbType.Oid       => "oid",
            NpgsqlDbType.Xid       => "xid",
            NpgsqlDbType.Xid8      => "xid8",
            NpgsqlDbType.Cid       => "cid",
            NpgsqlDbType.Regtype   => "regtype",
            NpgsqlDbType.Regconfig => "regconfig",

            // Misc types
            NpgsqlDbType.Boolean => "boolean",
            NpgsqlDbType.Bytea   => "bytea",
            NpgsqlDbType.Uuid    => "uuid",
            NpgsqlDbType.Varbit  => "bit varying",
            NpgsqlDbType.Bit     => "bit",
            NpgsqlDbType.Hstore  => "hstore",

            NpgsqlDbType.Geometry  => "geometry",
            NpgsqlDbType.Geography => "geography",

            // Built-in range types
            NpgsqlDbType.IntegerRange     => "int4range",
            NpgsqlDbType.BigIntRange      => "int8range",
            NpgsqlDbType.NumericRange     => "numrange",
            NpgsqlDbType.TimestampRange   => "tsrange",
            NpgsqlDbType.TimestampTzRange => "tstzrange",
            NpgsqlDbType.DateRange        => "daterange",

            // Built-in multirange types
            NpgsqlDbType.IntegerMultirange     => "int4multirange",
            NpgsqlDbType.BigIntMultirange      => "int8multirange",
            NpgsqlDbType.NumericMultirange     => "nummultirange",
            NpgsqlDbType.TimestampMultirange   => "tsmultirange",
            NpgsqlDbType.TimestampTzMultirange => "tstzmultirange",
            NpgsqlDbType.DateMultirange        => "datemultirange",

            // Internal types
            NpgsqlDbType.Int2Vector   => "int2vector",
            NpgsqlDbType.Oidvector    => "oidvector",
            NpgsqlDbType.PgLsn        => "pg_lsn",
            NpgsqlDbType.Tid          => "tid",
            NpgsqlDbType.InternalChar => "char",

            // Special types
            NpgsqlDbType.Unknown => "unknown",

            _ => npgsqlDbType.HasFlag(NpgsqlDbType.Array)
                ? NpgsqlDbTypeToDataTypeName(npgsqlDbType & ~NpgsqlDbType.Array) + "[]"
                : null // e.g. ranges
        };

            public static NpgsqlDbType DataTypeNameToNpgsqlDbType(string typeName)
        {
            // Strip any facet information (length/precision/scale)
            var parenIndex = typeName.IndexOf('(');
            if (parenIndex > -1)
                typeName = typeName.Substring(0, parenIndex);

            return typeName switch
            {
                // Numeric types
                "smallint" => NpgsqlDbType.Smallint,
                "integer" or "int" => NpgsqlDbType.Integer,
                "bigint" => NpgsqlDbType.Bigint,
                "real" => NpgsqlDbType.Real,
                "double precision" => NpgsqlDbType.Double,
                "numeric" => NpgsqlDbType.Numeric,
                "money" => NpgsqlDbType.Money,

                // Text types
                "text" => NpgsqlDbType.Text,
                "xml" => NpgsqlDbType.Xml,
                "character varying" or "varchar" => NpgsqlDbType.Varchar,
                "character" => NpgsqlDbType.Char,
                "name" => NpgsqlDbType.Name,
                "refcursor" => NpgsqlDbType.Refcursor,
                "citext" => NpgsqlDbType.Citext,
                "jsonb" => NpgsqlDbType.Jsonb,
                "json" => NpgsqlDbType.Json,
                "jsonpath" => NpgsqlDbType.JsonPath,

                // Date/time types
                "timestamp without time zone" or "timestamp" => NpgsqlDbType.Timestamp,
                "timestamp with time zone" or "timestamptz" => NpgsqlDbType.TimestampTz,
                "date" => NpgsqlDbType.Date,
                "time without time zone" or "timetz" => NpgsqlDbType.Time,
                "time with time zone" or "time" => NpgsqlDbType.TimeTz,
                "interval" => NpgsqlDbType.Interval,

                // Network types
                "cidr" => NpgsqlDbType.Cidr,
                "inet" => NpgsqlDbType.Inet,
                "macaddr" => NpgsqlDbType.MacAddr,
                "macaddr8" => NpgsqlDbType.MacAddr8,

                // Full-text search types
                "tsquery" => NpgsqlDbType.TsQuery,
                "tsvector" => NpgsqlDbType.TsVector,

                // Geometry types
                "box" => NpgsqlDbType.Box,
                "circle" => NpgsqlDbType.Circle,
                "line" => NpgsqlDbType.Line,
                "lseg" => NpgsqlDbType.LSeg,
                "path" => NpgsqlDbType.Path,
                "point" => NpgsqlDbType.Point,
                "polygon" => NpgsqlDbType.Polygon,

                // LTree types
                "lquery" => NpgsqlDbType.LQuery,
                "ltree" => NpgsqlDbType.LTree,
                "ltxtquery" => NpgsqlDbType.LTxtQuery,

                // UInt types
                "oid" => NpgsqlDbType.Oid,
                "xid" => NpgsqlDbType.Xid,
                "xid8" => NpgsqlDbType.Xid8,
                "cid" => NpgsqlDbType.Cid,
                "regtype" => NpgsqlDbType.Regtype,
                "regconfig" => NpgsqlDbType.Regconfig,

                // Misc types
                "boolean" or "bool" => NpgsqlDbType.Boolean,
                "bytea" => NpgsqlDbType.Bytea,
                "uuid" => NpgsqlDbType.Uuid,
                "bit varying" or "varbit" => NpgsqlDbType.Varbit,
                "bit" => NpgsqlDbType.Bit,
                "hstore" => NpgsqlDbType.Hstore,

                "geometry" => NpgsqlDbType.Geometry,
                "geography" => NpgsqlDbType.Geography,

                // Built-in range types
                "int4range" => NpgsqlDbType.IntegerRange,
                "int8range" => NpgsqlDbType.BigIntRange,
                "numrange" => NpgsqlDbType.NumericRange,
                "tsrange" => NpgsqlDbType.TimestampRange,
                "tstzrange" => NpgsqlDbType.TimestampTzRange,
                "daterange" => NpgsqlDbType.DateRange,

                // Built-in multirange types
                "int4multirange" => NpgsqlDbType.IntegerMultirange,
                "int8multirange" => NpgsqlDbType.BigIntMultirange,
                "nummultirange" => NpgsqlDbType.NumericMultirange,
                "tsmultirange" => NpgsqlDbType.TimestampMultirange,
                "tstzmultirange" => NpgsqlDbType.TimestampTzMultirange,
                "datemultirange" => NpgsqlDbType.DateMultirange,

                // Internal types
                "int2vector" => NpgsqlDbType.Int2Vector,
                "oidvector" => NpgsqlDbType.Oidvector,
                "pg_lsn" => NpgsqlDbType.PgLsn,
                "tid" => NpgsqlDbType.Tid,
                "char" => NpgsqlDbType.InternalChar,

                _ => typeName.EndsWith("[]", StringComparison.Ordinal) &&
                     DataTypeNameToNpgsqlDbType(typeName.Substring(0, typeName.Length - 2)) is { } elementNpgsqlDbType &&
                     elementNpgsqlDbType != NpgsqlDbType.Unknown
                    ? elementNpgsqlDbType | NpgsqlDbType.Array
                    : NpgsqlDbType.Unknown // e.g. ranges
            };
        }

    internal static NpgsqlDbType? DbTypeToNpgsqlDbType(DbType dbType)
        => dbType switch
        {
            DbType.AnsiString            => NpgsqlDbType.Text,
            DbType.Binary                => NpgsqlDbType.Bytea,
            DbType.Byte                  => NpgsqlDbType.Smallint,
            DbType.Boolean               => NpgsqlDbType.Boolean,
            DbType.Currency              => NpgsqlDbType.Money,
            DbType.Date                  => NpgsqlDbType.Date,
            DbType.DateTime              => LegacyTimestampBehavior ? NpgsqlDbType.Timestamp : NpgsqlDbType.TimestampTz,
            DbType.Decimal               => NpgsqlDbType.Numeric,
            DbType.VarNumeric            => NpgsqlDbType.Numeric,
            DbType.Double                => NpgsqlDbType.Double,
            DbType.Guid                  => NpgsqlDbType.Uuid,
            DbType.Int16                 => NpgsqlDbType.Smallint,
            DbType.Int32                 => NpgsqlDbType.Integer,
            DbType.Int64                 => NpgsqlDbType.Bigint,
            DbType.Single                => NpgsqlDbType.Real,
            DbType.String                => NpgsqlDbType.Text,
            DbType.Time                  => NpgsqlDbType.Time,
            DbType.AnsiStringFixedLength => NpgsqlDbType.Text,
            DbType.StringFixedLength     => NpgsqlDbType.Text,
            DbType.Xml                   => NpgsqlDbType.Xml,
            DbType.DateTime2             => NpgsqlDbType.Timestamp,
            DbType.DateTimeOffset        => NpgsqlDbType.TimestampTz,

            DbType.Object                => null,
            DbType.SByte                 => null,
            DbType.UInt16                => null,
            DbType.UInt32                => null,
            DbType.UInt64                => null,

            _ => throw new ArgumentOutOfRangeException(nameof(dbType), dbType, null)
        };

    internal static DbType NpgsqlDbTypeToDbType(NpgsqlDbType npgsqlDbType)
        => npgsqlDbType switch
        {
            // Numeric types
            NpgsqlDbType.Smallint    => DbType.Int16,
            NpgsqlDbType.Integer     => DbType.Int32,
            NpgsqlDbType.Bigint      => DbType.Int64,
            NpgsqlDbType.Real        => DbType.Single,
            NpgsqlDbType.Double      => DbType.Double,
            NpgsqlDbType.Numeric     => DbType.Decimal,
            NpgsqlDbType.Money       => DbType.Currency,

            // Text types
            NpgsqlDbType.Text        => DbType.String,
            NpgsqlDbType.Xml         => DbType.Xml,
            NpgsqlDbType.Varchar     => DbType.String,
            NpgsqlDbType.Char        => DbType.String,
            NpgsqlDbType.Name        => DbType.String,
            NpgsqlDbType.Refcursor   => DbType.String,
            NpgsqlDbType.Citext      => DbType.String,
            NpgsqlDbType.Jsonb       => DbType.Object,
            NpgsqlDbType.Json        => DbType.Object,
            NpgsqlDbType.JsonPath    => DbType.String,

            // Date/time types
            NpgsqlDbType.Timestamp   => LegacyTimestampBehavior ? DbType.DateTime : DbType.DateTime2,
            NpgsqlDbType.TimestampTz => LegacyTimestampBehavior ? DbType.DateTimeOffset : DbType.DateTime,
            NpgsqlDbType.Date        => DbType.Date,
            NpgsqlDbType.Time        => DbType.Time,

            // Misc data types
            NpgsqlDbType.Bytea       => DbType.Binary,
            NpgsqlDbType.Boolean     => DbType.Boolean,
            NpgsqlDbType.Uuid        => DbType.Guid,

            NpgsqlDbType.Unknown     => DbType.Object,

            _ => DbType.Object
        };

    #endregion Static translation tables
}
