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

using System.IO;
using System.Diagnostics;
using System.Data.Common;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;
using System.Runtime.Serialization;
using System.Runtime.CompilerServices;

namespace System.Data.SqlTypes
{
    [XmlSchemaProvider("GetXsdType")]
    public sealed class SqlChars : INullable, IXmlSerializable, ISerializable
    {
        // --------------------------------------------------------------
        //      Data members
        // --------------------------------------------------------------

        // SqlChars has five possible states
        // 1) SqlChars is Null
        //        - m_stream must be null, m_lCuLen must be x_lNull
        // 2) SqlChars contains a valid buffer,
        //        - m_rgchBuf must not be null, and m_stream must be null
        // 3) SqlChars contains a valid pointer
        //        - m_rgchBuf could be null or not,
        //            if not null, content is garbage, should never look into it.
        //      - m_stream must be null.
        // 4) SqlChars contains a SqlStreamChars
        //      - m_stream must not be null
        //      - m_rgchBuf could be null or not. if not null, content is garbage, should never look into it.
        //        - m_lCurLen must be x_lNull.
        // 5) SqlChars contains a Lazy Materialized Blob (ie, StorageState.Delayed)
        //
        internal char[]? _rgchBuf;  // Data buffer
        private long _lCurLen; // Current data length
        internal SqlStreamChars? _stream;
        private SqlBytesCharsState _state;

        private char[]? _rgchWorkBuf;   // A 1-char work buffer.

        // The max data length that we support at this time.
        private const long x_lMaxLen = int.MaxValue;

        private const long x_lNull = -1L;

        // --------------------------------------------------------------
        //      Constructor(s)
        // --------------------------------------------------------------

        // Public default constructor used for XML serialization
        public SqlChars()
        {
            SetNull();
        }

        // Create a SqlChars with an in-memory buffer
        public SqlChars(char[]? buffer)
        {
            _rgchBuf = buffer;
            _stream = null;
            if (_rgchBuf == null)
            {
                _state = SqlBytesCharsState.Null;
                _lCurLen = x_lNull;
            }
            else
            {
                _state = SqlBytesCharsState.Buffer;
                _lCurLen = _rgchBuf.Length;
            }

            _rgchWorkBuf = null;

            AssertValid();
        }

        // Create a SqlChars from a SqlString
        public SqlChars(SqlString value) : this(value.IsNull ? null : value.Value.ToCharArray())
        {
        }

        // Create a SqlChars from a SqlStreamChars
        internal SqlChars(SqlStreamChars s)
        {
            _rgchBuf = null;
            _lCurLen = x_lNull;
            _stream = s;
            _state = (s == null) ? SqlBytesCharsState.Null : SqlBytesCharsState.Stream;

            _rgchWorkBuf = null;

            AssertValid();
        }

        // --------------------------------------------------------------
        //      Public properties
        // --------------------------------------------------------------

        // INullable
        public bool IsNull
        {
            get
            {
                return _state == SqlBytesCharsState.Null;
            }
        }

        // Property: the in-memory buffer of SqlChars
        //        Return Buffer even if SqlChars is Null.

        public char[]? Buffer
        {
            get
            {
                if (FStream())
                {
                    CopyStreamToBuffer();
                }
                return _rgchBuf;
            }
        }

        // Property: the actual length of the data
        public long Length
        {
            get
            {
                return _state switch
                {
                    SqlBytesCharsState.Null => throw new SqlNullValueException(),
                    SqlBytesCharsState.Stream => _stream!.Length,
                    _ => _lCurLen,
                };
            }
        }

        // Property: the max length of the data
        //        Return MaxLength even if SqlChars is Null.
        //        When the buffer is also null, return -1.
        //        If containing a Stream, return -1.

        public long MaxLength
        {
            get
            {
                return _state switch
                {
                    SqlBytesCharsState.Stream => -1L,
                    _ => (_rgchBuf == null) ? -1L : _rgchBuf.Length,
                };
            }
        }

        // Property: get a copy of the data in a new char[] array.

        public char[] Value
        {
            get
            {
                char[] buffer;

                switch (_state)
                {
                    case SqlBytesCharsState.Null:
                        throw new SqlNullValueException();

                    case SqlBytesCharsState.Stream:
                        if (_stream!.Length > x_lMaxLen)
                            throw new SqlTypeException(SR.SqlMisc_BufferInsufficientMessage);
                        buffer = new char[_stream.Length];
                        if (_stream.Position != 0)
                            _stream.Seek(0, SeekOrigin.Begin);
                        _stream.Read(buffer, 0, checked((int)_stream.Length));
                        break;

                    default:
                        buffer = new char[_lCurLen];
                        Array.Copy(_rgchBuf!, buffer, (int)_lCurLen);
                        break;
                }

                return buffer;
            }
        }

        // class indexer

        public char this[long offset]
        {
            get
            {
                ArgumentOutOfRangeException.ThrowIfNegative(offset);
                ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(offset, Length);

                _rgchWorkBuf ??= new char[1];

                Read(offset, _rgchWorkBuf, 0, 1);
                return _rgchWorkBuf[0];
            }
            set
            {
                _rgchWorkBuf ??= new char[1];

                _rgchWorkBuf[0] = value;
                Write(offset, _rgchWorkBuf, 0, 1);
            }
        }

        internal SqlStreamChars Stream
        {
            get
            {
                return FStream() ? _stream! : new SqlStreamChars(this);
            }
            set
            {
                _lCurLen = x_lNull;
                _stream = value;
                _state = (value == null) ? SqlBytesCharsState.Null : SqlBytesCharsState.Stream;

                AssertValid();
            }
        }

        public StorageState Storage
        {
            get
            {
                return _state switch
                {
                    SqlBytesCharsState.Null => throw new SqlNullValueException(),
                    SqlBytesCharsState.Stream => StorageState.Stream,
                    SqlBytesCharsState.Buffer => StorageState.Buffer,
                    _ => StorageState.UnmanagedBuffer,
                };
            }
        }

        // --------------------------------------------------------------
        //      Public methods
        // --------------------------------------------------------------

        public void SetNull()
        {
            _lCurLen = x_lNull;
            _stream = null;
            _state = SqlBytesCharsState.Null;

            AssertValid();
        }

        // Set the current length of the data
        // If the SqlChars is Null, setLength will make it non-Null.
        public void SetLength(long value)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(value);

            if (FStream())
            {
                _stream!.SetLength(value);
            }
            else
            {
                // If there is a buffer, even the value of SqlChars is Null,
                // still allow setting length to zero, which will make it not Null.
                // If the buffer is null, raise exception
                //
                if (null == _rgchBuf)
                    throw new SqlTypeException(SR.SqlMisc_NoBufferMessage);

                ArgumentOutOfRangeException.ThrowIfGreaterThan(value, _rgchBuf.Length);

                if (IsNull)
                    // At this point we know that value is small enough
                    // Go back in buffer mode
                    _state = SqlBytesCharsState.Buffer;

                _lCurLen = value;
            }

            AssertValid();
        }

        // Read data of specified length from specified offset into a buffer

        public long Read(long offset, char[] buffer, int offsetInBuffer, int count)
        {
            if (IsNull)
                throw new SqlNullValueException();

            // Validate the arguments
            ArgumentNullException.ThrowIfNull(buffer);

            ArgumentOutOfRangeException.ThrowIfGreaterThan(offset, Length);
            ArgumentOutOfRangeException.ThrowIfNegative(offset);

            ArgumentOutOfRangeException.ThrowIfGreaterThan(offsetInBuffer, buffer.Length);
            ArgumentOutOfRangeException.ThrowIfNegative(offsetInBuffer);

            ArgumentOutOfRangeException.ThrowIfNegative(count);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(count, buffer.Length - offsetInBuffer);

            // Adjust count based on data length
            if (count > Length - offset)
                count = (int)(Length - offset);

            if (count != 0)
            {
                switch (_state)
                {
                    case SqlBytesCharsState.Stream:
                        if (_stream!.Position != offset)
                            _stream.Seek(offset, SeekOrigin.Begin);
                        _stream.Read(buffer, offsetInBuffer, count);
                        break;

                    default:
                        Array.Copy(_rgchBuf!, offset, buffer, offsetInBuffer, count);
                        break;
                }
            }
            return count;
        }

        // Write data of specified length into the SqlChars from specified offset

        public void Write(long offset, char[] buffer, int offsetInBuffer, int count)
        {
            if (FStream())
            {
                if (_stream!.Position != offset)
                    _stream.Seek(offset, SeekOrigin.Begin);
                _stream.Write(buffer, offsetInBuffer, count);
            }
            else
            {
                // Validate the arguments
                ArgumentNullException.ThrowIfNull(buffer);

                if (_rgchBuf == null)
                    throw new SqlTypeException(SR.SqlMisc_NoBufferMessage);

                ArgumentOutOfRangeException.ThrowIfNegative(offset);

                if (offset > _rgchBuf.Length)
                    throw new SqlTypeException(SR.SqlMisc_BufferInsufficientMessage);

                ArgumentOutOfRangeException.ThrowIfNegative(offsetInBuffer);
                ArgumentOutOfRangeException.ThrowIfGreaterThan(offsetInBuffer, buffer.Length);

                ArgumentOutOfRangeException.ThrowIfNegative(count);
                ArgumentOutOfRangeException.ThrowIfGreaterThan(count, buffer.Length - offsetInBuffer);

                if (count > _rgchBuf.Length - offset)
                    throw new SqlTypeException(SR.SqlMisc_BufferInsufficientMessage);

                if (IsNull)
                {
                    // If NULL and there is buffer inside, we only allow writing from
                    // offset zero.
                    //
                    if (offset != 0)
                        throw new SqlTypeException(SR.SqlMisc_WriteNonZeroOffsetOnNullMessage);

                    // treat as if our current length is zero.
                    // Note this has to be done after all inputs are validated, so that
                    // we won't throw exception after this point.
                    //
                    _lCurLen = 0;
                    _state = SqlBytesCharsState.Buffer;
                }
                else if (offset > _lCurLen)
                {
                    // Don't allow writing from an offset that this larger than current length.
                    // It would leave uninitialized data in the buffer.
                    //
                    throw new SqlTypeException(SR.SqlMisc_WriteOffsetLargerThanLenMessage);
                }

                if (count != 0)
                {
                    Array.Copy(buffer, offsetInBuffer, _rgchBuf, offset, count);

                    // If the last position that has been written is after
                    // the current data length, reset the length
                    if (_lCurLen < offset + count)
                        _lCurLen = offset + count;
                }
            }

            AssertValid();
        }

        public SqlString ToSqlString()
        {
            return IsNull ? SqlString.Null : new string(Value);
        }

        // --------------------------------------------------------------
        //      Conversion operators
        // --------------------------------------------------------------

        // Alternative method: ToSqlString()
        public static explicit operator SqlString(SqlChars value)
        {
            return value.ToSqlString();
        }

        // Alternative method: constructor SqlChars(SqlString)
        public static explicit operator SqlChars(SqlString value)
        {
            return new SqlChars(value);
        }

        // --------------------------------------------------------------
        //      Private utility functions
        // --------------------------------------------------------------

        [Conditional("DEBUG")]
        private void AssertValid()
        {
            Debug.Assert(_state >= SqlBytesCharsState.Null && _state <= SqlBytesCharsState.Stream);

            if (IsNull)
            {
            }
            else
            {
                Debug.Assert((_lCurLen >= 0 && _lCurLen <= x_lMaxLen) || FStream());
                Debug.Assert(FStream() || (_rgchBuf != null && _lCurLen <= _rgchBuf.Length));
                Debug.Assert(!FStream() || (_lCurLen == x_lNull));
            }
            Debug.Assert(_rgchWorkBuf == null || _rgchWorkBuf.Length == 1);
        }

        // whether the SqlChars contains a Stream
        internal bool FStream()
        {
            return _state == SqlBytesCharsState.Stream;
        }

        // Copy the data from the Stream to the array buffer.
        // If the SqlChars doesn't hold a buffer or the buffer
        // is not big enough, allocate new char array.
        private void CopyStreamToBuffer()
        {
            Debug.Assert(FStream());

            long lStreamLen = _stream!.Length;
            if (lStreamLen >= x_lMaxLen)
                throw new SqlTypeException(SR.SqlMisc_BufferInsufficientMessage);

            if (_rgchBuf == null || _rgchBuf.Length < lStreamLen)
                _rgchBuf = new char[lStreamLen];

            if (_stream.Position != 0)
                _stream.Seek(0, SeekOrigin.Begin);

            _stream.Read(_rgchBuf, 0, (int)lStreamLen);
            _stream = null;
            _lCurLen = lStreamLen;
            _state = SqlBytesCharsState.Buffer;

            AssertValid();
        }

        private void SetBuffer(char[] buffer)
        {
            _rgchBuf = buffer;
            _lCurLen = (_rgchBuf == null) ? x_lNull : _rgchBuf.Length;
            _stream = null;
            _state = (_rgchBuf == null) ? SqlBytesCharsState.Null : SqlBytesCharsState.Buffer;

            AssertValid();
        }

        // --------------------------------------------------------------
        //         XML Serialization
        // --------------------------------------------------------------


        XmlSchema? IXmlSerializable.GetSchema()
        {
            return null;
        }

        void IXmlSerializable.ReadXml(XmlReader r)
        {
            char[]? value;

            string? isNull = r.GetAttribute("nil", XmlSchema.InstanceNamespace);

            if (isNull != null && XmlConvert.ToBoolean(isNull))
            {
                // Read the next value.
                r.ReadElementString();
                SetNull();
            }
            else
            {
                value = r.ReadElementString().ToCharArray();
                SetBuffer(value);
            }
        }

        void IXmlSerializable.WriteXml(XmlWriter writer)
        {
            if (IsNull)
            {
                writer.WriteAttributeString("xsi", "nil", XmlSchema.InstanceNamespace, "true");
            }
            else
            {
                char[] value = Buffer!;
                writer.WriteString(new string(value, 0, (int)(Length)));
            }
        }

        public static XmlQualifiedName GetXsdType(XmlSchemaSet schemaSet)
        {
            return new XmlQualifiedName("string", XmlSchema.Namespace);
        }

        // --------------------------------------------------------------
        //         Serialization using ISerializable
        // --------------------------------------------------------------

        // State information is not saved. The current state is converted to Buffer and only the underlying
        // array is serialized, except for Null, in which case this state is kept.
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            throw new PlatformNotSupportedException();
        }

        // --------------------------------------------------------------
        //      Static fields, properties
        // --------------------------------------------------------------

        // Get a Null instance.
        // Since SqlChars is mutable, have to be property and create a new one each time.
        public static SqlChars Null
        {
            get
            {
                return new SqlChars((char[]?)null);
            }
        }
    } // class SqlChars

    // SqlStreamChars is a stream build on top of SqlChars, and
    // provides the Stream interface. The purpose is to help users
    // to read/write SqlChars object.
    internal sealed class SqlStreamChars
    {
        // --------------------------------------------------------------
        //      Data members
        // --------------------------------------------------------------

        private readonly SqlChars _sqlchars;        // the SqlChars object
        private long _lPosition;

        // --------------------------------------------------------------
        //      Constructor(s)
        // --------------------------------------------------------------

        internal SqlStreamChars(SqlChars s)
        {
            _sqlchars = s;
            _lPosition = 0;
        }

        // --------------------------------------------------------------
        //      Public properties
        // --------------------------------------------------------------

        public long Length
        {
            get
            {
                CheckIfStreamClosed("get_Length");
                return _sqlchars.Length;
            }
        }

        public long Position
        {
            get
            {
                CheckIfStreamClosed("get_Position");
                return _lPosition;
            }
            set
            {
                CheckIfStreamClosed("set_Position");
                ArgumentOutOfRangeException.ThrowIfNegative(value);
                ArgumentOutOfRangeException.ThrowIfGreaterThan(value, _sqlchars.Length);
                _lPosition = value;
            }
        }

        // --------------------------------------------------------------
        //      Public methods
        // --------------------------------------------------------------

        public long Seek(long offset, SeekOrigin origin)
        {
            CheckIfStreamClosed();

            long lPosition;

            switch (origin)
            {
                case SeekOrigin.Begin:
                    if (offset < 0 || offset > _sqlchars.Length)
                        throw ADP.ArgumentOutOfRange(nameof(offset));
                    _lPosition = offset;
                    break;

                case SeekOrigin.Current:
                    lPosition = _lPosition + offset;
                    if (lPosition < 0 || lPosition > _sqlchars.Length)
                        throw ADP.ArgumentOutOfRange(nameof(offset));
                    _lPosition = lPosition;
                    break;

                case SeekOrigin.End:
                    lPosition = _sqlchars.Length + offset;
                    if (lPosition < 0 || lPosition > _sqlchars.Length)
                        throw ADP.ArgumentOutOfRange(nameof(offset));
                    _lPosition = lPosition;
                    break;

                default:
                    throw ADP.ArgumentOutOfRange(nameof(offset));
            }

            return _lPosition;
        }

        // The Read/Write/Readchar/Writechar simply delegates to SqlChars
        public int Read(char[] buffer, int offset, int count)
        {
            CheckIfStreamClosed();

            ArgumentNullException.ThrowIfNull(buffer);
            ArgumentOutOfRangeException.ThrowIfNegative(offset);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(offset, buffer.Length);
            ArgumentOutOfRangeException.ThrowIfNegative(count);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(count, buffer.Length - offset);

            int icharsRead = (int)_sqlchars.Read(_lPosition, buffer, offset, count);
            _lPosition += icharsRead;

            return icharsRead;
        }

        public void Write(char[] buffer, int offset, int count)
        {
            CheckIfStreamClosed();

            ArgumentNullException.ThrowIfNull(buffer);
            ArgumentOutOfRangeException.ThrowIfNegative(offset);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(offset, buffer.Length);
            ArgumentOutOfRangeException.ThrowIfNegative(count);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(count, buffer.Length - offset);

            _sqlchars.Write(_lPosition, buffer, offset, count);
            _lPosition += count;
        }

        public void SetLength(long value)
        {
            CheckIfStreamClosed();

            _sqlchars.SetLength(value);
            if (_lPosition > value)
                _lPosition = value;
        }

        // --------------------------------------------------------------
        //      Private utility functions
        // --------------------------------------------------------------

        private bool FClosed()
        {
            return _sqlchars == null;
        }

        private void CheckIfStreamClosed([CallerMemberName] string methodname = "")
        {
            if (FClosed())
                throw ADP.StreamClosed(methodname);
        }
    } // class SqlStreamChars
} // namespace System.Data.SqlTypes
