using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Npgsql.TypeMapping;
using NpgsqlTypes;

namespace Npgsql;

/// <summary>
/// Represents a collection of parameters relevant to a <see cref="NpgsqlCommand"/> as well as their respective mappings to columns in
/// a <see cref="DataSet"/>.
/// </summary>
public sealed class NpgsqlParameterCollection : DbParameterCollection, IList<NpgsqlParameter>
{
    internal const int LookupThreshold = 5;

    internal List<NpgsqlParameter> InternalList { get; } = new(5);
#if DEBUG
    internal static bool TwoPassCompatMode;
#else
        internal static readonly bool TwoPassCompatMode;
#endif

    static NpgsqlParameterCollection()
        => TwoPassCompatMode = AppContext.TryGetSwitch("Npgsql.EnableLegacyCaseInsensitiveDbParameters", out var enabled)
                               && enabled;

    // Dictionary lookups for GetValue to improve performance. _caseSensitiveLookup is only ever used in legacy two-pass mode.
    Dictionary<string, int>? _caseInsensitiveLookup;
    Dictionary<string, int>? _caseSensitiveLookup;

    /// <summary>
    /// Initializes a new instance of the NpgsqlParameterCollection class.
    /// </summary>
    internal NpgsqlParameterCollection()
    {
    }

    bool LookupEnabled => InternalList.Count >= LookupThreshold;

    void LookupClear()
    {
        _caseInsensitiveLookup?.Clear();
        _caseSensitiveLookup?.Clear();
    }

    void LookupAdd(string name, int index)
    {
        if (_caseInsensitiveLookup is null)
            return;

        if (TwoPassCompatMode && !_caseSensitiveLookup!.ContainsKey(name))
            _caseSensitiveLookup[name] = index;

        if (!_caseInsensitiveLookup.ContainsKey(name))
            _caseInsensitiveLookup[name] = index;
    }

    void LookupInsert(string name, int index)
    {
        if (_caseInsensitiveLookup is null)
            return;

        if (TwoPassCompatMode &&
            (!_caseSensitiveLookup!.TryGetValue(name, out var indexCs) || index < indexCs))
        {
            for (var i = index + 1; i < InternalList.Count; i++)
            {
                var parameterName = InternalList[i].TrimmedName;
                if (_caseSensitiveLookup.TryGetValue(parameterName, out var currentI) && currentI + 1 == i)
                    _caseSensitiveLookup[parameterName] = i;
            }

            _caseSensitiveLookup[name] = index;
        }

        if (!_caseInsensitiveLookup.TryGetValue(name, out var indexCi) || index < indexCi)
        {
            for (var i = index + 1; i < InternalList.Count; i++)
            {
                var parameterName = InternalList[i].TrimmedName;
                if (_caseInsensitiveLookup.TryGetValue(parameterName, out var currentI) && currentI + 1 == i)
                    _caseInsensitiveLookup[parameterName] = i;
            }

            _caseInsensitiveLookup[name] = index;
        }
    }

    void LookupRemove(string name, int index)
    {
        if (_caseInsensitiveLookup is null)
            return;

        if (TwoPassCompatMode && _caseSensitiveLookup!.Remove(name))
        {
            for (var i = index; i < InternalList.Count; i++)
            {
                var parameterName = InternalList[i].TrimmedName;
                if (_caseSensitiveLookup.TryGetValue(parameterName, out var currentI) && currentI - 1 == i)
                    _caseSensitiveLookup[parameterName] = i;
            }
        }

        if (_caseInsensitiveLookup.Remove(name))
        {
            for (var i = index; i < InternalList.Count; i++)
            {
                var parameterName = InternalList[i].TrimmedName;
                if (_caseInsensitiveLookup.TryGetValue(parameterName, out var currentI) && currentI - 1 == i)
                    _caseInsensitiveLookup[parameterName] = i;
            }

            // Fix-up the case-insensitive lookup to point to the next match, if any.
            for (var i = 0; i < InternalList.Count; i++)
            {
                var value = InternalList[i];
                if (string.Equals(name, value.TrimmedName, StringComparison.OrdinalIgnoreCase))
                {
                    _caseInsensitiveLookup[value.TrimmedName] = i;
                    break;
                }
            }
        }

    }

    void LookupChangeName(NpgsqlParameter parameter, string oldName, string oldTrimmedName, int index)
    {
        if (string.Equals(oldTrimmedName, parameter.TrimmedName, StringComparison.OrdinalIgnoreCase))
            return;

        if (oldName.Length != 0)
            LookupRemove(oldTrimmedName, index);
        if (!parameter.IsPositional)
            LookupAdd(parameter.TrimmedName, index);
    }

    internal void ChangeParameterName(NpgsqlParameter parameter, string? value)
    {
        var oldName = parameter.ParameterName;
        var oldTrimmedName = parameter.TrimmedName;
        parameter.ChangeParameterName(value);

        if (_caseInsensitiveLookup is null || _caseInsensitiveLookup.Count == 0)
            return;

        var index = IndexOf(parameter);
        if (index == -1) // This would be weird.
            return;

        LookupChangeName(parameter, oldName, oldTrimmedName, index);
    }

    #region NpgsqlParameterCollection Member

    /// <summary>
    /// Gets the <see cref="NpgsqlParameter"/> with the specified name.
    /// </summary>
    /// <param name="parameterName">The name of the <see cref="NpgsqlParameter"/> to retrieve.</param>
    /// <value>
    /// The <see cref="NpgsqlParameter"/> with the specified name, or a <see langword="null"/> reference if the parameter is not found.
    /// </value>
    public new NpgsqlParameter this[string parameterName]
    {
        get
        {
            if (parameterName is null)
                throw new ArgumentNullException(nameof(parameterName));

            var index = IndexOf(parameterName);
            if (index == -1)
                throw new ArgumentException("Parameter not found");

            return InternalList[index];
        }
        set
        {
            if (parameterName is null)
                throw new ArgumentNullException(nameof(parameterName));
            if (value is null)
                throw new ArgumentNullException(nameof(value));

            var index = IndexOf(parameterName);
            if (index == -1)
                throw new ArgumentException("Parameter not found");

            if (!string.Equals(parameterName, value.TrimmedName, StringComparison.OrdinalIgnoreCase))
                throw new ArgumentException(
                    "Parameter name must be a case-insensitive match with the property 'ParameterName' on the given NpgsqlParameter",
                    nameof(parameterName));

            var oldValue = InternalList[index];
            LookupChangeName(value, oldValue.ParameterName, oldValue.TrimmedName, index);

            InternalList[index] = value;
        }
    }

    /// <summary>
    /// Gets the <see cref="NpgsqlParameter"/> at the specified index.
    /// </summary>
    /// <param name="index">The zero-based index of the <see cref="NpgsqlParameter"/> to retrieve.</param>
    /// <value>The <see cref="NpgsqlParameter"/> at the specified index.</value>
    public new NpgsqlParameter this[int index]
    {
        get => InternalList[index];
        set
        {
            if (value is null)
                throw new ArgumentNullException(nameof(value));
            if (value.Collection is not null)
                throw new InvalidOperationException("The parameter already belongs to a collection");

            var oldValue = InternalList[index];

            if (ReferenceEquals(oldValue, value))
                return;

            LookupChangeName(value, oldValue.ParameterName, oldValue.TrimmedName, index);

            InternalList[index] = value;
            value.Collection = this;
            oldValue.Collection = null;
        }
    }

    /// <summary>
    /// Adds the specified <see cref="NpgsqlParameter"/> object to the <see cref="NpgsqlParameterCollection"/>.
    /// </summary>
    /// <param name="value">The <see cref="NpgsqlParameter"/> to add to the collection.</param>
    /// <returns>The index of the new <see cref="NpgsqlParameter"/> object.</returns>
    public NpgsqlParameter Add(NpgsqlParameter value)
    {
        if (value is null)
            throw new ArgumentNullException(nameof(value));
        if (value.Collection is not null)
            throw new InvalidOperationException("The parameter already belongs to a collection");

        InternalList.Add(value);
        value.Collection = this;
        if (!value.IsPositional)
            LookupAdd(value.TrimmedName, InternalList.Count - 1);
        return value;
    }

    /// <inheritdoc />
    void ICollection<NpgsqlParameter>.Add(NpgsqlParameter item)
        => Add(item);

    /// <summary>
    /// Adds a <see cref="NpgsqlParameter"/> to the <see cref="NpgsqlParameterCollection"/> given the specified parameter name and
    /// value.
    /// </summary>
    /// <param name="parameterName">The name of the <see cref="NpgsqlParameter"/>.</param>
    /// <param name="value">The value of the <see cref="NpgsqlParameter"/> to add to the collection.</param>
    /// <returns>The parameter that was added.</returns>
    public NpgsqlParameter AddWithValue(string parameterName, object value)
        => Add(new NpgsqlParameter(parameterName, value));

    /// <summary>
    /// Adds a <see cref="NpgsqlParameter"/> to the <see cref="NpgsqlParameterCollection"/> given the specified parameter name,
    /// data type and value.
    /// </summary>
    /// <param name="parameterName">The name of the <see cref="NpgsqlParameter"/>.</param>
    /// <param name="parameterType">One of the NpgsqlDbType values.</param>
    /// <param name="value">The value of the <see cref="NpgsqlParameter"/> to add to the collection.</param>
    /// <returns>The parameter that was added.</returns>
    public NpgsqlParameter AddWithValue(string parameterName, NpgsqlDbType parameterType, object value)
        => Add(new NpgsqlParameter(parameterName, parameterType) { Value = value });

    /// <summary>
    /// Adds a <see cref="NpgsqlParameter"/> to the <see cref="NpgsqlParameterCollection"/> given the specified parameter name and
    /// value.
    /// </summary>
    /// <param name="parameterName">The name of the <see cref="NpgsqlParameter"/>.</param>
    /// <param name="value">The value of the <see cref="NpgsqlParameter"/> to add to the collection.</param>
    /// <param name="parameterType">One of the <see cref="NpgsqlDbType"/> values.</param>
    /// <param name="size">The length of the column.</param>
    /// <returns>The parameter that was added.</returns>
    public NpgsqlParameter AddWithValue(string parameterName, NpgsqlDbType parameterType, int size, object value)
        => Add(new NpgsqlParameter(parameterName, parameterType, size) { Value = value });

    /// <summary>
    /// Adds a <see cref="NpgsqlParameter"/> to the <see cref="NpgsqlParameterCollection"/> given the specified parameter name and
    /// value.
    /// </summary>
    /// <param name="parameterName">The name of the <see cref="NpgsqlParameter"/>.</param>
    /// <param name="value">The value of the <see cref="NpgsqlParameter"/> to add to the collection.</param>
    /// <param name="parameterType">One of the <see cref="NpgsqlDbType"/> values.</param>
    /// <param name="size">The length of the column.</param>
    /// <param name="sourceColumn">The name of the source column.</param>
    /// <returns>The parameter that was added.</returns>
    public NpgsqlParameter AddWithValue(string parameterName, NpgsqlDbType parameterType, int size, string? sourceColumn, object value)
        => Add(new NpgsqlParameter(parameterName, parameterType, size, sourceColumn) { Value = value });

    /// <summary>
    /// Adds a <see cref="NpgsqlParameter"/> to the <see cref="NpgsqlParameterCollection"/> given the specified value.
    /// </summary>
    /// <param name="value">The value of the <see cref="NpgsqlParameter"/> to add to the collection.</param>
    /// <returns>The parameter that was added.</returns>
    public NpgsqlParameter AddWithValue(object value)
        => Add(new NpgsqlParameter { Value = value });

    /// <summary>
    /// Adds a <see cref="NpgsqlParameter"/> to the <see cref="NpgsqlParameterCollection"/> given the specified data type and value.
    /// </summary>
    /// <param name="parameterType">One of the <see cref="NpgsqlDbType"/> values.</param>
    /// <param name="value">The value of the <see cref="NpgsqlParameter"/> to add to the collection.</param>
    /// <returns>The parameter that was added.</returns>
    public NpgsqlParameter AddWithValue(NpgsqlDbType parameterType, object value)
        => Add(new NpgsqlParameter { NpgsqlDbType = parameterType, Value = value });

    /// <summary>
    /// Adds a <see cref="NpgsqlParameter"/> to the <see cref="NpgsqlParameterCollection"/> given the parameter name and the data type.
    /// </summary>
    /// <param name="parameterName">The name of the parameter.</param>
    /// <param name="parameterType">One of the <see cref="DbType"/> values.</param>
    /// <returns>The index of the new <see cref="NpgsqlParameter"/> object.</returns>
    public NpgsqlParameter Add(string parameterName, NpgsqlDbType parameterType)
        => Add(new NpgsqlParameter(parameterName, parameterType));

    /// <summary>
    /// Adds a <see cref="NpgsqlParameter"/> to the <see cref="NpgsqlParameterCollection"/> with the parameter name, the data type,
    /// and the column length.
    /// </summary>
    /// <param name="parameterName">The name of the parameter.</param>
    /// <param name="parameterType">One of the <see cref="DbType"/> values.</param>
    /// <param name="size">The length of the column.</param>
    /// <returns>The index of the new <see cref="NpgsqlParameter"/> object.</returns>
    public NpgsqlParameter Add(string parameterName, NpgsqlDbType parameterType, int size)
        => Add(new NpgsqlParameter(parameterName, parameterType, size));

    /// <summary>
    /// Adds a <see cref="NpgsqlParameter"/> to the <see cref="NpgsqlParameterCollection"/> with the parameter name, the data type, the
    /// column length, and the source column name.
    /// </summary>
    /// <param name="parameterName">The name of the parameter.</param>
    /// <param name="parameterType">One of the <see cref="DbType"/> values.</param>
    /// <param name="size">The length of the column.</param>
    /// <param name="sourceColumn">The name of the source column.</param>
    /// <returns>The index of the new <see cref="NpgsqlParameter"/> object.</returns>
    public NpgsqlParameter Add(string parameterName, NpgsqlDbType parameterType, int size, string sourceColumn)
        => Add(new NpgsqlParameter(parameterName, parameterType, size, sourceColumn));

    #endregion

    #region IDataParameterCollection Member

    /// <inheritdoc />
    // ReSharper disable once ImplicitNotNullOverridesUnknownExternalMember
    public override void RemoveAt(string parameterName)
        => RemoveAt(IndexOf(parameterName ?? throw new ArgumentNullException(nameof(parameterName))));

    /// <inheritdoc />
    public override bool Contains(string parameterName)
        => IndexOf(parameterName ?? throw new ArgumentNullException(nameof(parameterName))) != -1;

    /// <inheritdoc />
    public override int IndexOf(string parameterName)
    {
        if (parameterName is null)
            return -1;

        if (parameterName.Length > 0 && (parameterName[0] == ':' || parameterName[0] == '@'))
            parameterName = parameterName.Remove(0, 1);

        // Using a dictionary is always faster after around 10 items when matched against reference equality.
        // For string equality this is the case after ~3 items so we take a decent compromise going with 5.
        if (LookupEnabled && parameterName.Length != 0)
        {
            if (_caseInsensitiveLookup is null)
                BuildLookup();

            if (TwoPassCompatMode && _caseSensitiveLookup!.TryGetValue(parameterName, out var indexCs))
                return indexCs;

            if (_caseInsensitiveLookup!.TryGetValue(parameterName, out var indexCi))
                return indexCi;

            return -1;
        }

        // Start with case-sensitive search in two pass mode.
        if (TwoPassCompatMode)
        {
            for (var i = 0; i < InternalList.Count; i++)
            {
                var name = InternalList[i].TrimmedName;
                if (string.Equals(parameterName, InternalList[i].TrimmedName))
                    return i;
            }
        }

        // Then do case-insensitive search.
        for (var i = 0; i < InternalList.Count; i++)
        {
            var name = InternalList[i].TrimmedName;
            if (ReferenceEquals(parameterName, name) || string.Equals(parameterName, name, StringComparison.OrdinalIgnoreCase))
                return i;
        }

        return -1;

        void BuildLookup()
        {
            if (TwoPassCompatMode)
                _caseSensitiveLookup = new Dictionary<string, int>(InternalList.Count, StringComparer.Ordinal);

            _caseInsensitiveLookup = new Dictionary<string, int>(InternalList.Count, StringComparer.OrdinalIgnoreCase);

            for (var i = 0; i < InternalList.Count; i++)
            {
                var item = InternalList[i];
                if (!item.IsPositional)
                    LookupAdd(item.TrimmedName, i);
            }
        }
    }

    #endregion

    #region IList Member

    /// <inheritdoc />
    public override bool IsReadOnly => false;

    /// <summary>
    /// Removes the specified <see cref="NpgsqlParameter"/> from the collection using a specific index.
    /// </summary>
    /// <param name="index">The zero-based index of the parameter.</param>
    public override void RemoveAt(int index)
    {
        if (InternalList.Count - 1 < index)
            throw new ArgumentOutOfRangeException(nameof(index));

        Remove(InternalList[index]);
    }

    /// <inheritdoc />
    public override void Insert(int index, object value)
        => Insert(index, Cast(value));

    /// <summary>
    /// Removes the specified <see cref="NpgsqlParameter"/> from the collection.
    /// </summary>
    /// <param name="parameterName">The name of the <see cref="NpgsqlParameter"/> to remove from the collection.</param>
    public void Remove(string parameterName)
    {
        if (parameterName is null)
            throw new ArgumentNullException(nameof(parameterName));

        var index = IndexOf(parameterName);
        if (index < 0)
            throw new InvalidOperationException("No parameter with the specified name exists in the collection");

        RemoveAt(index);
    }

    /// <summary>
    /// Removes the specified <see cref="NpgsqlParameter"/> from the collection.
    /// </summary>
    /// <param name="value">The <see cref="NpgsqlParameter"/> to remove from the collection.</param>
    public override void Remove(object value)
        => Remove(Cast(value));

    /// <inheritdoc />
    public override bool Contains(object value)
        => value is NpgsqlParameter param && InternalList.Contains(param);

    /// <summary>
    /// Gets a value indicating whether a <see cref="NpgsqlParameter"/> with the specified parameter name exists in the collection.
    /// </summary>
    /// <param name="parameterName">The name of the <see cref="NpgsqlParameter"/> object to find.</param>
    /// <param name="parameter">
    /// A reference to the requested parameter is returned in this out param if it is found in the list.
    /// This value is <see langword="null"/> if the parameter is not found.
    /// </param>
    /// <returns>
    /// <see langword="true"/> if the collection contains the parameter and param will contain the parameter;
    /// otherwise, <see langword="false"/>.
    /// </returns>
    public bool TryGetValue(string parameterName, [NotNullWhen(true)] out NpgsqlParameter? parameter)
    {
        if (parameterName is null)
            throw new ArgumentNullException(nameof(parameterName));

        var index = IndexOf(parameterName);

        if (index != -1)
        {
            parameter = InternalList[index];
            return true;
        }

        parameter = null;
        return false;
    }

    /// <summary>
    /// Removes all items from the collection.
    /// </summary>
    public override void Clear()
    {
        // clean up parameters so they can be added to another command if required.
        foreach (var toRemove in InternalList)
            toRemove.Collection = null;

        InternalList.Clear();
        LookupClear();
    }

    /// <inheritdoc />
    public override int IndexOf(object value)
        => IndexOf(Cast(value));

    /// <inheritdoc />
    public override int Add(object value)
    {
        Add(Cast(value));
        return Count - 1;
    }

    /// <inheritdoc />
    public override bool IsFixedSize => false;

    #endregion

    #region ICollection Member

    /// <inheritdoc />
    public override bool IsSynchronized => (InternalList as ICollection).IsSynchronized;

    /// <summary>
    /// Gets the number of <see cref="NpgsqlParameter"/> objects in the collection.
    /// </summary>
    /// <value>The number of <see cref="NpgsqlParameter"/> objects in the collection.</value>
    public override int Count => InternalList.Count;

    /// <inheritdoc />
    public override void CopyTo(Array array, int index)
        => ((ICollection)InternalList).CopyTo(array, index);

    /// <inheritdoc />
    bool ICollection<NpgsqlParameter>.IsReadOnly => false;

    /// <inheritdoc />
    public override object SyncRoot => ((ICollection)InternalList).SyncRoot;

    #endregion

    #region IEnumerable Member

    IEnumerator<NpgsqlParameter> IEnumerable<NpgsqlParameter>.GetEnumerator()
        => InternalList.GetEnumerator();

    /// <inheritdoc />
    public override IEnumerator GetEnumerator() => InternalList.GetEnumerator();

    #endregion

    /// <inheritdoc />
    public override void AddRange(Array values)
    {
        if (values is null)
            throw new ArgumentNullException(nameof(values));

        foreach (var parameter in values)
            Add(Cast(parameter) ?? throw new ArgumentException("Collection contains a null value.", nameof(values)));
    }

    /// <inheritdoc />
    protected override DbParameter GetParameter(string parameterName)
        => this[parameterName];

    /// <inheritdoc />
    protected override DbParameter GetParameter(int index)
        => this[index];

    /// <inheritdoc />
    protected override void SetParameter(string parameterName, DbParameter value)
        => this[parameterName] = Cast(value);

    /// <inheritdoc />
    protected override void SetParameter(int index, DbParameter value)
        => this[index] = Cast(value);

    /// <summary>
    /// Report the offset within the collection of the given parameter.
    /// </summary>
    /// <param name="item">Parameter to find.</param>
    /// <returns>Index of the parameter, or -1 if the parameter is not present.</returns>
    public int IndexOf(NpgsqlParameter item)
        => InternalList.IndexOf(item);

    /// <summary>
    /// Insert the specified parameter into the collection.
    /// </summary>
    /// <param name="index">Index of the existing parameter before which to insert the new one.</param>
    /// <param name="item">Parameter to insert.</param>
    public void Insert(int index, NpgsqlParameter item)
    {
        if (item is null)
            throw new ArgumentNullException(nameof(item));
        if (item.Collection != null)
            throw new Exception("The parameter already belongs to a collection");

        InternalList.Insert(index, item);
        item.Collection = this;
        if (!item.IsPositional)
            LookupInsert(item.TrimmedName, index);
    }

    /// <summary>
    /// Report whether the specified parameter is present in the collection.
    /// </summary>
    /// <param name="item">Parameter to find.</param>
    /// <returns>True if the parameter was found, otherwise false.</returns>
    public bool Contains(NpgsqlParameter item) => InternalList.Contains(item);

    /// <summary>
    /// Remove the specified parameter from the collection.
    /// </summary>
    /// <param name="item">Parameter to remove.</param>
    /// <returns>True if the parameter was found and removed, otherwise false.</returns>
    public bool Remove(NpgsqlParameter item)
    {
        if (item == null)
            throw new ArgumentNullException(nameof(item));
        if (item.Collection != this)
            throw new InvalidOperationException("The item does not belong to this collection");

        var index = IndexOf(item);
        if (index >= 0)
        {
            InternalList.RemoveAt(index);
            if (!LookupEnabled)
                LookupClear();
            if (!item.IsPositional)
                LookupRemove(item.TrimmedName, index);
            item.Collection = null;
            return true;
        }

        return false;
    }

    /// <summary>
    /// Convert collection to a System.Array.
    /// </summary>
    /// <param name="array">Destination array.</param>
    /// <param name="arrayIndex">Starting index in destination array.</param>
    public void CopyTo(NpgsqlParameter[] array, int arrayIndex)
        => InternalList.CopyTo(array, arrayIndex);

    /// <summary>
    /// Convert collection to a System.Array.
    /// </summary>
    /// <returns>NpgsqlParameter[]</returns>
    public NpgsqlParameter[] ToArray() => InternalList.ToArray();

    internal void CloneTo(NpgsqlParameterCollection other)
    {
        other.InternalList.Clear();
        foreach (var param in InternalList)
        {
            var newParam = param.Clone();
            newParam.Collection = this;
            other.InternalList.Add(newParam);
        }

        if (LookupEnabled && _caseInsensitiveLookup is not null)
        {
            other._caseInsensitiveLookup = new Dictionary<string, int>(_caseInsensitiveLookup, StringComparer.OrdinalIgnoreCase);
            if (TwoPassCompatMode)
            {
                Debug.Assert(_caseSensitiveLookup is not null);
                other._caseSensitiveLookup = new Dictionary<string, int>(_caseSensitiveLookup, StringComparer.Ordinal);
            }
        }
    }

    internal void ValidateAndBind(ConnectorTypeMapper typeMapper)
    {
        HasOutputParameters = false;
        PlaceholderType = PlaceholderType.NoParameters;

        for (var i = 0; i < InternalList.Count; i++)
        {
            var p = InternalList[i];

            CalculatePlaceholderType(p);

            switch (p.Direction)
            {
            case ParameterDirection.Input:
                break;

            case ParameterDirection.InputOutput:
                if (PlaceholderType == PlaceholderType.Positional)
                    throw new NotSupportedException("Output parameters are not supported in positional mode");
                HasOutputParameters = true;
                break;

            case ParameterDirection.Output:
                if (PlaceholderType == PlaceholderType.Positional)
                    throw new NotSupportedException("Output parameters are not supported in positional mode");
                HasOutputParameters = true;
                continue;

            case ParameterDirection.ReturnValue:
                // Simply ignored
                continue;

            default:
                throw new ArgumentOutOfRangeException(nameof(ParameterDirection),
                    $"Unhandled {nameof(ParameterDirection)} value: {p.Direction}");
            }

            p.Bind(typeMapper);
            p.LengthCache?.Clear();
            p.ValidateAndGetLength();
        }
    }

    internal void CalculatePlaceholderType(NpgsqlParameter p)
    {
        if (p.IsPositional)
        {
            switch (PlaceholderType)
            {
            case PlaceholderType.NoParameters:
                PlaceholderType = PlaceholderType.Positional;
                break;

            case PlaceholderType.Named:
                PlaceholderType = PlaceholderType.Mixed;
                break;

            case PlaceholderType.Positional:
            case PlaceholderType.Mixed:
                break;

            default:
                throw new ArgumentOutOfRangeException(
                    nameof(PlaceholderType), $"Unknown {nameof(PlaceholderType)} value: {PlaceholderType}");
            }
        }
        else
        {
            switch (PlaceholderType)
            {
            case PlaceholderType.NoParameters:
                PlaceholderType = PlaceholderType.Named;
                break;

            case PlaceholderType.Positional:
                PlaceholderType = PlaceholderType.Mixed;
                break;

            case PlaceholderType.Named:
            case PlaceholderType.Mixed:
                break;

            default:
                throw new ArgumentOutOfRangeException(
                    nameof(PlaceholderType), $"Unknown {nameof(PlaceholderType)} value: {PlaceholderType}");
            }
        }
    }

    internal bool HasOutputParameters { get; set; }
    internal PlaceholderType PlaceholderType { get; set; }

    static NpgsqlParameter Cast(object? value)
        => value is NpgsqlParameter p
            ? p
            : throw new InvalidCastException(
                $"The value \"{value}\" is not of type \"{nameof(NpgsqlParameter)}\" and cannot be used in this parameter collection.");
}

enum PlaceholderType
{
    /// <summary>
    /// The parameter collection includes no parameters.
    /// </summary>
    NoParameters,

    /// <summary>
    /// The parameter collection includes only named parameters.
    /// </summary>
    Named,

    /// <summary>
    /// The parameter collection includes only positional parameters.
    /// </summary>
    Positional,

    /// <summary>
    /// The parameter collection includes both named and positional parameters.
    /// This is only supported when <see cref="NpgsqlCommand.CommandType" /> is set to <see cref="CommandType.StoredProcedure" />.
    /// </summary>
    Mixed
}