// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel;            //Component
using System.Data.Common;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;

// todo:
// There may be two ways to improve performance:
// 1. pool statements on the connection object
// 2. Do not create a datareader object for non-datareader returning command execution.
//
// We do not want to do the effort unless we have to squeze performance.

namespace System.Data.Odbc
{
    [Designer("Microsoft.VSDesigner.Data.VS.OdbcCommandDesigner, Microsoft.VSDesigner, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
    [ToolboxItem(true)]
    public sealed class OdbcCommand : DbCommand, ICloneable
    {
        private static int s_objectTypeCount; // Bid counter
        internal readonly int ObjectID = System.Threading.Interlocked.Increment(ref s_objectTypeCount);

        private string? _commandText;
        private CommandType _commandType;
        private int _commandTimeout = ADP.DefaultCommandTimeout;
        private UpdateRowSource _updatedRowSource = UpdateRowSource.Both;
        private bool _designTimeInvisible;
        private bool _isPrepared;                        // true if the command is prepared

        private OdbcConnection? _connection;
        private OdbcTransaction? _transaction;

        private WeakReference? _weakDataReaderReference;

        private CMDWrapper? _cmdWrapper;

        private OdbcParameterCollection? _parameterCollection;   // Parameter collection

        private ConnectionState _cmdState;

        public OdbcCommand() : base()
        {
            GC.SuppressFinalize(this);
        }

        public OdbcCommand(string? cmdText) : this()
        {
            // note: arguments are assigned to properties so we do not have to trace them.
            // We still need to include them into the argument list of the definition!
            CommandText = cmdText;
        }

        public OdbcCommand(string? cmdText, OdbcConnection? connection) : this()
        {
            CommandText = cmdText;
            Connection = connection;
        }

        public OdbcCommand(string? cmdText, OdbcConnection? connection, OdbcTransaction? transaction) : this()
        {
            CommandText = cmdText;
            Connection = connection;
            Transaction = transaction;
        }

        private void DisposeDeadDataReader()
        {
            if (ConnectionState.Fetching == _cmdState)
            {
                if (null != _weakDataReaderReference && !_weakDataReaderReference.IsAlive)
                {
                    if (_cmdWrapper != null)
                    {
                        _cmdWrapper.FreeKeyInfoStatementHandle(ODBC32.STMT.CLOSE);
                        _cmdWrapper.FreeStatementHandle(ODBC32.STMT.CLOSE);
                    }
                    CloseFromDataReader();
                }
            }
        }

        private void DisposeDataReader()
        {
            if (null != _weakDataReaderReference)
            {
                IDisposable? reader = (IDisposable?)_weakDataReaderReference.Target;
                if ((null != reader) && _weakDataReaderReference.IsAlive)
                {
                    ((IDisposable)reader).Dispose();
                }
                CloseFromDataReader();
            }
        }

        internal void DisconnectFromDataReaderAndConnection()
        {
            // get a reference to the datareader if it is alive
            OdbcDataReader? liveReader = null;
            if (_weakDataReaderReference != null)
            {
                OdbcDataReader? reader;
                reader = (OdbcDataReader?)_weakDataReaderReference.Target;
                if (_weakDataReaderReference.IsAlive)
                {
                    liveReader = reader;
                }
            }

            // remove reference to this from the live datareader
            if (liveReader != null)
            {
                liveReader.Command = null;
            }

            _transaction = null;

            if (null != _connection)
            {
                _connection.RemoveWeakReference(this);
                _connection = null;
            }

            // if the reader is dead we have to dismiss the statement
            if (liveReader == null)
            {
                CloseCommandWrapper();
            }
            // else DataReader now has exclusive ownership
            _cmdWrapper = null;
        }

        protected override void Dispose(bool disposing)
        { // MDAC 65459
            if (disposing)
            {
                // release mananged objects
                // in V1.0, V1.1 the Connection,Parameters,CommandText,Transaction where reset
                this.DisconnectFromDataReaderAndConnection();
                _parameterCollection = null;
                CommandText = null;
            }
            _cmdWrapper = null;                         // let go of the CommandWrapper
            _isPrepared = false;

            base.Dispose(disposing);    // notify base classes
        }

        internal bool Canceling
        {
            get
            {
                return _cmdWrapper!.Canceling;
            }
        }

        [AllowNull]
        [Editor("Microsoft.VSDesigner.Data.Odbc.Design.OdbcCommandTextEditor, Microsoft.VSDesigner, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
                "System.Drawing.Design.UITypeEditor, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
        public override string CommandText
        {
            get
            {
                return _commandText ?? string.Empty;
            }
            set
            {
                if (_commandText != value)
                {
                    PropertyChanging();
                    _commandText = value;
                }
            }
        }

        public override int CommandTimeout
        { // V1.2.3300, XXXCommand V1.0.5000
            get
            {
                return _commandTimeout;
            }
            set
            {
                if (value < 0)
                {
                    throw ADP.InvalidCommandTimeout(value);
                }
                if (value != _commandTimeout)
                {
                    PropertyChanging();
                    _commandTimeout = value;
                }
            }
        }

        public void ResetCommandTimeout()
        { // V1.2.3300
            if (ADP.DefaultCommandTimeout != _commandTimeout)
            {
                PropertyChanging();
                _commandTimeout = ADP.DefaultCommandTimeout;
            }
        }

        private bool ShouldSerializeCommandTimeout()
        { // V1.2.3300
            return (ADP.DefaultCommandTimeout != _commandTimeout);
        }

        [
        DefaultValue(System.Data.CommandType.Text),
        ]
        public override CommandType CommandType
        {
            get
            {
                CommandType cmdType = _commandType;
                return ((0 != cmdType) ? cmdType : CommandType.Text);
            }
            set
            {
                switch (value)
                { // @perfnote: Enum.IsDefined
                    case CommandType.Text:
                    case CommandType.StoredProcedure:
                        PropertyChanging();
                        _commandType = value;
                        break;
                    case CommandType.TableDirect:
                        throw ODBC.NotSupportedCommandType(value);
                    default:
                        throw ADP.InvalidCommandType(value);
                }
            }
        }

        [Editor("Microsoft.VSDesigner.Data.Design.DbConnectionEditor, Microsoft.VSDesigner, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
                "System.Drawing.Design.UITypeEditor, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
        public new OdbcConnection? Connection
        {
            get
            {
                return _connection;
            }
            set
            {
                if (value != _connection)
                {
                    PropertyChanging();
                    this.DisconnectFromDataReaderAndConnection();
                    Debug.Assert(null == _cmdWrapper, "has CMDWrapper when setting connection");
                    _connection = value;
                    //OnSchemaChanged();
                }
            }
        }

        protected override DbConnection? DbConnection
        { // V1.2.3300
            get
            {
                return Connection;
            }
            set
            {
                Connection = (OdbcConnection?)value;
            }
        }

        protected override DbParameterCollection DbParameterCollection
        { // V1.2.3300
            get
            {
                return Parameters;
            }
        }

        protected override DbTransaction? DbTransaction
        { // V1.2.3300
            get
            {
                return Transaction;
            }
            set
            {
                Transaction = (OdbcTransaction?)value;
            }
        }

        // @devnote: By default, the cmd object is visible on the design surface (i.e. VS7 Server Tray)
        // to limit the number of components that clutter the design surface,
        // when the DataAdapter design wizard generates the insert/update/delete commands it will
        // set the DesignTimeVisible property to false so that cmds won't appear as individual objects
        [
        DefaultValue(true),
        DesignOnly(true),
        Browsable(false),
        EditorBrowsableAttribute(EditorBrowsableState.Never),
        ]
        public override bool DesignTimeVisible
        { // V1.2.3300, XXXCommand V1.0.5000
            get
            {
                return !_designTimeInvisible;
            }
            set
            {
                _designTimeInvisible = !value;
                TypeDescriptor.Refresh(this); // VS7 208845
            }
        }

        internal bool HasParameters
        {
            get
            {
                return (null != _parameterCollection);
            }
        }

        [
        DesignerSerializationVisibility(DesignerSerializationVisibility.Content),
        ]
        public new OdbcParameterCollection Parameters
        {
            get
            {
                if (null == _parameterCollection)
                {
                    _parameterCollection = new OdbcParameterCollection();
                }
                return _parameterCollection;
            }
        }

        [
        Browsable(false),
        DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden),
        ]
        public new OdbcTransaction? Transaction
        {
            get
            {
                if ((null != _transaction) && (null == _transaction.Connection))
                {
                    _transaction = null;       // Dawn of the Dead
                }
                return _transaction;
            }
            set
            {
                if (_transaction != value)
                {
                    PropertyChanging(); // fire event before value is validated
                    _transaction = value;
                }
            }
        }

        [
        DefaultValue(System.Data.UpdateRowSource.Both),
        ]
        public override UpdateRowSource UpdatedRowSource
        { // V1.2.3300, XXXCommand V1.0.5000
            get
            {
                return _updatedRowSource;
            }
            set
            {
                switch (value)
                { // @perfnote: Enum.IsDefined
                    case UpdateRowSource.None:
                    case UpdateRowSource.OutputParameters:
                    case UpdateRowSource.FirstReturnedRecord:
                    case UpdateRowSource.Both:
                        _updatedRowSource = value;
                        break;
                    default:
                        throw ADP.InvalidUpdateRowSource(value);
                }
            }
        }

        internal OdbcDescriptorHandle GetDescriptorHandle(ODBC32.SQL_ATTR attribute)
        {
            return _cmdWrapper!.GetDescriptorHandle(attribute);
        }


        // GetStatementHandle
        // ------------------
        // Try to return a cached statement handle.
        //
        // Creates a CmdWrapper object if necessary
        // If no handle is available a handle will be allocated.
        // Bindings will be unbound if a handle is cached and the bindings are invalid.
        //
        internal CMDWrapper GetStatementHandle()
        {
            // update the command wrapper object, allocate buffer
            // create reader object
            //
            if (_cmdWrapper == null)
            {
                _cmdWrapper = new CMDWrapper(_connection!);

                Debug.Assert(null != _connection, "GetStatementHandle without connection?");
                _connection.AddWeakReference(this, OdbcReferenceCollection.CommandTag);
            }

            _cmdWrapper._dataReaderBuf ??= new CNativeBuffer(4096);

            // if there is already a statement handle we need to do some cleanup
            //
            if (null == _cmdWrapper.StatementHandle)
            {
                _isPrepared = false;
                _cmdWrapper.CreateStatementHandle();
            }
            else if ((null != _parameterCollection) && _parameterCollection.RebindCollection)
            {
                _cmdWrapper.FreeStatementHandle(ODBC32.STMT.RESET_PARAMS);
            }
            return _cmdWrapper;
        }

        // OdbcCommand.Cancel()
        //
        // In ODBC3.0 ... a call to SQLCancel when no processing is done has no effect at all
        // (ODBC Programmer's Reference ...)
        //

        public override void Cancel()
        {
            CMDWrapper? wrapper = _cmdWrapper;
            if (null != wrapper)
            {
                wrapper.Canceling = true;
                OdbcStatementHandle? stmt = wrapper.StatementHandle;
                if (null != stmt)
                {
                    lock (stmt)
                    {
                        // Cancel the statement
                        ODBC32.SQLRETURN retcode = stmt.Cancel();

                        // copy of StatementErrorHandler, because stmt may become null
                        switch (retcode)
                        {
                            case ODBC32.SQLRETURN.SUCCESS:
                            case ODBC32.SQLRETURN.SUCCESS_WITH_INFO:
                                // don't fire info message events on cancel
                                break;
                            default:
                                throw wrapper.Connection.HandleErrorNoThrow(stmt, retcode)!;
                        }
                    }
                }
            }
        }


        object ICloneable.Clone()
        {
            OdbcCommand clone = new OdbcCommand();
            clone.CommandText = CommandText;
            clone.CommandTimeout = this.CommandTimeout;
            clone.CommandType = CommandType;
            clone.Connection = this.Connection;
            clone.Transaction = this.Transaction;
            clone.UpdatedRowSource = UpdatedRowSource;

            if ((null != _parameterCollection) && (0 < Parameters.Count))
            {
                OdbcParameterCollection parameters = clone.Parameters;
                foreach (ICloneable parameter in Parameters)
                {
                    parameters.Add(parameter.Clone());
                }
            }
            return clone;
        }

        internal bool RecoverFromConnection()
        {
            DisposeDeadDataReader();
            return (ConnectionState.Closed == _cmdState);
        }

        private void CloseCommandWrapper()
        {
            CMDWrapper? wrapper = _cmdWrapper;
            if (null != wrapper)
            {
                try
                {
                    wrapper.Dispose();

                    _connection?.RemoveWeakReference(this);
                }
                finally
                {
                    _cmdWrapper = null;
                }
            }
        }

        internal void CloseFromConnection()
        {
            if (null != _parameterCollection)
            {
                _parameterCollection.RebindCollection = true;
            }
            DisposeDataReader();
            CloseCommandWrapper();
            _isPrepared = false;
            _transaction = null;
        }

        internal void CloseFromDataReader()
        {
            _weakDataReaderReference = null;
            _cmdState = ConnectionState.Closed;
        }

        public new OdbcParameter CreateParameter()
        {
            return new OdbcParameter();
        }

        protected override DbParameter CreateDbParameter()
        {
            return CreateParameter();
        }

        protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior)
        {
            return ExecuteReader(behavior);
        }

        public override int ExecuteNonQuery()
        {
            using (OdbcDataReader reader = ExecuteReaderObject(0, ADP.ExecuteNonQuery, false))
            {
                reader.Close();
                return reader.RecordsAffected;
            }
        }

        public new OdbcDataReader ExecuteReader()
        {
            return ExecuteReader(0/*CommandBehavior*/);
        }


        public new OdbcDataReader ExecuteReader(CommandBehavior behavior)
        {
            return ExecuteReaderObject(behavior, ADP.ExecuteReader, true);
        }

        internal OdbcDataReader ExecuteReaderFromSQLMethod(object?[]? methodArguments,
                                                            ODBC32.SQL_API method)
        {
            return ExecuteReaderObject(CommandBehavior.Default, method.ToString(), true, methodArguments, method);
        }

        private OdbcDataReader ExecuteReaderObject(CommandBehavior behavior, string method, bool needReader)
        { // MDAC 68324
            if ((CommandText == null) || (CommandText.Length == 0))
            {
                throw (ADP.CommandTextRequired(method));
            }
            // using all functions to tell ExecuteReaderObject that
            return ExecuteReaderObject(behavior, method, needReader, null, ODBC32.SQL_API.SQLEXECDIRECT);
        }

        private OdbcDataReader ExecuteReaderObject(CommandBehavior behavior,
                                                   string method,
                                                   bool needReader,
                                                   object?[]? methodArguments,
                                                   ODBC32.SQL_API odbcApiMethod)
        { // MDAC 68324
            OdbcDataReader? localReader = null;
            try
            {
                DisposeDeadDataReader();    // this is a no-op if cmdState is not Fetching
                ValidateConnectionAndTransaction(method);  // cmdState will change to Executing

                if (0 != (CommandBehavior.SingleRow & behavior))
                {
                    // CommandBehavior.SingleRow implies CommandBehavior.SingleResult
                    behavior |= CommandBehavior.SingleResult;
                }

                ODBC32.SQLRETURN retcode;

                OdbcStatementHandle stmt = GetStatementHandle().StatementHandle!;
                _cmdWrapper!.Canceling = false;

                if (null != _weakDataReaderReference)
                {
                    if (_weakDataReaderReference.IsAlive)
                    {
                        object? target = _weakDataReaderReference.Target;
                        if (null != target && _weakDataReaderReference.IsAlive)
                        {
                            if (!((OdbcDataReader)target).IsClosed)
                            {
                                throw ADP.OpenReaderExists(); // MDAC 66411
                            }
                        }
                    }
                }
                localReader = new OdbcDataReader(this, _cmdWrapper, behavior);

                //Set command properties
                //Not all drivers support timeout. So fail silently if error
                if (!Connection!.ProviderInfo.NoQueryTimeout)
                {
                    TrySetStatementAttribute(stmt,
                        ODBC32.SQL_ATTR.QUERY_TIMEOUT,
                        (IntPtr)this.CommandTimeout);
                }

                // todo: If we remember the state we can omit a lot of SQLSetStmtAttrW calls ...
                // if we do not create a reader we do not even need to do that
                if (needReader)
                {
                    if (Connection.IsV3Driver)
                    {
                        if (!Connection.ProviderInfo.NoSqlSoptSSNoBrowseTable && !Connection.ProviderInfo.NoSqlSoptSSHiddenColumns)
                        {
                            // Need to get the metadata information

                            //SQLServer actually requires browse info turned on ahead of time...
                            //Note: We ignore any failures, since this is SQLServer specific
                            //We won't specialcase for SQL Server but at least for non-V3 drivers
                            if (localReader.IsBehavior(CommandBehavior.KeyInfo))
                            {
                                if (!_cmdWrapper._ssKeyInfoModeOn)
                                {
                                    TrySetStatementAttribute(stmt, (ODBC32.SQL_ATTR)ODBC32.SQL_SOPT_SS.NOBROWSETABLE, (IntPtr)ODBC32.SQL_NB.ON);
                                    TrySetStatementAttribute(stmt, (ODBC32.SQL_ATTR)ODBC32.SQL_SOPT_SS.HIDDEN_COLUMNS, (IntPtr)ODBC32.SQL_HC.ON);
                                    _cmdWrapper._ssKeyInfoModeOff = false;
                                    _cmdWrapper._ssKeyInfoModeOn = true;
                                }
                            }
                            else
                            {
                                if (!_cmdWrapper._ssKeyInfoModeOff)
                                {
                                    TrySetStatementAttribute(stmt, (ODBC32.SQL_ATTR)ODBC32.SQL_SOPT_SS.NOBROWSETABLE, (IntPtr)ODBC32.SQL_NB.OFF);
                                    TrySetStatementAttribute(stmt, (ODBC32.SQL_ATTR)ODBC32.SQL_SOPT_SS.HIDDEN_COLUMNS, (IntPtr)ODBC32.SQL_HC.OFF);
                                    _cmdWrapper._ssKeyInfoModeOff = true;
                                    _cmdWrapper._ssKeyInfoModeOn = false;
                                }
                            }
                        }
                    }
                }

                if (localReader.IsBehavior(CommandBehavior.KeyInfo) ||
                    localReader.IsBehavior(CommandBehavior.SchemaOnly))
                {
                    retcode = stmt.Prepare(CommandText);

                    if (ODBC32.SQLRETURN.SUCCESS != retcode)
                    {
                        _connection!.HandleError(stmt, retcode);
                    }
                }

                bool mustRelease = false;
                CNativeBuffer? parameterBuffer = _cmdWrapper._nativeParameterBuffer;

                try
                {
                    //Handle Parameters
                    //Note: We use the internal variable as to not instante a new object collection,
                    //for the common case of using no parameters.
                    if ((null != _parameterCollection) && (0 < _parameterCollection.Count))
                    {
                        int parameterBufferSize = _parameterCollection.CalcParameterBufferSize(this);

                        if (null == parameterBuffer || parameterBuffer.Length < parameterBufferSize)
                        {
                            parameterBuffer?.Dispose();
                            parameterBuffer = new CNativeBuffer(parameterBufferSize);
                            _cmdWrapper._nativeParameterBuffer = parameterBuffer;
                        }
                        else
                        {
                            parameterBuffer.ZeroMemory();
                        }

                        parameterBuffer.DangerousAddRef(ref mustRelease);

                        _parameterCollection.Bind(this, _cmdWrapper, parameterBuffer);
                    }

                    if (!localReader.IsBehavior(CommandBehavior.SchemaOnly))
                    {
                        // Can't get the KeyInfo after command execution (SQL Server only since it does not support multiple
                        // results on the same connection). Stored procedures (SP) do not return metadata before actual execution
                        // Need to check the column count since the command type may not be set to SP for a SP.
                        if ((localReader.IsBehavior(CommandBehavior.KeyInfo) || localReader.IsBehavior(CommandBehavior.SchemaOnly))
                            && (CommandType != CommandType.StoredProcedure))
                        {
                            short cColsAffected;
                            retcode = stmt.NumberOfResultColumns(out cColsAffected);
                            if (retcode == ODBC32.SQLRETURN.SUCCESS || retcode == ODBC32.SQLRETURN.SUCCESS_WITH_INFO)
                            {
                                if (cColsAffected > 0)
                                {
                                    localReader.GetSchemaTable();
                                }
                            }
                            else if (retcode == ODBC32.SQLRETURN.NO_DATA)
                            {
                                // do nothing
                            }
                            else
                            {
                                // any other returncode indicates an error
                                _connection!.HandleError(stmt, retcode);
                            }
                        }

                        switch (odbcApiMethod)
                        {
                            case ODBC32.SQL_API.SQLEXECDIRECT:
                                if (localReader.IsBehavior(CommandBehavior.KeyInfo) || _isPrepared)
                                {
                                    //Already prepared, so use SQLExecute
                                    retcode = stmt.Execute();
                                    // Build metadata here
                                    // localReader.GetSchemaTable();
                                }
                                else
                                {
#if DEBUG
                                    //if (AdapterSwitches.OleDbTrace.TraceInfo) {
                                    //    ADP.DebugWriteLine("SQLExecDirectW: " + CommandText);
                                    //}
#endif
                                    //SQLExecDirect
                                    retcode = stmt.ExecuteDirect(CommandText);
                                }
                                break;

                            case ODBC32.SQL_API.SQLTABLES:
                                retcode = stmt.Tables((string)methodArguments![0]!,  //TableCatalog
                                    (string)methodArguments[1]!,  //TableSchema,
                                    (string)methodArguments[2]!,  //TableName
                                    (string)methodArguments[3]!); //TableType
                                break;

                            case ODBC32.SQL_API.SQLCOLUMNS:
                                retcode = stmt.Columns((string)methodArguments![0]!,  //TableCatalog
                                    (string)methodArguments[1]!,  //TableSchema
                                    (string)methodArguments[2]!,  //TableName
                                    (string)methodArguments[3]!); //ColumnName
                                break;

                            case ODBC32.SQL_API.SQLPROCEDURES:
                                retcode = stmt.Procedures((string)methodArguments![0]!,  //ProcedureCatalog
                                    (string)methodArguments[1]!,  //ProcedureSchema
                                    (string)methodArguments[2]!); //procedureName
                                break;

                            case ODBC32.SQL_API.SQLPROCEDURECOLUMNS:
                                retcode = stmt.ProcedureColumns((string)methodArguments![0]!,  //ProcedureCatalog
                                    (string)methodArguments[1]!,  //ProcedureSchema
                                    (string)methodArguments[2]!,  //procedureName
                                    (string)methodArguments[3]!); //columnName
                                break;

                            case ODBC32.SQL_API.SQLSTATISTICS:
                                retcode = stmt.Statistics((string)methodArguments![0]!,  //TableCatalog
                                    (string)methodArguments[1]!,  //TableSchema
                                    (string)methodArguments[2]!,  //TableName
                                    (short)methodArguments[3]!,   //IndexTrpe
                                    (short)methodArguments[4]!);  //Accuracy
                                break;

                            case ODBC32.SQL_API.SQLGETTYPEINFO:
                                retcode = stmt.GetTypeInfo((short)methodArguments![0]!);  //SQL Type
                                break;

                            default:
                                // this should NEVER happen
                                Debug.Fail("ExecuteReaderObjectcalled with unsupported ODBC API method.");
                                throw ADP.InvalidOperation(method.ToString());
                        }

                        //Note: Execute will return NO_DATA for Update/Delete non-row returning queries
                        if ((ODBC32.SQLRETURN.SUCCESS != retcode) && (ODBC32.SQLRETURN.NO_DATA != retcode))
                        {
                            _connection!.HandleError(stmt, retcode);
                        }
                    } // end SchemaOnly
                }
                finally
                {
                    if (mustRelease)
                    {
                        parameterBuffer!.DangerousRelease();
                    }
                }

                _weakDataReaderReference = new WeakReference(localReader);

                // XXXCommand.Execute should position reader on first row returning result
                // any exceptions in the initial non-row returning results should be thrown
                // from ExecuteXXX not the DataReader
                if (!localReader.IsBehavior(CommandBehavior.SchemaOnly))
                {
                    localReader.FirstResult();
                }
                _cmdState = ConnectionState.Fetching;
            }
            finally
            {
                if (ConnectionState.Fetching != _cmdState)
                {
                    if (null != localReader)
                    {
                        // clear bindings so we don't grab output parameters on a failed execute
                        _parameterCollection?.ClearBindings();
                        ((IDisposable)localReader).Dispose();
                    }
                    if (ConnectionState.Closed != _cmdState)
                    {
                        _cmdState = ConnectionState.Closed;
                    }
                }
            }
            return localReader!;
        }

        public override object? ExecuteScalar()
        {
            object? value = null;
            using (OdbcDataReader reader = ExecuteReaderObject(0, ADP.ExecuteScalar, false))
            {
                if (reader.Read() && (0 < reader.FieldCount))
                {
                    value = reader.GetValue(0);
                }
                reader.Close();
            }
            return value;
        }

        internal string GetDiagSqlState()
        {
            return _cmdWrapper!.GetDiagSqlState();
        }

        private void PropertyChanging()
        {
            _isPrepared = false;
        }

        // Prepare
        //
        // if the CommandType property is set to TableDirect Prepare does nothing.
        // if the CommandType property is set to StoredProcedure Prepare should succeed but result
        // in a no-op
        //
        // throw InvalidOperationException
        // if the connection is not set
        // if the connection is not open
        //
        public override void Prepare()
        {
            ODBC32.SQLRETURN retcode;

            ValidateOpenConnection(ADP.Prepare);

            if (0 != (ConnectionState.Fetching & _connection!.InternalState))
            {
                throw ADP.OpenReaderExists();
            }

            if (CommandType == CommandType.TableDirect)
            {
                return; // do nothing
            }

            DisposeDeadDataReader();
            GetStatementHandle();

            OdbcStatementHandle stmt = _cmdWrapper!.StatementHandle!;

            retcode = stmt.Prepare(CommandText);


            if (ODBC32.SQLRETURN.SUCCESS != retcode)
            {
                _connection.HandleError(stmt, retcode);
            }
            _isPrepared = true;
        }



        private void TrySetStatementAttribute(OdbcStatementHandle stmt, ODBC32.SQL_ATTR stmtAttribute, IntPtr value)
        {
            ODBC32.SQLRETURN retcode = stmt.SetStatementAttribute(
                stmtAttribute,
                value,
                ODBC32.SQL_IS.UINTEGER);

            if (retcode == ODBC32.SQLRETURN.ERROR)
            {
                string sqlState;
                stmt.GetDiagnosticField(out sqlState);

                if ((sqlState == "HYC00") || (sqlState == "HY092"))
                {
                    Connection!.FlagUnsupportedStmtAttr(stmtAttribute);
                }
                else
                {
                    // now what? Should we throw?
                }
            }
        }

        private void ValidateOpenConnection(string methodName)
        {
            // see if we have a connection
            OdbcConnection? connection = Connection;

            if (null == connection)
            {
                throw ADP.ConnectionRequired(methodName);
            }

            // must have an open and available connection
            ConnectionState state = connection.State;

            if (ConnectionState.Open != state)
            {
                throw ADP.OpenConnectionRequired(methodName, state);
            }
        }

        private void ValidateConnectionAndTransaction(string method)
        {
            if (null == _connection)
            {
                throw ADP.ConnectionRequired(method);
            }
            _transaction = _connection.SetStateExecuting(method, Transaction);
            _cmdState = ConnectionState.Executing;
        }
    }
    internal sealed class CMDWrapper
    {
        private OdbcStatementHandle? _stmt;                  // hStmt
        private OdbcStatementHandle? _keyinfostmt;           // hStmt for keyinfo

        internal OdbcDescriptorHandle? _hdesc;              // hDesc

        internal CNativeBuffer? _nativeParameterBuffer;      // Native memory for internal memory management
        // (Performance optimization)

        internal CNativeBuffer? _dataReaderBuf;         // Reusable DataReader buffer

        private readonly OdbcConnection _connection;        // Connection
        private bool _canceling;             // true if the command is canceling
        internal bool _hasBoundColumns;
        internal bool _ssKeyInfoModeOn;       // tells us if the SqlServer specific options are on
        internal bool _ssKeyInfoModeOff;      // a tri-state value would be much better ...

        internal CMDWrapper(OdbcConnection connection)
        {
            _connection = connection;
        }

        internal bool Canceling
        {
            get
            {
                return _canceling;
            }
            set
            {
                _canceling = value;
            }
        }

        internal OdbcConnection Connection
        {
            get
            {
                return _connection;
            }
        }

        internal bool HasBoundColumns
        {
            //            get {
            //                return _hasBoundColumns;
            //            }
            set
            {
                _hasBoundColumns = value;
            }
        }

        internal OdbcStatementHandle? StatementHandle
        {
            get { return _stmt; }
        }

        internal OdbcStatementHandle? KeyInfoStatement
        {
            get
            {
                return _keyinfostmt;
            }
        }

        internal void CreateKeyInfoStatementHandle()
        {
            DisposeKeyInfoStatementHandle();
            _keyinfostmt = _connection.CreateStatementHandle();
        }

        internal void CreateStatementHandle()
        {
            DisposeStatementHandle();
            _stmt = _connection.CreateStatementHandle();
        }

        internal void Dispose()
        {
            if (null != _dataReaderBuf)
            {
                _dataReaderBuf.Dispose();
                _dataReaderBuf = null;
            }
            DisposeStatementHandle();

            CNativeBuffer? buffer = _nativeParameterBuffer;
            _nativeParameterBuffer = null;
            buffer?.Dispose();
            _ssKeyInfoModeOn = false;
            _ssKeyInfoModeOff = false;
        }

        private void DisposeDescriptorHandle()
        {
            OdbcDescriptorHandle? handle = _hdesc;
            if (null != handle)
            {
                _hdesc = null;
                handle.Dispose();
            }
        }
        internal void DisposeStatementHandle()
        {
            DisposeKeyInfoStatementHandle();
            DisposeDescriptorHandle();

            OdbcStatementHandle? handle = _stmt;
            if (null != handle)
            {
                _stmt = null;
                handle.Dispose();
            }
        }

        internal void DisposeKeyInfoStatementHandle()
        {
            OdbcStatementHandle? handle = _keyinfostmt;
            if (null != handle)
            {
                _keyinfostmt = null;
                handle.Dispose();
            }
        }

        internal void FreeStatementHandle(ODBC32.STMT stmt)
        {
            DisposeDescriptorHandle();

            OdbcStatementHandle? handle = _stmt;
            if (null != handle)
            {
                try
                {
                    ODBC32.SQLRETURN retcode;
                    retcode = handle.FreeStatement(stmt);
                    StatementErrorHandler(retcode);
                }
                catch (Exception e)
                {
                    //
                    if (ADP.IsCatchableExceptionType(e))
                    {
                        _stmt = null;
                        handle.Dispose();
                    }

                    throw;
                }
            }
        }

        internal void FreeKeyInfoStatementHandle(ODBC32.STMT stmt)
        {
            OdbcStatementHandle? handle = _keyinfostmt;
            if (null != handle)
            {
                try
                {
                    handle.FreeStatement(stmt);
                }
                catch (Exception e)
                {
                    //
                    if (ADP.IsCatchableExceptionType(e))
                    {
                        _keyinfostmt = null;
                        handle.Dispose();
                    }

                    throw;
                }
            }
        }

        // Get the Descriptor Handle for the current statement
        //
        internal OdbcDescriptorHandle GetDescriptorHandle(ODBC32.SQL_ATTR attribute)
        {
            OdbcDescriptorHandle? hdesc = _hdesc;
            if (null == hdesc)
            {
                _hdesc = hdesc = new OdbcDescriptorHandle(_stmt!, attribute);
            }
            return hdesc;
        }

        internal string GetDiagSqlState()
        {
            string sqlstate;
            _stmt!.GetDiagnosticField(out sqlstate);
            return sqlstate;
        }

        internal void StatementErrorHandler(ODBC32.SQLRETURN retcode)
        {
            switch (retcode)
            {
                case ODBC32.SQLRETURN.SUCCESS:
                case ODBC32.SQLRETURN.SUCCESS_WITH_INFO:
                    _connection.HandleErrorNoThrow(_stmt!, retcode);
                    break;
                default:
                    throw _connection.HandleErrorNoThrow(_stmt!, retcode)!;
            }
        }

        internal void UnbindStmtColumns()
        {
            if (_hasBoundColumns)
            {
                FreeStatementHandle(ODBC32.STMT.UNBIND);
                _hasBoundColumns = false;
            }
        }
    }
}
