//-----------------------------------------------------------------------
// 
//     Copyright (c) Microsoft Corporation.
// 
//-----------------------------------------------------------------------
namespace Microsoft.Isam.Esent.Interop
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    /// 
    /// Base class for objects that represent a column value to be set.
    /// 
    public abstract partial class ColumnValue
    {
        /// 
        /// Internal grbit.
        /// 
        private RetrieveColumnGrbit grbit;
        /// 
        /// Initializes a new instance of the ColumnValue class.
        /// 
        protected ColumnValue()
        {
            this.ItagSequence = 1;    
        }
        /// 
        /// Gets or sets the columnid to be set or retrieved.
        /// 
        public JET_COLUMNID Columnid { get; set; }
        /// 
        /// Gets the last set or retrieved value of the column. The
        /// value is returned as a generic object.
        /// 
        public abstract object ValueAsObject { get; }
        /// 
        /// Gets or sets column update options.
        /// 
        public SetColumnGrbit SetGrbit { get; set; }
        /// 
        /// Gets or sets column retrieval options.
        /// 
        public RetrieveColumnGrbit RetrieveGrbit
        {
            get
            {
                return this.grbit;
            }
            set
            {
                this.ValidateRetrieveGrbit(value);
                this.grbit = value;
            }
        }
        /// 
        /// Gets or sets the column itag sequence.
        /// 
        public int ItagSequence { get; set; }
        /// 
        /// Gets the warning generated by retrieving or setting this column.
        /// 
        public JET_wrn Error { get; internal set; }
        /// 
        /// Gets the byte length of a column value, which is zero if column is null, otherwise
        /// it matches the Size for fixed-size columns and represent the actual value byte
        /// length for variable sized columns (i.e. binary and string). For strings the length
        /// is determined in assumption two bytes per character.
        /// 
        public abstract int Length { get; }
        /// 
        /// Gets the size of the value in the column. This returns 0 for
        /// variable sized columns (i.e. binary and string).
        /// 
        protected abstract int Size { get; }
        /// 
        /// Returns a  that represents the current .
        /// 
        /// 
        /// A  that represents the current .
        /// 
        public abstract override string ToString();
        /// 
        /// Recursive RetrieveColumns method for data pinning. This should pin a buffer and
        /// call the inherited RetrieveColumns method.
        /// 
        /// The session to use.
        /// 
        /// The table to retrieve the columns from.
        /// 
        /// 
        /// Column values to retrieve.
        /// 
        internal static void RetrieveColumns(JET_SESID sesid, JET_TABLEID tableid, ColumnValue[] columnValues)
        {
            const int MaxColumnValues = 1024;
            if (columnValues.Length > MaxColumnValues)
            {
                throw new ArgumentOutOfRangeException("columnValues", columnValues.Length, "Too many column values");    
            }
            unsafe
            {
                byte[] buffer = null;
                NATIVE_RETRIEVECOLUMN* nativeRetrievecolumns = stackalloc NATIVE_RETRIEVECOLUMN[columnValues.Length];
                try
                {
                    buffer = Caches.ColumnCache.Allocate();
                    Debug.Assert(MaxColumnValues * 16 < buffer.Length, "Maximum size of fixed columns could exceed buffer size");
                    fixed (byte* pinnedBuffer = buffer)
                    {
                        byte* currentBuffer = pinnedBuffer;
                        int numVariableLengthColumns = columnValues.Length;
                        // First the fixed-size columns
                        for (int i = 0; i < columnValues.Length; ++i)
                        {
                            if (0 != columnValues[i].Size)
                            {
                                columnValues[i].MakeNativeRetrieveColumn(ref nativeRetrievecolumns[i]);
                                nativeRetrievecolumns[i].pvData = new IntPtr(currentBuffer);
                                nativeRetrievecolumns[i].cbData = checked((uint)columnValues[i].Size);
                                currentBuffer += nativeRetrievecolumns[i].cbData;
                                Debug.Assert(currentBuffer <= pinnedBuffer + buffer.Length, "Moved past end of pinned buffer");
                                numVariableLengthColumns--;
                            }
                        }
                        // Now the variable-length columns
                        if (numVariableLengthColumns > 0)
                        {
                            int bufferUsed = checked((int)(currentBuffer - pinnedBuffer));
                            int bufferRemaining = checked(buffer.Length - bufferUsed);
                            int bufferPerColumn = bufferRemaining / numVariableLengthColumns;
                            Debug.Assert(bufferPerColumn > 0, "Not enough buffer left to retrieve variable length columns");
                            // Now the variable-size columns
                            for (int i = 0; i < columnValues.Length; ++i)
                            {
                                if (0 == columnValues[i].Size)
                                {
                                    columnValues[i].MakeNativeRetrieveColumn(ref nativeRetrievecolumns[i]);
                                    nativeRetrievecolumns[i].pvData = new IntPtr(currentBuffer);
                                    nativeRetrievecolumns[i].cbData = checked((uint)bufferPerColumn);
                                    currentBuffer += nativeRetrievecolumns[i].cbData;
                                    Debug.Assert(currentBuffer <= pinnedBuffer + buffer.Length, "Moved past end of pinned buffer");
                                }
                            }
                        }
                        // Retrieve the columns
                        Api.Check(Api.Impl.JetRetrieveColumns(sesid, tableid, nativeRetrievecolumns, columnValues.Length));
                        // Propagate the warnings and output.
                        for (int i = 0; i < columnValues.Length; ++i)
                        {
                            columnValues[i].Error = (JET_wrn)nativeRetrievecolumns[i].err;
                            columnValues[i].ItagSequence = (int)nativeRetrievecolumns[i].itagSequence;
                        }
                        // Now parse out the columns that were retrieved successfully
                        for (int i = 0; i < columnValues.Length; ++i)
                        {
                            if (nativeRetrievecolumns[i].err != (int)JET_wrn.BufferTruncated)
                            {
                                byte* columnBuffer = (byte*)nativeRetrievecolumns[i].pvData;
                                int startIndex = checked((int)(columnBuffer - pinnedBuffer));
                                columnValues[i].GetValueFromBytes(
                                    buffer,
                                    startIndex,
                                    checked((int)nativeRetrievecolumns[i].cbActual),
                                    nativeRetrievecolumns[i].err);
                            }
                        }
                    }
                    // Finally retrieve the buffers where the columns weren't large enough.
                    RetrieveTruncatedBuffers(sesid, tableid, columnValues, nativeRetrievecolumns);
                }
                finally
                {
                    if (buffer != null)
                    {
                        Caches.ColumnCache.Free(ref buffer);
                    }
                }
            }
        }
        /// 
        /// Recursive SetColumns method for data pinning. This should populate the buffer and
        /// call the inherited SetColumns method.
        /// 
        /// The session to use.
        /// 
        /// The table to set the columns in. An update should be prepared.
        /// 
        /// 
        /// Column values to set.
        /// 
        /// 
        /// Structures to put the pinned data in.
        /// 
        /// Offset of this object in the array.
        /// An error code.
        internal abstract unsafe int SetColumns(JET_SESID sesid, JET_TABLEID tableid, ColumnValue[] columnValues, NATIVE_SETCOLUMN* nativeColumns, int i);
        /// 
        /// Recursive SetColumns function used to pin data.
        /// 
        /// The session to use.
        /// 
        /// The table to set the columns in. An update should be prepared.
        /// 
        /// 
        /// Column values to set.
        /// 
        /// 
        /// Structures to put the pinned data in.
        /// 
        /// Offset of this object in the array.
        /// The buffer for this object.
        /// Size of the buffer for ths object.
        /// True if this object is non null.
        /// An error code.
        /// 
        /// This is marked as internal because it uses the NATIVE_SETCOLUMN type
        /// which is also marked as internal. It should be treated as a protected
        /// method though.
        /// 
        internal unsafe int SetColumns(
            JET_SESID sesid,
            JET_TABLEID tableid,
            ColumnValue[] columnValues,
            NATIVE_SETCOLUMN* nativeColumns,
            int i,
            void* buffer,
            int bufferSize,
            bool hasValue)
        {
            Debug.Assert(this == columnValues[i], "SetColumns should be called on the current object");
            this.MakeNativeSetColumn(ref nativeColumns[i]);
            if (hasValue)
            {
                nativeColumns[i].cbData = checked((uint)bufferSize);
                nativeColumns[i].pvData = new IntPtr(buffer);
                if (0 == bufferSize)
                {
                    nativeColumns[i].grbit |= (uint)SetColumnGrbit.ZeroLength;
                }
            }
            int err = i == columnValues.Length - 1
                          ? Api.Impl.JetSetColumns(sesid, tableid, nativeColumns, columnValues.Length)
                          : columnValues[i + 1].SetColumns(sesid, tableid, columnValues, nativeColumns, i + 1);
            this.Error = (JET_wrn)nativeColumns[i].err;
            return err;
        }
        /// 
        /// Given data retrieved from ESENT, decode the data and set the value in the ColumnValue object.
        /// 
        /// An array of bytes.
        /// The starting position within the bytes.
        /// The number of bytes to decode.
        /// The error returned from ESENT.
        protected abstract void GetValueFromBytes(byte[] value, int startIndex, int count, int err);
        /// 
        /// Validation for the requested retrieve options for the column.
        /// 
        /// The retrieve options to validate.
        protected virtual void ValidateRetrieveGrbit(RetrieveColumnGrbit grbit)
        {
            // We cannot support this request when there is no way to indicate that a column reference is returned.
            if ((grbit & (RetrieveColumnGrbit)0x00020000) != 0)  // UnpublishedGrbits.RetrieveAsRefIfNotInRecord
            {
                throw new EsentInvalidGrbitException();
            }
        }
        /// 
        /// Retrieve the value for columns whose buffers were truncated.
        /// 
        /// The session to use.
        /// The table to use.
        /// The column values.
        /// 
        /// The native retrieve columns that match the column values.
        /// 
        private static unsafe void RetrieveTruncatedBuffers(JET_SESID sesid, JET_TABLEID tableid, ColumnValue[] columnValues, NATIVE_RETRIEVECOLUMN* nativeRetrievecolumns)
        {
            for (int i = 0; i < columnValues.Length; ++i)
            {
                if (nativeRetrievecolumns[i].err == (int)JET_wrn.BufferTruncated)
                {
                    var buffer = new byte[nativeRetrievecolumns[i].cbActual];
                    int actualSize;
                    int err;
                    var retinfo = new JET_RETINFO { itagSequence = columnValues[i].ItagSequence };
                    // Pin the buffer and retrieve the data
                    fixed (byte* pinnedBuffer = buffer)
                    {
                        err = Api.Impl.JetRetrieveColumn(
                                      sesid,
                                      tableid,
                                      columnValues[i].Columnid,
                                      new IntPtr(pinnedBuffer),
                                      buffer.Length,
                                      out actualSize,
                                      columnValues[i].RetrieveGrbit,
                                      retinfo);
                    }
                    if (JET_wrn.BufferTruncated == (JET_wrn)err)
                    {
                        string error = string.Format(
                            CultureInfo.CurrentCulture,
                            "Column size changed from {0} to {1}. The record was probably updated by another thread.",
                            buffer.Length,
                            actualSize);
                        Trace.TraceError(error);
                        throw new InvalidOperationException(error);
                    }
                    // Throw errors, but put warnings in the structure
                    Api.Check(err);
                    columnValues[i].Error = (JET_wrn)err;
                    // For BytesColumnValue this will copy the data to a new array.
                    // If this situation becomes common we should simply use the array.
                    columnValues[i].GetValueFromBytes(buffer, 0, actualSize, err);
                }
            }
        }
        /// 
        /// Create a native SetColumn from this object.
        /// 
        /// The native setcolumn structure to fill in.
        private void MakeNativeSetColumn(ref NATIVE_SETCOLUMN setcolumn)
        {
            setcolumn.columnid = this.Columnid.Value;
            setcolumn.grbit = (uint)this.SetGrbit;
            setcolumn.itagSequence = checked((uint)this.ItagSequence);
        }
        /// 
        /// Create a native RetrieveColumn from this object.
        /// 
        /// 
        /// The retrieve column structure to fill in.
        /// 
        private void MakeNativeRetrieveColumn(ref NATIVE_RETRIEVECOLUMN retrievecolumn)
        {
            retrievecolumn.columnid = this.Columnid.Value;
            retrievecolumn.grbit = (uint)this.RetrieveGrbit;
            retrievecolumn.itagSequence = checked((uint)this.ItagSequence);
        }
    }
}