/*             ----> DO NOT REMOVE THE FOLLOWING NOTICE <----
 *
 *                 Copyright (c) 2014-2015 Datalight, Inc.
 *                     All Rights Reserved Worldwide.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; use version 2 of the License.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but "AS-IS," WITHOUT ANY WARRANTY; without even the implied warranty
 *  of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along
 *  with this program; if not, write to the Free Software Foundation, Inc.,
 *  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

/*  Businesses and individuals that for commercial or other reasons cannot
 *  comply with the terms of the GPLv2 license may obtain a commercial license
 *  before incorporating Reliance Edge into proprietary software for
 *  distribution in any form.  Visit http://www.datalight.com/reliance-edge for
 *  more information.
 */

/** @file
 *  @brief Implements inode I/O functions.
 */
#include <redfs.h>
#include <redcore.h>


/*  This value is used to initialize the uIndirEntry and uDindirEntry members of
 *  the CINODE structure.  After seeking, a value of COORD_ENTRY_INVALID in
 *  uIndirEntry indicates that there is no indirect node in the path through the
 *  file metadata structure, and a value of COORD_ENTRY_INVALID in uDindirEntry
 *  indicates that there is no double indirect node.
 */
#define COORD_ENTRY_INVALID    ( UINT16_MAX )

/*  This enumeration is used by the BranchBlock() and BranchBlockCost()
 *  functions to determine which blocks of the file metadata structure need to
 *  be branched, and which to ignore.  DINDIR requires requires branching the
 *  double indirect only, INDIR requires branching the double indirect
 *  (if present) and the indirect, and FILE_DATA requires branching the indirect
 *  and double indirect (if present) and the file data block.
 */
typedef enum
{
    BRANCHDEPTH_DINDIR = 0U,
    BRANCHDEPTH_INDIR = 1U,
    BRANCHDEPTH_FILE_DATA = 2U,
    BRANCHDEPTH_MAX = BRANCHDEPTH_FILE_DATA
} BRANCHDEPTH;


#if REDCONF_READ_ONLY == 0
    #if DELETE_SUPPORTED || TRUNCATE_SUPPORTED
        static REDSTATUS Shrink( CINODE * pInode,
                                 uint64_t ullSize );
        #if DINDIR_POINTERS > 0U
            static REDSTATUS TruncDindir( CINODE * pInode,
                                          bool * pfFreed );
        #endif
        #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
            static REDSTATUS TruncIndir( CINODE * pInode,
                                         bool * pfFreed );
        #endif
        static REDSTATUS TruncDataBlock( const CINODE * pInode,
                                         uint32_t * pulBlock,
                                         bool fPropagate );
    #endif /* if DELETE_SUPPORTED || TRUNCATE_SUPPORTED */
    static REDSTATUS ExpandPrepare( CINODE * pInode );
#endif /* if REDCONF_READ_ONLY == 0 */
static void SeekCoord( CINODE * pInode,
                       uint32_t ulBlock );
static REDSTATUS ReadUnaligned( CINODE * pInode,
                                uint64_t ullStart,
                                uint32_t ulLen,
                                uint8_t * pbBuffer );
static REDSTATUS ReadAligned( CINODE * pInode,
                              uint32_t ulBlockStart,
                              uint32_t ulBlockCount,
                              uint8_t * pbBuffer );
#if REDCONF_READ_ONLY == 0
    static REDSTATUS WriteUnaligned( CINODE * pInode,
                                     uint64_t ullStart,
                                     uint32_t ulLen,
                                     const uint8_t * pbBuffer );
    static REDSTATUS WriteAligned( CINODE * pInode,
                                   uint32_t ulBlockStart,
                                   uint32_t * pulBlockCount,
                                   const uint8_t * pbBuffer );
#endif
static REDSTATUS GetExtent( CINODE * pInode,
                            uint32_t ulBlockStart,
                            uint32_t * pulExtentStart,
                            uint32_t * pulExtentLen );
#if REDCONF_READ_ONLY == 0
    static REDSTATUS BranchBlock( CINODE * pInode,
                                  BRANCHDEPTH depth,
                                  bool fBuffer );
    static REDSTATUS BranchOneBlock( uint32_t * pulBlock,
                                     void ** ppBuffer,
                                     uint16_t uBFlag );
    static REDSTATUS BranchBlockCost( const CINODE * pInode,
                                      BRANCHDEPTH depth,
                                      uint32_t * pulCost );
    static uint32_t FreeBlockCount( void );
#endif /* if REDCONF_READ_ONLY == 0 */


/** @brief Read data from an inode.
 *
 *  @param pInode   A pointer to the cached inode structure of the inode from
 *                  which to read.
 *  @param ullStart The file offset at which to read.
 *  @param pulLen   On input, the number of bytes to attempt to read.  On
 *                  successful return, populated with the number of bytes
 *                  actually read.
 *  @param pBuffer  The buffer to read into.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_EINVAL @p pInode is not a mounted cached inode pointer; or
 *                      @p pulLen is `NULL`; or @p pBuffer is `NULL`.
 */
REDSTATUS RedInodeDataRead( CINODE * pInode,
                            uint64_t ullStart,
                            uint32_t * pulLen,
                            void * pBuffer )
{
    REDSTATUS ret = 0;

    if( !CINODE_IS_MOUNTED( pInode ) || ( pulLen == NULL ) || ( pBuffer == NULL ) )
    {
        ret = -RED_EINVAL;
    }
    else if( ullStart >= pInode->pInodeBuf->ullSize )
    {
        *pulLen = 0U;
    }
    else if( *pulLen == 0U )
    {
        /*  Do nothing, just return success.
         */
    }
    else
    {
        uint8_t * pbBuffer = CAST_VOID_PTR_TO_UINT8_PTR( pBuffer );
        uint32_t ulReadIndex = 0U;
        uint32_t ulLen = *pulLen;
        uint32_t ulRemaining;

        /*  Reading beyond the end of the file is not allowed.  If the requested
         *  read extends beyond the end of the file, truncate the read length so
         *  that the read stops at the end of the file.
         */
        if( ( pInode->pInodeBuf->ullSize - ullStart ) < ulLen )
        {
            ulLen = ( uint32_t ) ( pInode->pInodeBuf->ullSize - ullStart );
        }

        ulRemaining = ulLen;

        /*  Unaligned partial block at start.
         */
        if( ( ullStart & ( REDCONF_BLOCK_SIZE - 1U ) ) != 0U )
        {
            uint32_t ulBytesInFirstBlock = REDCONF_BLOCK_SIZE - ( uint32_t ) ( ullStart & ( REDCONF_BLOCK_SIZE - 1U ) );
            uint32_t ulThisRead = REDMIN( ulRemaining, ulBytesInFirstBlock );

            ret = ReadUnaligned( pInode, ullStart, ulThisRead, pbBuffer );

            if( ret == 0 )
            {
                ulReadIndex += ulThisRead;
                ulRemaining -= ulThisRead;
            }
        }

        /*  Whole blocks.
         */
        if( ( ret == 0 ) && ( ulRemaining >= REDCONF_BLOCK_SIZE ) )
        {
            uint32_t ulBlockOffset = ( uint32_t ) ( ( ullStart + ulReadIndex ) >> BLOCK_SIZE_P2 );
            uint32_t ulBlockCount = ulRemaining >> BLOCK_SIZE_P2;

            REDASSERT( ( ( ullStart + ulReadIndex ) & ( REDCONF_BLOCK_SIZE - 1U ) ) == 0U );

            ret = ReadAligned( pInode, ulBlockOffset, ulBlockCount, &pbBuffer[ ulReadIndex ] );

            if( ret == 0 )
            {
                ulReadIndex += ulBlockCount << BLOCK_SIZE_P2;
                ulRemaining -= ulBlockCount << BLOCK_SIZE_P2;
            }
        }

        /*  Aligned partial block at end.
         */
        if( ( ret == 0 ) && ( ulRemaining > 0U ) )
        {
            REDASSERT( ulRemaining < REDCONF_BLOCK_SIZE );
            REDASSERT( ( ( ullStart + ulReadIndex ) & ( REDCONF_BLOCK_SIZE - 1U ) ) == 0U );

            ret = ReadUnaligned( pInode, ullStart + ulReadIndex, ulRemaining, &pbBuffer[ ulReadIndex ] );
        }

        if( ret == 0 )
        {
            *pulLen = ulLen;
        }
    }

    return ret;
}


#if REDCONF_READ_ONLY == 0

/** @brief Write to an inode.
 *
 *  @param pInode   A pointer to the cached inode structure of the inode into
 *                  which to write.
 *  @param ullStart The file offset at which to write.
 *  @param pulLen   On input, the number of bytes to attempt to write.  On
 *                  successful return, populated with the number of bytes
 *                  actually written.
 *  @param pBuffer  The buffer to write from.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EFBIG  @p ullStart is greater than the maximum file size; or
 *                      @p ullStart is equal to the maximum file size and the
 *                      write length is non-zero.
 *  @retval -RED_EINVAL @p pInode is not a mounted cached inode pointer; or
 *                      @p pulLen is `NULL`; or @p pBuffer is `NULL`.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_ENOSPC No data can be written because there is insufficient
 *                      free space.
 */
    REDSTATUS RedInodeDataWrite( CINODE * pInode,
                                 uint64_t ullStart,
                                 uint32_t * pulLen,
                                 const void * pBuffer )
    {
        REDSTATUS ret = 0;

        if( !CINODE_IS_DIRTY( pInode ) || ( pulLen == NULL ) || ( pBuffer == NULL ) )
        {
            ret = -RED_EINVAL;
        }
        else if( ( ullStart > INODE_SIZE_MAX ) || ( ( ullStart == INODE_SIZE_MAX ) && ( *pulLen > 0U ) ) )
        {
            ret = -RED_EFBIG;
        }
        else if( *pulLen == 0U )
        {
            /*  Do nothing, just return success.
             */
        }
        else
        {
            const uint8_t * pbBuffer = CAST_VOID_PTR_TO_CONST_UINT8_PTR( pBuffer );
            uint32_t ulWriteIndex = 0U;
            uint32_t ulLen = *pulLen;
            uint32_t ulRemaining;

            if( ( INODE_SIZE_MAX - ullStart ) < ulLen )
            {
                ulLen = ( uint32_t ) ( INODE_SIZE_MAX - ullStart );
            }

            ulRemaining = ulLen;

            /*  If the write is beyond the current end of the file, and the current
             *  end of the file is not block-aligned, then there may be some data
             *  that needs to be zeroed in the last block.
             */
            if( ullStart > pInode->pInodeBuf->ullSize )
            {
                ret = ExpandPrepare( pInode );
            }

            /*  Partial block at start.
             */
            if( ( ret == 0 ) && ( ( ( ullStart & ( REDCONF_BLOCK_SIZE - 1U ) ) != 0U ) || ( ulRemaining < REDCONF_BLOCK_SIZE ) ) )
            {
                uint32_t ulBytesInFirstBlock = REDCONF_BLOCK_SIZE - ( uint32_t ) ( ullStart & ( REDCONF_BLOCK_SIZE - 1U ) );
                uint32_t ulThisWrite = REDMIN( ulRemaining, ulBytesInFirstBlock );

                ret = WriteUnaligned( pInode, ullStart, ulThisWrite, pbBuffer );

                if( ret == 0 )
                {
                    ulWriteIndex += ulThisWrite;
                    ulRemaining -= ulThisWrite;
                }
            }

            /*  Whole blocks.
             */
            if( ( ret == 0 ) && ( ulRemaining >= REDCONF_BLOCK_SIZE ) )
            {
                uint32_t ulBlockOffset = ( uint32_t ) ( ( ullStart + ulWriteIndex ) >> BLOCK_SIZE_P2 );
                uint32_t ulBlockCount = ulRemaining >> BLOCK_SIZE_P2;
                uint32_t ulBlocksWritten = ulBlockCount;

                REDASSERT( ( ( ullStart + ulWriteIndex ) & ( REDCONF_BLOCK_SIZE - 1U ) ) == 0U );

                ret = WriteAligned( pInode, ulBlockOffset, &ulBlocksWritten, &pbBuffer[ ulWriteIndex ] );

                if( ( ret == -RED_ENOSPC ) && ( ulWriteIndex > 0U ) )
                {
                    ulBlocksWritten = 0U;
                    ret = 0;
                }

                if( ret == 0 )
                {
                    ulWriteIndex += ulBlocksWritten << BLOCK_SIZE_P2;
                    ulRemaining -= ulBlocksWritten << BLOCK_SIZE_P2;

                    if( ulBlocksWritten < ulBlockCount )
                    {
                        ulRemaining = 0U;
                    }
                }
            }

            /*  Partial block at end.
             */
            if( ( ret == 0 ) && ( ulRemaining > 0U ) )
            {
                REDASSERT( ulRemaining < REDCONF_BLOCK_SIZE );
                REDASSERT( ( ( ullStart + ulWriteIndex ) & ( REDCONF_BLOCK_SIZE - 1U ) ) == 0U );
                REDASSERT( ulWriteIndex > 0U );

                ret = WriteUnaligned( pInode, ullStart + ulWriteIndex, ulRemaining, &pbBuffer[ ulWriteIndex ] );

                if( ret == -RED_ENOSPC )
                {
                    ret = 0;
                }
                else if( ret == 0 )
                {
                    ulWriteIndex += ulRemaining;

                    REDASSERT( ulWriteIndex == ulLen );
                }
                else
                {
                    /*  Unexpected error, return it.
                     */
                }
            }

            if( ret == 0 )
            {
                *pulLen = ulWriteIndex;

                if( ( ullStart + ulWriteIndex ) > pInode->pInodeBuf->ullSize )
                {
                    pInode->pInodeBuf->ullSize = ullStart + ulWriteIndex;
                }
            }
        }

        return ret;
    }


    #if DELETE_SUPPORTED || TRUNCATE_SUPPORTED

/** @brief Change the size of an inode.
 *
 *  @param pInode   A pointer to the cached inode structure.
 *  @param ullSize  The new file size for the inode.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EFBIG  @p ullSize is greater than the maximum file size.
 *  @retval -RED_EINVAL @p pInode is not a mounted cached inode pointer.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_ENOSPC Insufficient free space to perform the truncate.
 */
        REDSTATUS RedInodeDataTruncate( CINODE * pInode,
                                        uint64_t ullSize )
        {
            REDSTATUS ret = 0;

            /*  The inode does not need to be dirtied when it is being deleted, because
             *  the inode buffer will be discarded without ever being written to disk.
             *  Thus, we only check to see if it's mounted here.
             */
            if( !CINODE_IS_MOUNTED( pInode ) )
            {
                ret = -RED_EINVAL;
            }
            else if( ullSize > INODE_SIZE_MAX )
            {
                ret = -RED_EFBIG;
            }
            else
            {
                if( ullSize > pInode->pInodeBuf->ullSize )
                {
                    ret = ExpandPrepare( pInode );
                }
                else if( ullSize < pInode->pInodeBuf->ullSize )
                {
                    ret = Shrink( pInode, ullSize );
                }
                else
                {
                    /*  Size is staying the same, nothing to do.
                     */
                }

                if( ret == 0 )
                {
                    pInode->pInodeBuf->ullSize = ullSize;
                }
            }

            return ret;
        }


/** @brief Free all file data beyond a specified point.
 *
 *  @param pInode   A pointer to the cached inode structure.
 *  @param ullSize  The point beyond which to free all file data.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_ENOSPC Insufficient free space to perform the truncate.
 *  @retval -RED_EINVAL Invalid parameters.
 */
        static REDSTATUS Shrink( CINODE * pInode,
                                 uint64_t ullSize )
        {
            REDSTATUS ret = 0;

            /*  pInode->fDirty is checked explicitly here, instead of using the
             *  CINODE_IS_DIRTY() macro, to avoid a duplicate mount check.
             */
            if( !CINODE_IS_MOUNTED( pInode ) || ( ( ullSize > 0U ) && !pInode->fDirty ) )
            {
                REDERROR();
                ret = -RED_EINVAL;
            }
            else
            {
                uint32_t ulTruncBlock = ( uint32_t ) ( ( ullSize + REDCONF_BLOCK_SIZE - 1U ) >> BLOCK_SIZE_P2 );

                RedInodePutData( pInode );

                #if REDCONF_DIRECT_POINTERS > 0U
                    while( ulTruncBlock < REDCONF_DIRECT_POINTERS )
                    {
                        ret = TruncDataBlock( pInode, &pInode->pInodeBuf->aulEntries[ ulTruncBlock ], true );

                        if( ret != 0 )
                        {
                            break;
                        }

                        ulTruncBlock++;
                    }
                #endif /* if REDCONF_DIRECT_POINTERS > 0U */

                #if REDCONF_INDIRECT_POINTERS > 0U
                    while( ( ret == 0 ) && ( ulTruncBlock < ( REDCONF_DIRECT_POINTERS + INODE_INDIR_BLOCKS ) ) )
                    {
                        ret = RedInodeDataSeek( pInode, ulTruncBlock );

                        if( ( ret == 0 ) || ( ret == -RED_ENODATA ) )
                        {
                            bool fFreed;

                            ret = TruncIndir( pInode, &fFreed );

                            if( ret == 0 )
                            {
                                if( fFreed )
                                {
                                    pInode->pInodeBuf->aulEntries[ pInode->uInodeEntry ] = BLOCK_SPARSE;
                                }

                                /*  The next seek will go to the beginning of the next
                                 *  indirect.
                                 */
                                ulTruncBlock += ( INDIR_ENTRIES - pInode->uIndirEntry );
                            }
                        }
                    }
                #endif /* if REDCONF_INDIRECT_POINTERS > 0U */

                #if DINDIR_POINTERS > 0U
                    while( ( ret == 0 ) && ( ulTruncBlock < INODE_DATA_BLOCKS ) )
                    {
                        ret = RedInodeDataSeek( pInode, ulTruncBlock );

                        if( ( ret == 0 ) || ( ret == -RED_ENODATA ) )
                        {
                            bool fFreed;

                            /*  TruncDindir() invokes seek as it goes along, which will
                             *  update the entry values (possibly all three of these);
                             *  make a copy so we can compute things correctly after.
                             */
                            uint16_t uOrigInodeEntry = pInode->uInodeEntry;
                            uint16_t uOrigDindirEntry = pInode->uDindirEntry;
                            uint16_t uOrigIndirEntry = pInode->uIndirEntry;

                            ret = TruncDindir( pInode, &fFreed );

                            if( ret == 0 )
                            {
                                if( fFreed )
                                {
                                    pInode->pInodeBuf->aulEntries[ uOrigInodeEntry ] = BLOCK_SPARSE;
                                }

                                /*  The next seek will go to the beginning of the next
                                 *  double indirect.
                                 */
                                ulTruncBlock += ( DINDIR_DATA_BLOCKS - ( uOrigDindirEntry * INDIR_ENTRIES ) ) - uOrigIndirEntry;
                            }
                        }
                    }
                #endif /* if DINDIR_POINTERS > 0U */
            }

            return ret;
        }


        #if DINDIR_POINTERS > 0U

/** @brief Truncate a double indirect.
 *
 *  @param pInode   A pointer to the cached inode, whose coordinates indicate
 *                  the truncation boundary.
 *  @param pfFreed  On successful return, populated with whether the double
 *                  indirect node was freed.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_ENOSPC Insufficient free space to perform the truncate.
 *  @retval -RED_EINVAL Invalid parameters.
 */
            static REDSTATUS TruncDindir( CINODE * pInode,
                                          bool * pfFreed )
            {
                REDSTATUS ret = 0;

                if( !CINODE_IS_MOUNTED( pInode ) || ( pfFreed == NULL ) )
                {
                    REDERROR();
                    ret = -RED_EINVAL;
                }
                else if( pInode->pDindir == NULL )
                {
                    *pfFreed = false;
                }
                else
                {
                    bool fBranch = false;
                    uint16_t uEntry;

                    /*  The double indirect is definitely going to be branched (instead of
                     *  deleted) if any of its indirect pointers which are entirely prior to
                     *  the truncation boundary are non-sparse.
                     */
                    for( uEntry = 0U; !fBranch && ( uEntry < pInode->uDindirEntry ); uEntry++ )
                    {
                        fBranch = pInode->pDindir->aulEntries[ uEntry ] != BLOCK_SPARSE;
                    }

                    /*  Unless we already know for a fact that the double indirect is going
                     *  to be branched, examine the contents of the indirect pointer which
                     *  straddles the truncation boundary.  If the indirect is going to be
                     *  deleted, we know this indirect pointer is going away, and that might
                     *  mean the double indirect is going to be deleted also.
                     */
                    if( !fBranch && ( pInode->pDindir->aulEntries[ pInode->uDindirEntry ] != BLOCK_SPARSE ) )
                    {
                        for( uEntry = 0U; !fBranch && ( uEntry < pInode->uIndirEntry ); uEntry++ )
                        {
                            fBranch = pInode->pIndir->aulEntries[ uEntry ] != BLOCK_SPARSE;
                        }
                    }

                    if( fBranch )
                    {
                        ret = BranchBlock( pInode, BRANCHDEPTH_DINDIR, false );
                    }

                    if( ret == 0 )
                    {
                        uint32_t ulBlock = pInode->ulLogicalBlock;
                        uint16_t uStart = pInode->uDindirEntry; /* pInode->uDindirEntry will change. */

                        for( uEntry = uStart; uEntry < INDIR_ENTRIES; uEntry++ )
                        {
                            /*  Seek so that TruncIndir() has the correct indirect
                             *  buffer and indirect entry.
                             */
                            ret = RedInodeDataSeek( pInode, ulBlock );

                            if( ret == -RED_ENODATA )
                            {
                                ret = 0;
                            }

                            if( ( ret == 0 ) && ( pInode->ulIndirBlock != BLOCK_SPARSE ) )
                            {
                                bool fIndirFreed;

                                ret = TruncIndir( pInode, &fIndirFreed );

                                if( ret == 0 )
                                {
                                    /*  All of the indirects after the one which straddles
                                     *  the truncation boundary should definitely end up
                                     *  deleted.
                                     */
                                    REDASSERT( ( uEntry == uStart ) || fIndirFreed );

                                    /*  If the double indirect is being freed, all of the
                                     *  indirects should be freed too.
                                     */
                                    REDASSERT( fIndirFreed || fBranch );

                                    if( fBranch && fIndirFreed )
                                    {
                                        pInode->pDindir->aulEntries[ uEntry ] = BLOCK_SPARSE;
                                    }
                                }
                            }

                            if( ret != 0 )
                            {
                                break;
                            }

                            ulBlock += ( INDIR_ENTRIES - pInode->uIndirEntry );
                        }

                        if( ret == 0 )
                        {
                            *pfFreed = !fBranch;

                            if( !fBranch )
                            {
                                RedInodePutDindir( pInode );

                                ret = RedImapBlockSet( pInode->ulDindirBlock, false );
                            }
                        }
                    }
                }

                return ret;
            }
        #endif /* DINDIR_POINTERS > 0U */


        #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES

/** @brief Truncate a indirect.
 *
 *  @param pInode   A pointer to the cached inode, whose coordinates indicate
 *                  the truncation boundary.
 *  @param pfFreed  On successful return, populated with whether the indirect
 *                  node was freed.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_ENOSPC Insufficient free space to perform the truncate.
 *  @retval -RED_EINVAL Invalid parameters.
 */
            static REDSTATUS TruncIndir( CINODE * pInode,
                                         bool * pfFreed )
            {
                REDSTATUS ret = 0;

                if( !CINODE_IS_MOUNTED( pInode ) || ( pfFreed == NULL ) )
                {
                    REDERROR();
                    ret = -RED_EINVAL;
                }
                else if( pInode->pIndir == NULL )
                {
                    *pfFreed = false;
                }
                else
                {
                    bool fBranch = false;
                    uint16_t uEntry;

                    /*  Scan the range of entries which are not being truncated.  If there
                     *  is anything there, then the indirect will not be empty after the
                     *  truncate, so it is branched and modified instead of deleted.
                     */
                    for( uEntry = 0U; !fBranch && ( uEntry < pInode->uIndirEntry ); uEntry++ )
                    {
                        fBranch = pInode->pIndir->aulEntries[ uEntry ] != BLOCK_SPARSE;
                    }

                    if( fBranch )
                    {
                        ret = BranchBlock( pInode, BRANCHDEPTH_INDIR, false );
                    }

                    if( ret == 0 )
                    {
                        for( uEntry = pInode->uIndirEntry; uEntry < INDIR_ENTRIES; uEntry++ )
                        {
                            ret = TruncDataBlock( pInode, &pInode->pIndir->aulEntries[ uEntry ], fBranch );

                            if( ret != 0 )
                            {
                                break;
                            }
                        }

                        if( ret == 0 )
                        {
                            *pfFreed = !fBranch;

                            if( !fBranch )
                            {
                                RedInodePutIndir( pInode );

                                ret = RedImapBlockSet( pInode->ulIndirBlock, false );
                            }
                        }
                    }
                }

                return ret;
            }
        #endif /* REDCONF_DIRECT_POINTERS < INODE_ENTRIES */


/** @brief Truncate a file data block.
 *
 *  @param pInode       A pointer to the cached inode structure.
 *  @param pulBlock     On entry, contains the block to be truncated.  On
 *                      successful return, if @p fPropagate is true, populated
 *                      with BLOCK_SPARSE, otherwise unmodified.
 *  @param fPropagate   Whether the parent node is being branched.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_EINVAL Invalid parameters.
 */
        static REDSTATUS TruncDataBlock( const CINODE * pInode,
                                         uint32_t * pulBlock,
                                         bool fPropagate )
        {
            REDSTATUS ret = 0;

            if( !CINODE_IS_MOUNTED( pInode ) || ( pulBlock == NULL ) )
            {
                REDERROR();
                ret = -RED_EINVAL;
            }
            else if( *pulBlock != BLOCK_SPARSE )
            {
                ret = RedImapBlockSet( *pulBlock, false );

                #if REDCONF_INODE_BLOCKS == 1
                    if( ret == 0 )
                    {
                        if( pInode->pInodeBuf->ulBlocks == 0U )
                        {
                            CRITICAL_ERROR();
                            ret = -RED_EFUBAR;
                        }
                        else
                        {
                            pInode->pInodeBuf->ulBlocks--;
                        }
                    }
                #endif /* if REDCONF_INODE_BLOCKS == 1 */

                if( ( ret == 0 ) && fPropagate )
                {
                    *pulBlock = BLOCK_SPARSE;
                }
            }
            else
            {
                /*  Data block is sparse, nothing to truncate.
                 */
            }

            return ret;
        }
    #endif /* DELETE_SUPPORTED || TRUNCATE_SUPPORTED */


/** @brief Prepare to increase the file size.
 *
 *  When the inode size is increased, a sparse region is created.  It is
 *  possible that a prior shrink operation to an unaligned size left stale data
 *  beyond the end of the file in the last data block.  That data is not zeroed
 *  while shrinking the inode in order to transfer the disk full burden from the
 *  shrink operation to the expand operation.
 *
 *  @param pInode   A pointer to the cached inode structure.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_ENOSPC Insufficient free space to perform the truncate.
 *  @retval -RED_EINVAL Invalid parameters.
 */
    static REDSTATUS ExpandPrepare( CINODE * pInode )
    {
        REDSTATUS ret = 0;

        if( !CINODE_IS_DIRTY( pInode ) )
        {
            REDERROR();
            ret = -RED_EINVAL;
        }
        else
        {
            uint32_t ulOldSizeByteInBlock = ( uint32_t ) ( pInode->pInodeBuf->ullSize & ( REDCONF_BLOCK_SIZE - 1U ) );

            if( ulOldSizeByteInBlock != 0U )
            {
                ret = RedInodeDataSeek( pInode, ( uint32_t ) ( pInode->pInodeBuf->ullSize >> BLOCK_SIZE_P2 ) );

                if( ret == -RED_ENODATA )
                {
                    ret = 0;
                }
                else if( ret == 0 )
                {
                    ret = BranchBlock( pInode, BRANCHDEPTH_FILE_DATA, true );

                    if( ret == 0 )
                    {
                        RedMemSet( &pInode->pbData[ ulOldSizeByteInBlock ], 0U, REDCONF_BLOCK_SIZE - ulOldSizeByteInBlock );
                    }
                }
                else
                {
                    REDERROR();
                }
            }
        }

        return ret;
    }
#endif /* REDCONF_READ_ONLY == 0 */


/** @brief Seek to a given position within an inode, then buffer the data block.
 *
 *  On successful return, pInode->pbData will be populated with a buffer
 *  corresponding to the @p ulBlock block offset.
 *
 *  @param pInode   A pointer to the cached inode structure.
 *  @param ulBlock  The block offset to seek to and buffer.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0               Operation was successful.
 *  @retval -RED_ENODATA    The block offset is sparse.
 *  @retval -RED_EINVAL     @p ulBlock is too large.
 *  @retval -RED_EIO        A disk I/O error occurred.
 */
REDSTATUS RedInodeDataSeekAndRead( CINODE * pInode,
                                   uint32_t ulBlock )
{
    REDSTATUS ret;

    ret = RedInodeDataSeek( pInode, ulBlock );

    if( ( ret == 0 ) && ( pInode->pbData == NULL ) )
    {
        REDASSERT( pInode->ulDataBlock != BLOCK_SPARSE );

        ret = RedBufferGet( pInode->ulDataBlock, 0U, CAST_VOID_PTR_PTR( &pInode->pbData ) );
    }

    return ret;
}


/** @brief Seek to a given position within an inode.
 *
 *  On successful return, pInode->ulDataBlock will be populated with the
 *  physical block number corresponding to the @p ulBlock block offset.
 *
 *  Note: Callers of this function depend on its parameter checking.
 *
 *  @param pInode   A pointer to the cached inode structure.
 *  @param ulBlock  The block offset to seek to.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0               Operation was successful.
 *  @retval -RED_ENODATA    The block offset is sparse.
 *  @retval -RED_EINVAL     @p ulBlock is too large; or @p pInode is not a
 *                          mounted cached inode pointer.
 *  @retval -RED_EIO        A disk I/O error occurred.
 */
REDSTATUS RedInodeDataSeek( CINODE * pInode,
                            uint32_t ulBlock )
{
    REDSTATUS ret = 0;

    if( !CINODE_IS_MOUNTED( pInode ) || ( ulBlock >= INODE_DATA_BLOCKS ) )
    {
        ret = -RED_EINVAL;
    }
    else
    {
        SeekCoord( pInode, ulBlock );

        #if DINDIR_POINTERS > 0U
            if( pInode->uDindirEntry != COORD_ENTRY_INVALID )
            {
                if( pInode->ulDindirBlock == BLOCK_SPARSE )
                {
                    /*  If the double indirect is unallocated, so is the indirect.
                     */
                    pInode->ulIndirBlock = BLOCK_SPARSE;
                }
                else
                {
                    if( pInode->pDindir == NULL )
                    {
                        ret = RedBufferGet( pInode->ulDindirBlock, BFLAG_META_DINDIR, CAST_VOID_PTR_PTR( &pInode->pDindir ) );
                    }

                    if( ret == 0 )
                    {
                        pInode->ulIndirBlock = pInode->pDindir->aulEntries[ pInode->uDindirEntry ];
                    }
                }
            }
        #endif /* if DINDIR_POINTERS > 0U */

        #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
            if( ( ret == 0 ) && ( pInode->uIndirEntry != COORD_ENTRY_INVALID ) )
            {
                if( pInode->ulIndirBlock == BLOCK_SPARSE )
                {
                    /*  If the indirect is unallocated, so is the data block.
                     */
                    pInode->ulDataBlock = BLOCK_SPARSE;
                }
                else
                {
                    if( pInode->pIndir == NULL )
                    {
                        ret = RedBufferGet( pInode->ulIndirBlock, BFLAG_META_INDIR, CAST_VOID_PTR_PTR( &pInode->pIndir ) );
                    }

                    if( ret == 0 )
                    {
                        pInode->ulDataBlock = pInode->pIndir->aulEntries[ pInode->uIndirEntry ];
                    }
                }
            }
        #endif /* if REDCONF_DIRECT_POINTERS < INODE_ENTRIES */

        if( ( ret == 0 ) && ( pInode->ulDataBlock == BLOCK_SPARSE ) )
        {
            ret = -RED_ENODATA;
        }
    }

    return ret;
}


/** @brief Seek to the coordinates.
 *
 *  Compute the new coordinates, and put any buffers which are not needed or are
 *  no longer appropriate.
 *
 *  @param pInode   A pointer to the cached inode structure.
 *  @param ulBlock  The block offset to seek to.
 */
static void SeekCoord( CINODE * pInode,
                       uint32_t ulBlock )
{
    if( !CINODE_IS_MOUNTED( pInode ) || ( ulBlock >= INODE_DATA_BLOCKS ) )
    {
        REDERROR();
    }
    else if( ( pInode->ulLogicalBlock != ulBlock ) || !pInode->fCoordInited )
    {
        RedInodePutData( pInode );
        pInode->ulLogicalBlock = ulBlock;

        #if REDCONF_DIRECT_POINTERS > 0U
            #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
                if( ulBlock < REDCONF_DIRECT_POINTERS )
            #endif
            {
                #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
                    RedInodePutCoord( pInode );
                #endif

                pInode->uInodeEntry = ( uint16_t ) ulBlock;
                pInode->ulDataBlock = pInode->pInodeBuf->aulEntries[ pInode->uInodeEntry ];

                #if DINDIR_POINTERS > 0U
                    pInode->uDindirEntry = COORD_ENTRY_INVALID;
                #endif
                #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
                    pInode->uIndirEntry = COORD_ENTRY_INVALID;
                #endif
            }

            #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
                else
            #endif
        #endif /* if REDCONF_DIRECT_POINTERS > 0U */
        #if REDCONF_INDIRECT_POINTERS > 0U
            #if REDCONF_INDIRECT_POINTERS < INODE_ENTRIES
                if( ulBlock < ( INODE_INDIR_BLOCKS + REDCONF_DIRECT_POINTERS ) )
            #endif
            {
                uint32_t ulIndirRangeOffset = ulBlock - REDCONF_DIRECT_POINTERS;
                uint16_t uInodeEntry = ( uint16_t ) ( ( ulIndirRangeOffset / INDIR_ENTRIES ) + REDCONF_DIRECT_POINTERS );
                uint16_t uIndirEntry = ( uint16_t ) ( ulIndirRangeOffset % INDIR_ENTRIES );

                #if DINDIR_POINTERS > 0U
                    RedInodePutDindir( pInode );
                #endif

                /*  If the inode entry is not changing, then the previous indirect
                 *  is still the correct one.  Otherwise, the old indirect will be
                 *  released and the new one will be read later.
                 */
                if( ( pInode->uInodeEntry != uInodeEntry ) || !pInode->fCoordInited )
                {
                    RedInodePutIndir( pInode );

                    pInode->uInodeEntry = uInodeEntry;

                    pInode->ulIndirBlock = pInode->pInodeBuf->aulEntries[ pInode->uInodeEntry ];
                }

                #if DINDIR_POINTERS > 0U
                    pInode->uDindirEntry = COORD_ENTRY_INVALID;
                #endif
                pInode->uIndirEntry = uIndirEntry;

                /*  At this point, the following pInode members are needed but not
                 *  yet populated:
                 *
                 *  - pIndir
                 *  - ulDataBlock
                 */
            }

            #if DINDIR_POINTERS > 0U
                else
            #endif
        #endif /* if REDCONF_INDIRECT_POINTERS > 0U */
        #if DINDIR_POINTERS > 0U
        {
            uint32_t ulDindirRangeOffset = ( ulBlock - REDCONF_DIRECT_POINTERS ) - INODE_INDIR_BLOCKS;
            uint16_t uInodeEntry = ( uint16_t ) ( ( ulDindirRangeOffset / DINDIR_DATA_BLOCKS ) + REDCONF_DIRECT_POINTERS + REDCONF_INDIRECT_POINTERS );
            uint32_t ulDindirNodeOffset = ulDindirRangeOffset % DINDIR_DATA_BLOCKS;
            uint16_t uDindirEntry = ( uint16_t ) ( ulDindirNodeOffset / INDIR_ENTRIES );
            uint16_t uIndirEntry = ( uint16_t ) ( ulDindirNodeOffset % INDIR_ENTRIES );

            /*  If the inode entry is not changing, then the previous double
             *  indirect is still the correct one.  Otherwise, the old double
             *  indirect will be released and the new one will be read later.
             */
            if( ( pInode->uInodeEntry != uInodeEntry ) || !pInode->fCoordInited )
            {
                RedInodePutIndir( pInode );
                RedInodePutDindir( pInode );

                pInode->uInodeEntry = uInodeEntry;

                pInode->ulDindirBlock = pInode->pInodeBuf->aulEntries[ pInode->uInodeEntry ];
            }

            /*  If neither the inode entry nor double indirect entry are
             *  changing, then the previous indirect is still the correct one.
             *  Otherwise, it old indirect will be released and the new one will
             *  be read later.
             */
            else if( pInode->uDindirEntry != uDindirEntry )
            {
                RedInodePutIndir( pInode );
            }
            else
            {
                /*  Data buffer has already been put, nothing to do.
                 */
            }

            pInode->uDindirEntry = uDindirEntry;
            pInode->uIndirEntry = uIndirEntry;

            /*  At this point, the following pInode members are needed but not
             *  yet populated:
             *
             *  - pDindir
             *  - pIndir
             *  - ulIndirBlock
             *  - ulDataBlock
             */
        }
        #elif ( REDCONF_DIRECT_POINTERS > 0U ) && ( REDCONF_INDIRECT_POINTERS > 0U )
            else
            {
                /*  There are no double indirects, so the block should have been in
                 *  the direct or indirect range.
                 */
                REDERROR();
            }
        #endif /* if DINDIR_POINTERS > 0U */

        pInode->fCoordInited = true;
    }
    else
    {
        /*  Seeking to the current position, nothing to do.
         */
    }
}


/** @brief Read an unaligned portion of a block.
 *
 *  @param pInode   A pointer to the cached inode structure.
 *  @param ullStart The file offset at which to read.
 *  @param ulLen    The number of bytes to read.
 *  @param pbBuffer The buffer to read into.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_EINVAL Invalid parameters.
 */
static REDSTATUS ReadUnaligned( CINODE * pInode,
                                uint64_t ullStart,
                                uint32_t ulLen,
                                uint8_t * pbBuffer )
{
    REDSTATUS ret;

    /*  This read should not cross a block boundary.
     */
    if( ( ( ullStart >> BLOCK_SIZE_P2 ) != ( ( ( ullStart + ulLen ) - 1U ) >> BLOCK_SIZE_P2 ) ) ||
        ( pbBuffer == NULL ) )
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        ret = RedInodeDataSeekAndRead( pInode, ( uint32_t ) ( ullStart >> BLOCK_SIZE_P2 ) );

        if( ret == 0 )
        {
            RedMemCpy( pbBuffer, &pInode->pbData[ ullStart & ( REDCONF_BLOCK_SIZE - 1U ) ], ulLen );
        }
        else if( ret == -RED_ENODATA )
        {
            /*  Sparse block, return zeroed data.
             */
            RedMemSet( pbBuffer, 0U, ulLen );
            ret = 0;
        }
        else
        {
            /*  No action, just return the error.
             */
        }
    }

    return ret;
}


/** @brief Read one or more whole blocks.
 *
 *  @param pInode       A pointer to the cached inode structure.
 *  @param ulBlockStart The file block offset at which to read.
 *  @param ulBlockCount The number of blocks to read.
 *  @param pbBuffer     The buffer to read into.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_EINVAL Invalid parameters.
 */
static REDSTATUS ReadAligned( CINODE * pInode,
                              uint32_t ulBlockStart,
                              uint32_t ulBlockCount,
                              uint8_t * pbBuffer )
{
    REDSTATUS ret = 0;

    if( pbBuffer == NULL )
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        uint32_t ulBlockIndex = 0U;

        /*  Read the data from disk one contiguous extent at a time.
         */
        while( ( ret == 0 ) && ( ulBlockIndex < ulBlockCount ) )
        {
            uint32_t ulExtentStart;
            uint32_t ulExtentLen = ulBlockCount - ulBlockIndex;

            ret = GetExtent( pInode, ulBlockStart + ulBlockIndex, &ulExtentStart, &ulExtentLen );

            if( ret == 0 )
            {
                #if REDCONF_READ_ONLY == 0

                    /*  Before reading directly from disk, flush any dirty file data
                     *  buffers in the range to avoid reading stale data.
                     */
                    ret = RedBufferFlush( ulExtentStart, ulExtentLen );

                    if( ret == 0 )
                #endif
                {
                    ret = RedIoRead( gbRedVolNum, ulExtentStart, ulExtentLen, &pbBuffer[ ulBlockIndex << BLOCK_SIZE_P2 ] );

                    if( ret == 0 )
                    {
                        ulBlockIndex += ulExtentLen;
                    }
                }
            }
            else if( ret == -RED_ENODATA )
            {
                /*  Sparse block, return zeroed data.
                 */
                RedMemSet( &pbBuffer[ ulBlockIndex << BLOCK_SIZE_P2 ], 0U, REDCONF_BLOCK_SIZE );
                ulBlockIndex++;
                ret = 0;
            }
            else
            {
                /*  An unexpected error occurred; the loop will terminate.
                 */
            }
        }
    }

    return ret;
}


#if REDCONF_READ_ONLY == 0

/** @brief Write an unaligned portion of a block.
 *
 *  @param pInode   A pointer to the cached inode structure.
 *  @param ullStart The file offset at which to write.
 *  @param ulLen    The number of bytes to write.
 *  @param pbBuffer The buffer to write from.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_ENOSPC No data can be written because there is insufficient
 *                      free space.
 *  @retval -RED_EINVAL Invalid parameters.
 */
    static REDSTATUS WriteUnaligned( CINODE * pInode,
                                     uint64_t ullStart,
                                     uint32_t ulLen,
                                     const uint8_t * pbBuffer )
    {
        REDSTATUS ret;

        /*  This write should not cross a block boundary.
         */
        if( ( ( ullStart >> BLOCK_SIZE_P2 ) != ( ( ( ullStart + ulLen ) - 1U ) >> BLOCK_SIZE_P2 ) ) ||
            ( pbBuffer == NULL ) )
        {
            REDERROR();
            ret = -RED_EINVAL;
        }
        else
        {
            ret = RedInodeDataSeek( pInode, ( uint32_t ) ( ullStart >> BLOCK_SIZE_P2 ) );

            if( ( ret == 0 ) || ( ret == -RED_ENODATA ) )
            {
                ret = BranchBlock( pInode, BRANCHDEPTH_FILE_DATA, true );

                if( ret == 0 )
                {
                    RedMemCpy( &pInode->pbData[ ullStart & ( REDCONF_BLOCK_SIZE - 1U ) ], pbBuffer, ulLen );
                }
            }
        }

        return ret;
    }


/** @brief Write one or more whole blocks.
 *
 *  @param pInode           A pointer to the cached inode structure.
 *  @param ulBlockStart     The file block offset at which to write.
 *  @param pulBlockCount    On entry, the number of blocks to attempt to write.
 *                          On successful return, the number of blocks actually
 *                          written.
 *  @param pbBuffer         The buffer to write from.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_ENOSPC No data can be written because there is insufficient
 *                      free space.
 *  @retval -RED_EINVAL Invalid parameters.
 */
    static REDSTATUS WriteAligned( CINODE * pInode,
                                   uint32_t ulBlockStart,
                                   uint32_t * pulBlockCount,
                                   const uint8_t * pbBuffer )
    {
        REDSTATUS ret = 0;

        if( ( pulBlockCount == NULL ) || ( pbBuffer == NULL ) )
        {
            REDERROR();
            ret = -RED_EINVAL;
        }
        else
        {
            bool fFull = false;
            uint32_t ulBlockCount = *pulBlockCount;
            uint32_t ulBlockIndex;

            /*  Branch all of the file data blocks in advance.
             */
            for( ulBlockIndex = 0U; ( ulBlockIndex < ulBlockCount ) && !fFull; ulBlockIndex++ )
            {
                ret = RedInodeDataSeek( pInode, ulBlockStart + ulBlockIndex );

                if( ( ret == 0 ) || ( ret == -RED_ENODATA ) )
                {
                    ret = BranchBlock( pInode, BRANCHDEPTH_FILE_DATA, false );

                    if( ret == -RED_ENOSPC )
                    {
                        if( ulBlockIndex > 0U )
                        {
                            ret = 0;
                        }

                        fFull = true;
                    }
                }

                if( ret != 0 )
                {
                    break;
                }
            }

            ulBlockCount = ulBlockIndex;
            ulBlockIndex = 0U;

            if( fFull )
            {
                ulBlockCount--;
            }

            /*  Write the data to disk one contiguous extent at a time.
             */
            while( ( ret == 0 ) && ( ulBlockIndex < ulBlockCount ) )
            {
                uint32_t ulExtentStart;
                uint32_t ulExtentLen = ulBlockCount - ulBlockIndex;

                ret = GetExtent( pInode, ulBlockStart + ulBlockIndex, &ulExtentStart, &ulExtentLen );

                if( ret == 0 )
                {
                    ret = RedIoWrite( gbRedVolNum, ulExtentStart, ulExtentLen, &pbBuffer[ ulBlockIndex << BLOCK_SIZE_P2 ] );

                    if( ret == 0 )
                    {
                        /*  If there is any buffered file data for the extent we
                         *  just wrote, those buffers are now stale.
                         */
                        ret = RedBufferDiscardRange( ulExtentStart, ulExtentLen );
                    }

                    if( ret == 0 )
                    {
                        ulBlockIndex += ulExtentLen;
                    }
                }
            }

            if( ret == 0 )
            {
                *pulBlockCount = ulBlockCount;
            }
        }

        return ret;
    }
#endif /* REDCONF_READ_ONLY == 0 */


/** @brief Get the physical block number and count of contiguous blocks given a
 *         starting logical block number.
 *
 *  @param pInode           A pointer to the cached inode structure.
 *  @param ulBlockStart     The file block offset for the start of the extent.
 *  @param pulExtentStart   On successful return, the starting physical block
 *                          number of the contiguous extent.
 *  @param pulExtentLen     On entry, the maximum length of the extent; on
 *                          successful return, the length of the contiguous
 *                          extent.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0               Operation was successful.
 *  @retval -RED_EIO        A disk I/O error occurred.
 *  @retval -RED_ENODATA    The block offset is sparse.
 *  @retval -RED_EINVAL     Invalid parameters.
 */
static REDSTATUS GetExtent( CINODE * pInode,
                            uint32_t ulBlockStart,
                            uint32_t * pulExtentStart,
                            uint32_t * pulExtentLen )
{
    REDSTATUS ret;

    if( ( pulExtentStart == NULL ) || ( pulExtentLen == NULL ) )
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        ret = RedInodeDataSeek( pInode, ulBlockStart );

        if( ret == 0 )
        {
            uint32_t ulExtentLen = *pulExtentLen;
            uint32_t ulFirstBlock = pInode->ulDataBlock;
            uint32_t ulRunLen = 1U;

            while( ( ret == 0 ) && ( ulRunLen < ulExtentLen ) )
            {
                ret = RedInodeDataSeek( pInode, ulBlockStart + ulRunLen );

                /*  The extent ends when we find a sparse data block or when the
                 *  data block is not contiguous with the preceding data block.
                 */
                if( ( ret == -RED_ENODATA ) || ( ( ret == 0 ) && ( pInode->ulDataBlock != ( ulFirstBlock + ulRunLen ) ) ) )
                {
                    ret = 0;
                    break;
                }

                ulRunLen++;
            }

            if( ret == 0 )
            {
                *pulExtentStart = ulFirstBlock;
                *pulExtentLen = ulRunLen;
            }
        }
    }

    return ret;
}


#if REDCONF_READ_ONLY == 0

/** @brief Allocate or branch the file metadata path and data block if necessary.
 *
 *  Optionally, can stop allocating/branching at a certain depth.
 *
 *  @param pInode   A pointer to the cached inode structure.
 *  @param depth    A BRANCHDEPTH_ value indicating the lowest depth to branch.
 *  @param fBuffer  Whether to buffer the data block.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_ENOSPC No data can be written because there is insufficient
 *                      free space.
 */
    static REDSTATUS BranchBlock( CINODE * pInode,
                                  BRANCHDEPTH depth,
                                  bool fBuffer )
    {
        REDSTATUS ret;
        uint32_t ulCost = 0U; /* Init'd to quiet warnings. */

        ret = BranchBlockCost( pInode, depth, &ulCost );

        if( ( ret == 0 ) && ( ulCost > FreeBlockCount() ) )
        {
            ret = -RED_ENOSPC;
        }

        if( ret == 0 )
        {
            #if DINDIR_POINTERS > 0U
                if( pInode->uDindirEntry != COORD_ENTRY_INVALID )
                {
                    ret = BranchOneBlock( &pInode->ulDindirBlock, CAST_VOID_PTR_PTR( &pInode->pDindir ), BFLAG_META_DINDIR );

                    if( ret == 0 )
                    {
                        /*  In case we just created the double indirect.
                         */
                        pInode->pDindir->ulInode = pInode->ulInode;

                        pInode->pInodeBuf->aulEntries[ pInode->uInodeEntry ] = pInode->ulDindirBlock;
                    }
                }

                if( ret == 0 )
            #endif /* if DINDIR_POINTERS > 0U */
            #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
            {
                if( ( pInode->uIndirEntry != COORD_ENTRY_INVALID ) && ( depth >= BRANCHDEPTH_INDIR ) )
                {
                    ret = BranchOneBlock( &pInode->ulIndirBlock, CAST_VOID_PTR_PTR( &pInode->pIndir ), BFLAG_META_INDIR );

                    if( ret == 0 )
                    {
                        /*  In case we just created the indirect.
                         */
                        pInode->pIndir->ulInode = pInode->ulInode;

                        #if DINDIR_POINTERS > 0U
                            if( pInode->uDindirEntry != COORD_ENTRY_INVALID )
                            {
                                pInode->pDindir->aulEntries[ pInode->uDindirEntry ] = pInode->ulIndirBlock;
                            }
                            else
                        #endif
                        {
                            pInode->pInodeBuf->aulEntries[ pInode->uInodeEntry ] = pInode->ulIndirBlock;
                        }
                    }
                }
            }

            if( ret == 0 )
            #endif /* if REDCONF_DIRECT_POINTERS < INODE_ENTRIES */
            {
                if( depth == BRANCHDEPTH_FILE_DATA )
                {
                    #if REDCONF_INODE_BLOCKS == 1
                        bool fAllocedNew = ( pInode->ulDataBlock == BLOCK_SPARSE );
                    #endif
                    void ** ppBufPtr = ( fBuffer || ( pInode->pbData != NULL ) ) ? CAST_VOID_PTR_PTR( &pInode->pbData ) : NULL;

                    ret = BranchOneBlock( &pInode->ulDataBlock, ppBufPtr, 0U );

                    if( ret == 0 )
                    {
                        #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
                            if( pInode->uIndirEntry != COORD_ENTRY_INVALID )
                            {
                                pInode->pIndir->aulEntries[ pInode->uIndirEntry ] = pInode->ulDataBlock;
                            }
                            else
                        #endif
                        {
                            pInode->pInodeBuf->aulEntries[ pInode->uInodeEntry ] = pInode->ulDataBlock;
                        }

                        #if REDCONF_INODE_BLOCKS == 1
                            if( fAllocedNew )
                            {
                                if( pInode->pInodeBuf->ulBlocks < INODE_DATA_BLOCKS )
                                {
                                    pInode->pInodeBuf->ulBlocks++;
                                }
                                else
                                {
                                    CRITICAL_ERROR();
                                    ret = -RED_EFUBAR;
                                }
                            }
                        #endif /* if REDCONF_INODE_BLOCKS == 1 */
                    }
                }
            }

            CRITICAL_ASSERT( ret == 0 );
        }

        return ret;
    }


/** @brief Branch a block.
 *
 *  The block can be a double indirect, indirect, or file data block.
 *
 *  The caller should have already handled the disk full implications of
 *  branching this block.
 *
 *  @param pulBlock On entry, the current block number, which may be
 *                  BLOCK_SPARSE if the block is to be newly allocated.  On
 *                  successful return, populated with the new block number,
 *                  which may be the same as the original block number if it
 *                  was not BLOCK_SPARSE and the block was already branched.
 *  @param ppBuffer If NULL, indicates that the caller does not want to buffer
 *                  the branched block.  If non-NULL, the caller does want the
 *                  branched block buffered, and the following is true:  On
 *                  entry, the current buffer for the block, if there is one, or
 *                  NULL if there is no buffer.  On successful exit, populated
 *                  with a buffer for the block, which will be dirty.  If the
 *                  block number is initially BLOCK_SPARSE, there should be no
 *                  buffer for the block.
 *  @param uBFlag   The buffer type flags: BFLAG_META_DINDIR, BFLAG_META_INDIR,
 *                  or zero for file data.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_EINVAL Invalid parameters.
 */
    static REDSTATUS BranchOneBlock( uint32_t * pulBlock,
                                     void ** ppBuffer,
                                     uint16_t uBFlag )
    {
        REDSTATUS ret = 0;

        if( pulBlock == NULL )
        {
            REDERROR();
            ret = -RED_EINVAL;
        }
        else
        {
            ALLOCSTATE state = ALLOCSTATE_FREE;
            uint32_t ulPrevBlock = *pulBlock;

            if( ulPrevBlock != BLOCK_SPARSE )
            {
                ret = RedImapBlockState( ulPrevBlock, &state );
            }

            if( ret == 0 )
            {
                if( state == ALLOCSTATE_NEW )
                {
                    /*  Block is already branched, so simply get it buffered dirty
                     *  if requested.
                     */
                    if( ppBuffer != NULL )
                    {
                        if( *ppBuffer != NULL )
                        {
                            RedBufferDirty( *ppBuffer );
                        }
                        else
                        {
                            ret = RedBufferGet( ulPrevBlock, uBFlag | BFLAG_DIRTY, ppBuffer );
                        }
                    }
                }
                else
                {
                    /*  Block does not exist or is committed state, so allocate a
                     *  new block for the branch.
                     */
                    ret = RedImapAllocBlock( pulBlock );

                    if( ret == 0 )
                    {
                        if( ulPrevBlock == BLOCK_SPARSE )
                        {
                            /*  Block did not exist previously, so just get it
                             *  buffered if requested.
                             */
                            if( ppBuffer != NULL )
                            {
                                if( *ppBuffer != NULL )
                                {
                                    /*  How could there be an existing buffer when
                                     *  the block did not exist?
                                     */
                                    REDERROR();
                                    ret = -RED_EINVAL;
                                }
                                else
                                {
                                    ret = RedBufferGet( *pulBlock, ( uint16_t ) ( ( uint32_t ) uBFlag | BFLAG_NEW | BFLAG_DIRTY ), ppBuffer );
                                }
                            }
                        }
                        else
                        {
                            /*  Branch the buffer for the committed state block to
                             *  the newly allocated location.
                             */
                            if( ppBuffer != NULL )
                            {
                                if( *ppBuffer == NULL )
                                {
                                    ret = RedBufferGet( ulPrevBlock, uBFlag, ppBuffer );
                                }

                                if( ret == 0 )
                                {
                                    RedBufferBranch( *ppBuffer, *pulBlock );
                                }
                            }

                            /*  Mark the committed state block almost free.
                             */
                            if( ret == 0 )
                            {
                                ret = RedImapBlockSet( ulPrevBlock, false );
                            }
                        }
                    }
                }
            }
        }

        return ret;
    }


/** @brief Compute the free space cost of branching a block.
 *
 *  The caller must first use RedInodeDataSeek() to the block to be branched.
 *
 *  @param pInode   A pointer to the cached inode structure, whose coordinates
 *                  indicate the block to be branched.
 *  @param depth    A BRANCHDEPTH_ value indicating how much of the file
 *                  metadata structure needs to be branched.
 *  @param pulCost  On successful return, populated with the number of blocks
 *                  that must be allocated from free space in order to branch
 *                  the given block.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_EINVAL Invalid parameters.
 */
    static REDSTATUS BranchBlockCost( const CINODE * pInode,
                                      BRANCHDEPTH depth,
                                      uint32_t * pulCost )
    {
        REDSTATUS ret = 0;

        if( !CINODE_IS_MOUNTED( pInode ) || !pInode->fCoordInited || ( depth > BRANCHDEPTH_MAX ) || ( pulCost == NULL ) )
        {
            REDERROR();
            ret = -RED_EINVAL;
        }
        else
        {
            ALLOCSTATE state;

            /*  ulCost is initialized to the maximum number of blocks that could
             *  be branched, and decremented for every block we determine does not
             *  need to be branched.
             */
            #if DINDIR_POINTERS > 0U
                uint32_t ulCost = 3U;
            #elif REDCONF_DIRECT_POINTERS < INODE_ENTRIES
                uint32_t ulCost = 2U;
            #else
                uint32_t ulCost = 1U;
            #endif

            #if DINDIR_POINTERS > 0U
                if( pInode->uDindirEntry != COORD_ENTRY_INVALID )
                {
                    if( pInode->ulDindirBlock != BLOCK_SPARSE )
                    {
                        ret = RedImapBlockState( pInode->ulDindirBlock, &state );

                        if( ( ret == 0 ) && ( state == ALLOCSTATE_NEW ) )
                        {
                            /*  Double indirect already branched.
                             */
                            ulCost--;
                        }
                    }
                }
                else
                {
                    /*  At this inode offset there are no double indirects.
                     */
                    ulCost--;
                }

                if( ret == 0 )
            #endif /* if DINDIR_POINTERS > 0U */
            #if REDCONF_DIRECT_POINTERS < INODE_ENTRIES
            {
                if( ( pInode->uIndirEntry != COORD_ENTRY_INVALID ) && ( depth >= BRANCHDEPTH_INDIR ) )
                {
                    if( pInode->ulIndirBlock != BLOCK_SPARSE )
                    {
                        ret = RedImapBlockState( pInode->ulIndirBlock, &state );

                        if( ( ret == 0 ) && ( state == ALLOCSTATE_NEW ) )
                        {
                            /*  Indirect already branched.
                             */
                            ulCost--;
                        }
                    }
                }
                else
                {
                    /*  Either not branching this deep, or at this inode offset
                     *  there are no indirects.
                     */
                    ulCost--;
                }
            }

            if( ret == 0 )
            #endif /* if REDCONF_DIRECT_POINTERS < INODE_ENTRIES */
            {
                if( depth == BRANCHDEPTH_FILE_DATA )
                {
                    if( pInode->ulDataBlock != BLOCK_SPARSE )
                    {
                        ret = RedImapBlockState( pInode->ulDataBlock, &state );

                        if( ( ret == 0 ) && ( state == ALLOCSTATE_NEW ) )
                        {
                            /*  File data block already branched.
                             */
                            ulCost--;

                            /*  If the file data block is branched, then its parent
                             *  nodes should be branched as well.
                             */
                            REDASSERT( ulCost == 0U );
                        }
                    }
                }
                else
                {
                    /*  Not branching this deep.
                     */
                    ulCost--;
                }
            }

            if( ret == 0 )
            {
                *pulCost = ulCost;
            }
        }

        return ret;
    }


/** @brief Yields the number of currently available free blocks.
 *
 *  Accounts for reserved blocks, subtracting the number of reserved blocks if
 *  they are unavailable.
 *
 *  @return Number of currently available free blocks.
 */
    static uint32_t FreeBlockCount( void )
    {
        uint32_t ulFreeBlocks = gpRedMR->ulFreeBlocks;

        #if RESERVED_BLOCKS > 0U
            if( !gpRedCoreVol->fUseReservedBlocks )
            {
                if( ulFreeBlocks >= RESERVED_BLOCKS )
                {
                    ulFreeBlocks -= RESERVED_BLOCKS;
                }
                else
                {
                    ulFreeBlocks = 0U;
                }
            }
        #endif /* if RESERVED_BLOCKS > 0U */

        return ulFreeBlocks;
    }
#endif /* REDCONF_READ_ONLY == 0 */
