using System;
using System.Data.Common;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace System.Data.SqlClient
{
    sealed internal class SqlSequentialTextReader : System.IO.TextReader
    {
        private SqlDataReader _reader;  // The SqlDataReader that we are reading data from
        private int _columnIndex;       // The index of out column in the table
        private Encoding _encoding;     // Encoding for this character stream
        private Decoder _decoder;       // Decoder based on the encoding (NOTE: Decoders are stateful as they are designed to process streams of data)
        private byte[] _leftOverBytes;  // Bytes leftover from the last Read() operation - this can be null if there were no bytes leftover (Possible optimization: re-use the same array?)
        private int _peekedChar;        // The last character that we peeked at (or -1 if we haven't peeked at anything)
        private Task _currentTask;      // The current async task
        private CancellationTokenSource _disposalTokenSource;    // Used to indicate that a cancellation is requested due to disposal

        internal SqlSequentialTextReader(SqlDataReader reader, int columnIndex, Encoding encoding)
        {
            Debug.Assert(reader != null, "Null reader when creating sequential textreader");
            Debug.Assert(columnIndex >= 0, "Invalid column index when creating sequential textreader");
            Debug.Assert(encoding != null, "Null encoding when creating sequential textreader");

            _reader = reader;
            _columnIndex = columnIndex;
            _encoding = encoding;
            _decoder = encoding.GetDecoder();
            _leftOverBytes = null;
            _peekedChar = -1;
            _currentTask = null;
            _disposalTokenSource = new CancellationTokenSource();
        }

        internal int ColumnIndex
        {
            get { return _columnIndex; }
        }

        public override int Peek()
        {
            if (_currentTask != null)
            {
                throw ADP.AsyncOperationPending();
            }
            if (IsClosed)
            {
                throw ADP.ObjectDisposed(this);
            }

            if (!HasPeekedChar)
            {
                _peekedChar = Read();
            }

            Debug.Assert(_peekedChar == -1 || ((_peekedChar >= char.MinValue) && (_peekedChar <= char.MaxValue)), string.Format("Bad peeked character: {0}", _peekedChar));
            return _peekedChar;
        }

        public override int Read()
        {
            if (_currentTask != null)
            {
                throw ADP.AsyncOperationPending();
            }
            if (IsClosed)
            {
                throw ADP.ObjectDisposed(this);
            }

            int readChar = -1;

            // If there is already a peeked char, then return it
            if (HasPeekedChar)
            {
                readChar = _peekedChar;
                _peekedChar = -1;
            }
            // If there is data available try to read a char
            else
            {
                char[] tempBuffer = new char[1];
                int charsRead = InternalRead(tempBuffer, 0, 1);
                if (charsRead == 1)
                {
                    readChar = tempBuffer[0];
                }
            }

            Debug.Assert(readChar == -1 || ((readChar >= char.MinValue) && (readChar <= char.MaxValue)), string.Format("Bad read character: {0}", readChar));
            return readChar;
        }

        public override int Read(char[] buffer, int index, int count)
        {
            ValidateReadParameters(buffer, index, count);

            if (IsClosed)
            {
                throw ADP.ObjectDisposed(this);
            }
            if (_currentTask != null)
            {
                throw ADP.AsyncOperationPending();
            }

            int charsRead = 0;
            int charsNeeded = count;
            // Load in peeked char
            if ((charsNeeded > 0) && (HasPeekedChar))
            {
                Debug.Assert((_peekedChar >= char.MinValue) && (_peekedChar <= char.MaxValue), string.Format("Bad peeked character: {0}", _peekedChar));
                buffer[index + charsRead] = (char)_peekedChar;
                charsRead++;
                charsNeeded--;
                _peekedChar = -1;
            }

            // If we need more data and there is data avaiable, read
            charsRead += InternalRead(buffer, index + charsRead, charsNeeded);

            return charsRead;
        }

        public override Task<int> ReadAsync(char[] buffer, int index, int count)
        {
            ValidateReadParameters(buffer, index, count);
            TaskCompletionSource<int> completion = new TaskCompletionSource<int>();

            if (IsClosed)
            {
                completion.SetException(ADP.ExceptionWithStackTrace(ADP.ObjectDisposed(this)));
            }
            else
            {
                try
                {
                    Task original = Interlocked.CompareExchange<Task>(ref _currentTask, completion.Task, null);
                    if (original != null)
                    {
                        completion.SetException(ADP.ExceptionWithStackTrace(ADP.AsyncOperationPending()));
                    }
                    else
                    {
                        bool completedSynchronously = true;
                        int charsRead = 0;
                        int adjustedIndex = index;
                        int charsNeeded = count;

                        // Load in peeked char
                        if ((HasPeekedChar) && (charsNeeded > 0))
                        {
                            // Take a copy of _peekedChar in case it is cleared during close
                            int peekedChar = _peekedChar;
                            if (peekedChar >= char.MinValue)
                            {
                                Debug.Assert((_peekedChar >= char.MinValue) && (_peekedChar <= char.MaxValue), string.Format("Bad peeked character: {0}", _peekedChar));
                                buffer[adjustedIndex] = (char)peekedChar;
                                adjustedIndex++;
                                charsRead++;
                                charsNeeded--;
                                _peekedChar = -1;
                            }
                        }

                        int byteBufferUsed;
                        byte[] byteBuffer = PrepareByteBuffer(charsNeeded, out byteBufferUsed);

                        // Permit a 0 byte read in order to advance the reader to the correct column
                        if ((byteBufferUsed < byteBuffer.Length) || (byteBuffer.Length == 0))
                        {
                            int bytesRead;
                            var reader = _reader;
                            if (reader != null) 
                            {
                                Task<int> getBytesTask = reader.GetBytesAsync(_columnIndex, byteBuffer, byteBufferUsed, byteBuffer.Length - byteBufferUsed, Timeout.Infinite, _disposalTokenSource.Token, out bytesRead);
                                if (getBytesTask == null) {
                                    byteBufferUsed += bytesRead;
                                }
                                else {
                                    // We need more data - setup the callback, and mark this as not completed [....]
                                    completedSynchronously = false;
                                    getBytesTask.ContinueWith((t) =>
                                    {
                                        _currentTask = null;
                                        // If we completed but the textreader is closed, then report cancellation
                                        if ((t.Status == TaskStatus.RanToCompletion) && (!IsClosed))
                                        {
                                            try
                                            {
                                                int bytesReadFromStream = t.Result;
                                                byteBufferUsed += bytesReadFromStream;
                                                if (byteBufferUsed > 0)
                                                {
                                                    charsRead += DecodeBytesToChars(byteBuffer, byteBufferUsed, buffer, adjustedIndex, charsNeeded);
                                                }
                                                completion.SetResult(charsRead);
                                            }
                                            catch (Exception ex)
                                            {
                                                completion.SetException(ex);
                                            }
                                        }
                                        else if (IsClosed)
                                        {
                                            completion.SetException(ADP.ExceptionWithStackTrace(ADP.ObjectDisposed(this)));
                                        }
                                        else if (t.Status == TaskStatus.Faulted)
                                        {
                                            if (t.Exception.InnerException is SqlException)
                                            {
                                                // ReadAsync can't throw a SqlException, so wrap it in an IOException
                                                completion.SetException(ADP.ExceptionWithStackTrace(ADP.ErrorReadingFromStream(t.Exception.InnerException)));
                                            }
                                            else
                                            {
                                                completion.SetException(t.Exception.InnerException);
                                            }
                                        }
                                        else
                                        {
                                            completion.SetCanceled();
                                        }
                                    }, TaskScheduler.Default);
                                }
                            

                                if ((completedSynchronously) && (byteBufferUsed > 0))
                                {
                                    // No more data needed, decode what we have
                                    charsRead += DecodeBytesToChars(byteBuffer, byteBufferUsed, buffer, adjustedIndex, charsNeeded);
                                }
                            }
                            else
                            {
                                // Reader is null, close must of happened in the middle of this read
                                completion.SetException(ADP.ExceptionWithStackTrace(ADP.ObjectDisposed(this)));
                            }
                        }
                    

                        if (completedSynchronously)
                        {
                            _currentTask = null;
                            if (IsClosed)
                            {
                                completion.SetCanceled();
                            }
                            else
                            {
                                completion.SetResult(charsRead);
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    // In case of any errors, ensure that the completion is completed and the task is set back to null if we switched it
                    completion.TrySetException(ex);
                    Interlocked.CompareExchange(ref _currentTask, null, completion.Task);
                    throw;
                }
            }

            return completion.Task;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Set the textreader as closed
                SetClosed();
            }

            base.Dispose(disposing);
        }

        /// <summary>
        /// Forces the TextReader to act as if it was closed
        /// This does not actually close the stream, read off the rest of the data or dispose this
        /// </summary>
        internal void SetClosed()
        {
            _disposalTokenSource.Cancel();
            _reader = null;
            _peekedChar = -1;

            // Wait for pending task
            var currentTask = _currentTask;
            if (currentTask != null) 
            {
                ((IAsyncResult)currentTask).AsyncWaitHandle.WaitOne();
            }
        }

        /// <summary>
        /// Performs the actual reading and converting
        /// NOTE: This assumes that buffer, index and count are all valid, we're not closed (!IsClosed) and that there is data left (IsDataLeft())
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="index"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        private int InternalRead(char[] buffer, int index, int count)
        {
            Debug.Assert(buffer != null, "Null output buffer");
            Debug.Assert((index >= 0) && (count >= 0) && (index + count <= buffer.Length), string.Format("Bad count: {0} or index: {1}", count, index));
            Debug.Assert(!IsClosed, "Can't read while textreader is closed");

            try
            {
                int byteBufferUsed;
                byte[] byteBuffer = PrepareByteBuffer(count, out byteBufferUsed);
                byteBufferUsed += _reader.GetBytesInternalSequential(_columnIndex, byteBuffer, byteBufferUsed, byteBuffer.Length - byteBufferUsed);

                if (byteBufferUsed > 0) {
                    return DecodeBytesToChars(byteBuffer, byteBufferUsed, buffer, index, count);
                }
                else {
                    // Nothing to read, or nothing read
                    return 0;
                }
            }
            catch (SqlException ex)
            {
                // Read can't throw a SqlException - so wrap it in an IOException
                throw ADP.ErrorReadingFromStream(ex);
            }
        }

        /// <summary>
        /// Creates a byte array large enough to store all bytes for the characters in the current encoding, then fills it with any leftover bytes
        /// </summary>
        /// <param name="numberOfChars">Number of characters that are to be read</param>
        /// <param name="byteBufferUsed">Number of bytes pre-filled by the leftover bytes</param>
        /// <returns>A byte array of the correct size, pre-filled with leftover bytes</returns>
        private byte[] PrepareByteBuffer(int numberOfChars, out int byteBufferUsed)
        {
            Debug.Assert(numberOfChars >= 0, "Can't prepare a byte buffer for negative characters");

            byte[] byteBuffer;

            if (numberOfChars == 0)
            {
                byteBuffer = new byte[0];
                byteBufferUsed = 0;
            }
            else 
            {
                int byteBufferSize = _encoding.GetMaxByteCount(numberOfChars);

                if (_leftOverBytes != null)
                {
                    // If we have more leftover bytes than we need for this conversion, then just re-use the leftover buffer
                    if (_leftOverBytes.Length > byteBufferSize)
                    {
                        byteBuffer = _leftOverBytes;
                        byteBufferUsed = byteBuffer.Length;
                    }
                    else
                    {
                        // Otherwise, copy over the leftover buffer
                        byteBuffer = new byte[byteBufferSize];
                        Array.Copy(_leftOverBytes, byteBuffer, _leftOverBytes.Length);
                        byteBufferUsed = _leftOverBytes.Length;
                    }
                }
                else
                {
                    byteBuffer = new byte[byteBufferSize];
                    byteBufferUsed = 0;
                }
            }

            return byteBuffer;
        }

        /// <summary>
        /// Decodes the given bytes into characters, and stores the leftover bytes for later use
        /// </summary>
        /// <param name="inBuffer">Buffer of bytes to decode</param>
        /// <param name="inBufferCount">Number of bytes to decode from the inBuffer</param>
        /// <param name="outBuffer">Buffer to write the characters to</param>
        /// <param name="outBufferOffset">Offset to start writing to outBuffer at</param>
        /// <param name="outBufferCount">Maximum number of characters to decode</param>
        /// <returns>The actual number of characters decoded</returns>
        private int DecodeBytesToChars(byte[] inBuffer, int inBufferCount, char[] outBuffer, int outBufferOffset, int outBufferCount)
        {
            Debug.Assert(inBuffer != null, "Null input buffer");
            Debug.Assert((inBufferCount > 0) && (inBufferCount <= inBuffer.Length), string.Format("Bad inBufferCount: {0}", inBufferCount));
            Debug.Assert(outBuffer != null, "Null output buffer");
            Debug.Assert((outBufferOffset >= 0) && (outBufferCount > 0) && (outBufferOffset + outBufferCount <= outBuffer.Length), string.Format("Bad outBufferCount: {0} or outBufferOffset: {1}", outBufferCount, outBufferOffset));

            int charsRead;
            int bytesUsed;
            bool completed;
            _decoder.Convert(inBuffer, 0, inBufferCount, outBuffer, outBufferOffset, outBufferCount, false, out bytesUsed, out charsRead, out completed);
            
            // completed may be false and there is no spare bytes if the Decoder has stored bytes to use later
            if ((!completed) && (bytesUsed < inBufferCount))
            {
                _leftOverBytes = new byte[inBufferCount - bytesUsed];
                Array.Copy(inBuffer, bytesUsed, _leftOverBytes, 0, _leftOverBytes.Length);
            }
            else
            {
                // If Convert() sets completed to true, then it must have used all of the bytes we gave it
                Debug.Assert(bytesUsed >= inBufferCount, "Converted completed, but not all bytes were used");
                _leftOverBytes = null;
            }

            Debug.Assert(((_reader == null) || (_reader.ColumnDataBytesRemaining() > 0) || (!completed) || (_leftOverBytes == null)), "Stream has run out of data and the decoder finished, but there are leftover bytes");
            Debug.Assert(charsRead > 0, "Converted no chars. Bad encoding?");

            return charsRead;
        }

        /// <summary>
        /// True if this TextReader is supposed to be closed
        /// </summary>
        private bool IsClosed
        {
            get { return (_reader == null); } 
        }

        /// <summary>
        /// True if there is data left to read
        /// </summary>
        /// <returns></returns>
        private bool IsDataLeft
        {
            get { return ((_leftOverBytes != null) || (_reader.ColumnDataBytesRemaining() > 0)); }
        }

        /// <summary>
        /// True if there is a peeked character available
        /// </summary>
        private bool HasPeekedChar
        {
            get { return (_peekedChar >= char.MinValue); }
        }

        /// <summary>
        /// Checks the the parameters passed into a Read() method are valid
        /// </summary>
        /// <param name="buffer"></param>
        /// <param name="index"></param>
        /// <param name="count"></param>
        internal static void ValidateReadParameters(char[] buffer, int index, int count) 
        {
            if (buffer == null)
            {
                throw ADP.ArgumentNull(ADP.ParameterBuffer);
            }
			if (index < 0)
            {
                throw ADP.ArgumentOutOfRange(ADP.ParameterIndex);
            }
			if (count < 0)
            {
                throw ADP.ArgumentOutOfRange(ADP.ParameterCount);
            }
            try
            {
                if (checked(index + count) > buffer.Length)
                {
                    throw ExceptionBuilder.InvalidOffsetLength();
                }
            }
            catch (OverflowException)
            {
                // If we've overflowed when adding index and count, then they never would have fit into buffer anyway
                throw ExceptionBuilder.InvalidOffsetLength();
            }
        }
    }

    sealed internal class SqlUnicodeEncoding : UnicodeEncoding
    {
        private static SqlUnicodeEncoding _singletonEncoding = new SqlUnicodeEncoding();

        private SqlUnicodeEncoding() : base(bigEndian: false, byteOrderMark: false, throwOnInvalidBytes: false)
        {}

        public override Decoder GetDecoder()
        {
            return new SqlUnicodeDecoder();
        }

        public override int GetMaxByteCount(int charCount)
        {
            // SQL Server never sends a BOM, so we can assume that its 2 bytes per char
            return charCount * 2;
        }

        public static Encoding SqlUnicodeEncodingInstance
        {
            get { return _singletonEncoding; }
        }

        sealed private class SqlUnicodeDecoder : Decoder
        {
            public override int GetCharCount(byte[] bytes, int index, int count)
            {
                // SQL Server never sends a BOM, so we can assume that its 2 bytes per char
                return count / 2;
            }

            public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
            {
                // This method is required - simply call Convert()
                int bytesUsed;
                int charsUsed;
                bool completed;
                Convert(bytes, byteIndex, byteCount, chars, charIndex, chars.Length - charIndex, true, out bytesUsed, out charsUsed, out completed);
                return charsUsed;
            }

            public override void Convert(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex, int charCount, bool flush, out int bytesUsed, out int charsUsed, out bool completed)
            {
                // Assume 2 bytes per char and no BOM
                charsUsed = Math.Min(charCount, byteCount / 2);
                bytesUsed = charsUsed * 2;
                completed = (bytesUsed == byteCount);

                // BlockCopy uses offsets\length measured in bytes, not the actual array index
                Buffer.BlockCopy(bytes, byteIndex, chars, charIndex * 2, bytesUsed);
            }
        }
    }    
}
