// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Data.Common;
using System.Diagnostics;                   // Debug services
using System.Runtime.InteropServices;
using System.Text;

namespace System.Data.Odbc
{
    internal sealed class CNativeBuffer : System.Data.ProviderBase.DbBuffer
    {
        internal CNativeBuffer(int initialSize) : base(initialSize)
        {
        }

        internal short ShortLength
        {
            get
            {
                return checked((short)Length);
            }
        }

        internal object MarshalToManaged(int offset, ODBC32.SQL_C sqlctype, int cb)
        {
            object value;
            switch (sqlctype)
            {
                case ODBC32.SQL_C.WCHAR:
                    //Note: We always bind as unicode
                    if (cb == ODBC32.SQL_NTS)
                    {
                        value = PtrToStringUni(offset);
                        break;
                    }
                    Debug.Assert((cb > 0), "Character count negative ");
                    Debug.Assert((Length >= cb), "Native buffer too small ");
                    cb = Math.Min(cb / 2, (Length - 2) / 2);
                    value = PtrToStringUni(offset, cb);
                    break;

                case ODBC32.SQL_C.CHAR:
                case ODBC32.SQL_C.BINARY:
                    Debug.Assert((cb > 0), "Character count negative ");
                    Debug.Assert((Length >= cb), "Native buffer too small ");
                    cb = Math.Min(cb, Length);
                    value = ReadBytes(offset, cb);
                    break;

                case ODBC32.SQL_C.SSHORT:
                    value = ReadInt16(offset);
                    break;

                case ODBC32.SQL_C.SLONG:
                    value = ReadInt32(offset);
                    break;

                case ODBC32.SQL_C.SBIGINT:
                    value = ReadInt64(offset);
                    break;

                case ODBC32.SQL_C.BIT:
                    byte b = ReadByte(offset);
                    value = (b != 0x00);
                    break;

                case ODBC32.SQL_C.REAL:
                    value = ReadSingle(offset);
                    break;

                case ODBC32.SQL_C.DOUBLE:
                    value = ReadDouble(offset);
                    break;

                case ODBC32.SQL_C.UTINYINT:
                    value = ReadByte(offset);
                    break;

                case ODBC32.SQL_C.GUID:
                    value = ReadGuid(offset);
                    break;

                case ODBC32.SQL_C.TYPE_TIMESTAMP:
                    //So we are mapping this ourselves.
                    //typedef struct tagTIMESTAMP_STRUCT
                    //{
                    //      SQLSMALLINT    year;
                    //      SQLUSMALLINT   month;
                    //      SQLUSMALLINT   day;
                    //      SQLUSMALLINT   hour;
                    //      SQLUSMALLINT   minute;
                    //      SQLUSMALLINT   second;
                    //      SQLUINTEGER    fraction;    (billoniths of a second)
                    //}

                    value = ReadDateTime(offset);
                    break;

                // Note: System does not provide a date-only type
                case ODBC32.SQL_C.TYPE_DATE:
                    //  typedef struct tagDATE_STRUCT
                    //  {
                    //      SQLSMALLINT    year;
                    //      SQLUSMALLINT   month;
                    //      SQLUSMALLINT   day;
                    //  } DATE_STRUCT;

                    value = ReadDate(offset);
                    break;

                // Note: System does not provide a date-only type
                case ODBC32.SQL_C.TYPE_TIME:
                    //  typedef struct tagTIME_STRUCT
                    //  {
                    //      SQLUSMALLINT   hour;
                    //      SQLUSMALLINT   minute;
                    //      SQLUSMALLINT   second;
                    //  } TIME_STRUCT;

                    value = ReadTime(offset);
                    break;

                case ODBC32.SQL_C.NUMERIC:
                    //Note: Unfortunatly the ODBC NUMERIC structure and the URT DECIMAL structure do not
                    //align, so we can't so do the typical "PtrToStructure" call (below) like other types
                    //We actually have to go through the pain of pulling our raw bytes and building the decimal
                    //  Marshal.PtrToStructure(buffer, typeof(decimal));

                    //So we are mapping this ourselves
                    //typedef struct tagSQL_NUMERIC_STRUCT
                    //{
                    //  SQLCHAR     precision;
                    //  SQLSCHAR    scale;
                    //  SQLCHAR     sign;   /* 1 if positive, 0 if negative */
                    //  SQLCHAR     val[SQL_MAX_NUMERIC_LEN];
                    //} SQL_NUMERIC_STRUCT;

                    value = ReadNumeric(offset);
                    break;

                default:
                    Debug.Fail("UnknownSQLCType");
                    value = null;
                    break;
            };
            return value;
        }

        // if sizeorprecision applies only for wchar and numeric values
        // for wchar the a value of null means take the value's size
        //
        internal void MarshalToNative(int offset, object value, ODBC32.SQL_C sqlctype, int sizeorprecision, int valueOffset)
        {
            switch (sqlctype)
            {
                case ODBC32.SQL_C.WCHAR:
                    {
                        //Note: We always bind as unicode
                        //Note: StructureToPtr fails indicating string it a non-blittable type
                        //and there is no MarshalStringTo* that moves to an existing buffer,
                        //they all alloc and return a new one, not at all what we want...

                        //So we have to copy the raw bytes of the string ourself?!

                        char[] rgChars;
                        int length;
                        Debug.Assert(value is string || value is char[], "Only string or char[] can be marshaled to WCHAR");

                        if (value is string)
                        {
                            length = Math.Max(0, ((string)value).Length - valueOffset);

                            if ((sizeorprecision > 0) && (sizeorprecision < length))
                            {
                                length = sizeorprecision;
                            }

                            rgChars = ((string)value).ToCharArray(valueOffset, length);
                            Debug.Assert(rgChars.Length < (base.Length - valueOffset), "attempting to extend parameter buffer!");

                            WriteCharArray(offset, rgChars, 0, rgChars.Length);
                            WriteInt16(offset + (rgChars.Length * 2), 0); // Add the null terminator
                        }
                        else
                        {
                            length = Math.Max(0, ((char[])value).Length - valueOffset);
                            if ((sizeorprecision > 0) && (sizeorprecision < length))
                            {
                                length = sizeorprecision;
                            }
                            rgChars = (char[])value;
                            Debug.Assert(rgChars.Length < (base.Length - valueOffset), "attempting to extend parameter buffer!");

                            WriteCharArray(offset, rgChars, valueOffset, length);
                            WriteInt16(offset + (rgChars.Length * 2), 0); // Add the null terminator
                        }
                        break;
                    }

                case ODBC32.SQL_C.BINARY:
                case ODBC32.SQL_C.CHAR:
                    {
                        byte[] rgBytes = (byte[])value;
                        int length = rgBytes.Length;

                        Debug.Assert((valueOffset <= length), "Offset out of Range");

                        // reduce length by the valueOffset
                        //
                        length -= valueOffset;

                        // reduce length to be no more than size (if size is given)
                        //
                        if ((sizeorprecision > 0) && (sizeorprecision < length))
                        {
                            length = sizeorprecision;
                        }

                        //AdjustSize(rgBytes.Length+1);
                        //buffer = DangerousAllocateAndGetHandle();      // Realloc may have changed buffer address
                        Debug.Assert(length < (base.Length - valueOffset), "attempting to extend parameter buffer!");

                        WriteBytes(offset, rgBytes, valueOffset, length);
                        break;
                    }

                case ODBC32.SQL_C.UTINYINT:
                    WriteByte(offset, (byte)value);
                    break;

                case ODBC32.SQL_C.SSHORT:   //Int16
                    WriteInt16(offset, (short)value);
                    break;

                case ODBC32.SQL_C.SLONG:    //Int32
                    WriteInt32(offset, (int)value);
                    break;

                case ODBC32.SQL_C.REAL:     //float
                    WriteSingle(offset, (float)value);
                    break;

                case ODBC32.SQL_C.SBIGINT:  //Int64
                    WriteInt64(offset, (long)value);
                    break;

                case ODBC32.SQL_C.DOUBLE:   //Double
                    WriteDouble(offset, (double)value);
                    break;

                case ODBC32.SQL_C.GUID:     //Guid
                    WriteGuid(offset, (Guid)value);
                    break;

                case ODBC32.SQL_C.BIT:
                    WriteByte(offset, (byte)(((bool)value) ? 1 : 0));
                    break;

                case ODBC32.SQL_C.TYPE_TIMESTAMP:
                    {
                        //typedef struct tagTIMESTAMP_STRUCT
                        //{
                        //      SQLSMALLINT    year;
                        //      SQLUSMALLINT   month;
                        //      SQLUSMALLINT   day;
                        //      SQLUSMALLINT   hour;
                        //      SQLUSMALLINT   minute;
                        //      SQLUSMALLINT   second;
                        //      SQLUINTEGER    fraction;    (billoniths of a second)
                        //}

                        //We have to map this ourselves, due to the different structures between
                        //ODBC TIMESTAMP and URT DateTime, (ie: can't use StructureToPtr)

                        WriteODBCDateTime(offset, (DateTime)value);
                        break;
                    }

                // Note: System does not provide a date-only type
                case ODBC32.SQL_C.TYPE_DATE:
                    {
                        //  typedef struct tagDATE_STRUCT
                        //  {
                        //      SQLSMALLINT    year;
                        //      SQLUSMALLINT   month;
                        //      SQLUSMALLINT   day;
                        //  } DATE_STRUCT;

                        WriteDate(offset, (DateTime)value);
                        break;
                    }

                // Note: System does not provide a date-only type
                case ODBC32.SQL_C.TYPE_TIME:
                    {
                        //  typedef struct tagTIME_STRUCT
                        //  {
                        //      SQLUSMALLINT   hour;
                        //      SQLUSMALLINT   minute;
                        //      SQLUSMALLINT   second;
                        //  } TIME_STRUCT;

                        WriteTime(offset, (TimeSpan)value);
                        break;
                    }

                case ODBC32.SQL_C.NUMERIC:
                    {
                        WriteNumeric(offset, (decimal)value, checked((byte)sizeorprecision));
                        break;
                    }

                default:
                    Debug.Fail("UnknownSQLCType");
                    break;
            }
        }

        internal HandleRef PtrOffset(int offset, int length)
        {
            //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            // NOTE: You must have called DangerousAddRef before calling this
            //       method, or you run the risk of allowing Handle Recycling
            //       to occur!
            //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
            Validate(offset, length);

            IntPtr ptr = ADP.IntPtrOffset(DangerousGetHandle(), offset);
            return new HandleRef(this, ptr);
        }

        internal void WriteODBCDateTime(int offset, DateTime value)
        {
            short[] buffer = new short[6] {
                unchecked((short)value.Year),
                unchecked((short)value.Month),
                unchecked((short)value.Day),
                unchecked((short)value.Hour),
                unchecked((short)value.Minute),
                unchecked((short)value.Second),
            };
            WriteInt16Array(offset, buffer, 0, 6);
            WriteInt32(offset + 12, value.Millisecond * 1000000); //fraction
        }
    }


    internal sealed class CStringTokenizer
    {
        private readonly StringBuilder _token;
        private readonly string _sqlstatement;
        private readonly char _quote;         // typically the semicolon '"'
        private readonly char _escape;        // typically the same char as the quote
        private int _len = 0;
        private int _idx = 0;

        internal CStringTokenizer(string text, char quote, char escape)
        {
            _token = new StringBuilder();
            _quote = quote;
            _escape = escape;
            _sqlstatement = text;
            if (text != null)
            {
                int iNul = text.IndexOf('\0');
                _len = (0 > iNul) ? text.Length : iNul;
            }
            else
            {
                _len = 0;
            }
        }

        internal int CurrentPosition
        {
            get { return _idx; }
        }

        // Returns the next token in the statement, advancing the current index to
        //  the start of the token
        internal string NextToken()
        {
            if (_token.Length != 0)
            {                   // if we've read a token before
                _idx += _token.Length;                  // proceed the internal marker (_idx) behind the token
                _token.Remove(0, _token.Length);        // and start over with a fresh token
            }

            while ((_idx < _len) && char.IsWhiteSpace(_sqlstatement[_idx]))
            {
                // skip whitespace
                _idx++;
            }

            if (_idx == _len)
            {
                // return if string is empty
                return string.Empty;
            }

            int curidx = _idx;                          // start with internal index at current index
            bool endtoken = false;                      //

            // process characters until we reache the end of the token or the end of the string
            //
            while (!endtoken && curidx < _len)
            {
                if (IsValidNameChar(_sqlstatement[curidx]))
                {
                    while ((curidx < _len) && IsValidNameChar(_sqlstatement[curidx]))
                    {
                        _token.Append(_sqlstatement[curidx]);
                        curidx++;
                    }
                }
                else
                {
                    char currentchar = _sqlstatement[curidx];
                    if (currentchar == '[')
                    {
                        curidx = GetTokenFromBracket(curidx);
                    }
                    else if (' ' != _quote && currentchar == _quote)
                    { // if the ODBC driver does not support quoted identifiers it returns a single blank character
                        curidx = GetTokenFromQuote(curidx);
                    }
                    else
                    {
                        // Some other marker like , ; ( or )
                        // could also be * or ?
                        if (!char.IsWhiteSpace(currentchar))
                        {
                            switch (currentchar)
                            {
                                case ',':
                                    // commas are not part of a token so we'll only append them if they are at the beginning
                                    if (curidx == _idx)
                                        _token.Append(currentchar);
                                    break;
                                default:
                                    _token.Append(currentchar);
                                    break;
                            }
                        }
                        endtoken = true;
                        break;
                    }
                }
            }

            return (_token.Length > 0) ? _token.ToString() : string.Empty;
        }

        private int GetTokenFromBracket(int curidx)
        {
            Debug.Assert((_sqlstatement[curidx] == '['), "GetTokenFromQuote: character at starting position must be same as quotechar");
            while (curidx < _len)
            {
                _token.Append(_sqlstatement[curidx]);
                curidx++;
                if (_sqlstatement[curidx - 1] == ']')
                    break;
            }
            return curidx;
        }

        // attempts to complete an encapsulated token (e.g. "scott")
        // double quotes are valid part of the token (e.g. "foo""bar")
        //        
        private int GetTokenFromQuote(int curidx)
        {
            Debug.Assert(_quote != ' ', "ODBC driver doesn't support quoted identifiers -- GetTokenFromQuote should not be used in this case");
            Debug.Assert((_sqlstatement[curidx] == _quote), "GetTokenFromQuote: character at starting position must be same as quotechar");

            int localidx = curidx;                                  // start with local index at current index
            while (localidx < _len)
            {                               // run to the end of the statement
                _token.Append(_sqlstatement[localidx]);             // append current character to token
                if (_sqlstatement[localidx] == _quote)
                {
                    if (localidx > curidx)
                    {                         // don't care for the first char
                        if (_sqlstatement[localidx - 1] != _escape)
                        { // if it's not escape we look at the following char
                            if (localidx + 1 < _len)
                            {                // do not overrun the end of the string
                                if (_sqlstatement[localidx + 1] != _quote)
                                {
                                    return localidx + 1;              // We've reached the end of the quoted text
                                }
                            }
                        }
                    }
                }
                localidx++;
            }
            return localidx;
        }

        private bool IsValidNameChar(char ch)
        {
            return (char.IsLetterOrDigit(ch) ||
                    (ch == '_') || (ch == '-') || (ch == '.') ||
                    (ch == '$') || (ch == '#') || (ch == '@') ||
                    (ch == '~') || (ch == '`') || (ch == '%') ||
                    (ch == '^') || (ch == '&') || (ch == '|'));
        }

        // Searches for the token given, starting from the current position
        // If found, positions the currentindex at the
        // beginning of the token if found.
        internal int FindTokenIndex(string tokenString)
        {
            string nextToken;
            while (true)
            {
                nextToken = NextToken();
                if ((_idx == _len) || string.IsNullOrEmpty(nextToken))
                { // fxcop
                    break;
                }
                if (string.Equals(tokenString, nextToken, StringComparison.OrdinalIgnoreCase))
                {
                    return _idx;
                }
            }
            return -1;
        }

        // Skips the whitespace found in the beginning of the string.
        internal bool StartsWith(string tokenString)
        {
            int tempidx = 0;
            while ((tempidx < _len) && char.IsWhiteSpace(_sqlstatement[tempidx]))
            {
                tempidx++;
            }
            if ((_len - tempidx) < tokenString.Length)
            {
                return false;
            }

            if (0 == string.Compare(_sqlstatement, tempidx, tokenString, 0, tokenString.Length, StringComparison.OrdinalIgnoreCase))
            {
                // Reset current position and token
                _idx = 0;
                NextToken();
                return true;
            }
            return false;
        }
    }
}

