/*             ----> 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 allocation routines.
 *
 *  This module implements routines for working with the imap, a bitmap which
 *  tracks which blocks are allocated or free.  Some of the functionality is
 *  delegated to imapinline.c and imapextern.c.
 */
#include <redfs.h>
#include <redcore.h>


/** @brief Get the allocation bit of a block from either metaroot.
 *
 *  Will pass the call down either to the inline imap or to the external imap
 *  implementation, whichever is appropriate for the current volume.
 *
 *  @param bMR          The metaroot index: either 0 or 1.
 *  @param ulBlock      The block number to query.
 *  @param pfAllocated  On successful return, populated with the allocation bit
 *                      of the block.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EINVAL @p bMR is out of range; or @p ulBlock is out of range;
 *                      or @p pfAllocated is `NULL`.
 *  @retval -RED_EIO    A disk I/O error occurred.
 */
REDSTATUS RedImapBlockGet( uint8_t bMR,
                           uint32_t ulBlock,
                           bool * pfAllocated )
{
    REDSTATUS ret;

    if( ( bMR > 1U ) ||
        ( ulBlock < gpRedCoreVol->ulInodeTableStartBN ) ||
        ( ulBlock >= gpRedVolume->ulBlockCount ) ||
        ( pfAllocated == NULL ) )
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        #if ( REDCONF_IMAP_INLINE == 1 ) && ( REDCONF_IMAP_EXTERNAL == 1 )
            if( gpRedCoreVol->fImapInline )
            {
                ret = RedImapIBlockGet( bMR, ulBlock, pfAllocated );
            }
            else
            {
                ret = RedImapEBlockGet( bMR, ulBlock, pfAllocated );
            }
        #elif REDCONF_IMAP_INLINE == 1
            ret = RedImapIBlockGet( bMR, ulBlock, pfAllocated );
        #else /* if ( REDCONF_IMAP_INLINE == 1 ) && ( REDCONF_IMAP_EXTERNAL == 1 ) */
            ret = RedImapEBlockGet( bMR, ulBlock, pfAllocated );
        #endif /* if ( REDCONF_IMAP_INLINE == 1 ) && ( REDCONF_IMAP_EXTERNAL == 1 ) */
    }

    return ret;
}


#if REDCONF_READ_ONLY == 0

/** @brief Set the allocation bit of a block in the working metaroot.
 *
 *  Will pass the call down either to the inline imap or to the external imap
 *  implementation, whichever is appropriate for the current volume.
 *
 *  @param ulBlock      The block number to allocate or free.
 *  @param fAllocated   Whether to allocate the block (true) or free it (false).
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EINVAL @p ulBlock is out of range; or @p ulBlock is allocable
 *                      and @p fAllocated is 1.
 *  @retval -RED_EIO    A disk I/O error occurred.
 */
    REDSTATUS RedImapBlockSet( uint32_t ulBlock,
                               bool fAllocated )
    {
        REDSTATUS ret;

        if( ( ulBlock < gpRedCoreVol->ulInodeTableStartBN ) ||
            ( ulBlock >= gpRedVolume->ulBlockCount ) )
        {
            REDERROR();
            ret = -RED_EINVAL;
        }
        else if( ( ulBlock >= gpRedCoreVol->ulFirstAllocableBN ) &&
                 ( ( fAllocated && ( gpRedMR->ulFreeBlocks == 0U ) ) ||
                   ( ( !fAllocated ) && ( gpRedMR->ulFreeBlocks >= gpRedVolume->ulBlocksAllocable ) ) ) )
        {
            /*  Attempting either to free more blocks than are allocable, or
             *  allocate a block when there are none available.  This could indicate
             *  metadata corruption.
             */
            CRITICAL_ERROR();
            ret = -RED_EFUBAR;
        }
        else
        {
            #if ( REDCONF_IMAP_INLINE == 1 ) && ( REDCONF_IMAP_EXTERNAL == 1 )
                if( gpRedCoreVol->fImapInline )
                {
                    ret = RedImapIBlockSet( ulBlock, fAllocated );
                }
                else
                {
                    ret = RedImapEBlockSet( ulBlock, fAllocated );
                }
            #elif REDCONF_IMAP_INLINE == 1
                ret = RedImapIBlockSet( ulBlock, fAllocated );
            #else /* if ( REDCONF_IMAP_INLINE == 1 ) && ( REDCONF_IMAP_EXTERNAL == 1 ) */
                ret = RedImapEBlockSet( ulBlock, fAllocated );
            #endif /* if ( REDCONF_IMAP_INLINE == 1 ) && ( REDCONF_IMAP_EXTERNAL == 1 ) */

            /*  Any change to the allocation state of a block indicates that the
             *  volume is now branched.
             */
            gpRedCoreVol->fBranched = true;
        }

        /*  If a block was marked as no longer in use, discard it from the buffers.
         */
        if( ( ret == 0 ) && ( !fAllocated ) )
        {
            ret = RedBufferDiscardRange( ulBlock, 1U );
            CRITICAL_ASSERT( ret == 0 );
        }

        /*  Adjust the free/almost free block count if the block was allocable.
         *  Discard the block if required.
         */
        if( ( ret == 0 ) && ( ulBlock >= gpRedCoreVol->ulFirstAllocableBN ) )
        {
            if( fAllocated )
            {
                gpRedMR->ulFreeBlocks--;
            }
            else
            {
                bool fWasAllocated;

                /*  Whether the block became free or almost free depends on its
                 *  previous allocation state.  If it was used, then it is now
                 *  almost free.  Otherwise, it was new and is now free.
                 */
                ret = RedImapBlockGet( 1U - gpRedCoreVol->bCurMR, ulBlock, &fWasAllocated );
                CRITICAL_ASSERT( ret == 0 );

                if( ret == 0 )
                {
                    if( fWasAllocated )
                    {
                        gpRedCoreVol->ulAlmostFreeBlocks++;
                    }
                    else
                    {
                        gpRedMR->ulFreeBlocks++;
                    }
                }
            }
        }

        return ret;
    }


/** @brief Allocate one block.
 *
 *  @param pulBlock On successful return, populated with the allocated block
 *                  number.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EINVAL @p pulBlock is `NULL`.
 *  @retval -RED_EIO    A disk I/O error occurred.
 *  @retval -RED_ENOSPC Insufficient free space to perform the allocation.
 */
    REDSTATUS RedImapAllocBlock( uint32_t * pulBlock )
    {
        REDSTATUS ret;

        if( pulBlock == NULL )
        {
            REDERROR();
            ret = -RED_EINVAL;
        }
        else if( gpRedMR->ulFreeBlocks == 0U )
        {
            ret = -RED_ENOSPC;
        }
        else
        {
            uint32_t ulStopBlock = gpRedMR->ulAllocNextBlock;
            bool fAllocated = false;

            do
            {
                ALLOCSTATE state;

                ret = RedImapBlockState( gpRedMR->ulAllocNextBlock, &state );
                CRITICAL_ASSERT( ret == 0 );

                if( ret == 0 )
                {
                    if( state == ALLOCSTATE_FREE )
                    {
                        ret = RedImapBlockSet( gpRedMR->ulAllocNextBlock, true );
                        CRITICAL_ASSERT( ret == 0 );

                        *pulBlock = gpRedMR->ulAllocNextBlock;
                        fAllocated = true;
                    }

                    /*  Increment the next block number, wrapping it when the end of
                     *  the volume is reached.
                     */
                    gpRedMR->ulAllocNextBlock++;

                    if( gpRedMR->ulAllocNextBlock == gpRedVolume->ulBlockCount )
                    {
                        gpRedMR->ulAllocNextBlock = gpRedCoreVol->ulFirstAllocableBN;
                    }
                }
            }
            while( ( ret == 0 ) && !fAllocated && ( gpRedMR->ulAllocNextBlock != ulStopBlock ) );

            if( ( ret == 0 ) && !fAllocated )
            {
                /*  The free block count was already determined to be non-zero, no
                 *  error occurred while looking for free blocks, but no free blocks
                 *  were found.  This indicates metadata corruption.
                 */
                CRITICAL_ERROR();
                ret = -RED_EFUBAR;
            }
        }

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


/** @brief Get the allocation state of a block.
 *
 *  Takes into account the allocation bits from both metaroots, and returns one
 *  of four possible allocation state values:
 *
 *  - ::ALLOCSTATE_FREE Free and may be allocated; writeable.
 *  - ::ALLOCSTATE_USED In-use and transacted; not writeable.
 *  - ::ALLOCSTATE_NEW In-use but not transacted; writeable.
 *  - ::ALLOCSTATE_AFREE Will become free after a transaction; not writeable.
 *
 *  @param ulBlock  The block number to query.
 *  @param pState   On successful return, populated with the state of the block.
 *
 *  @return A negated ::REDSTATUS code indicating the operation result.
 *
 *  @retval 0           Operation was successful.
 *  @retval -RED_EINVAL @p ulBlock is out of range; or @p pState is `NULL`.
 *  @retval -RED_EIO    A disk I/O error occurred.
 */
REDSTATUS RedImapBlockState( uint32_t ulBlock,
                             ALLOCSTATE * pState )
{
    REDSTATUS ret;

    if( ( ulBlock < gpRedCoreVol->ulInodeTableStartBN ) ||
        ( ulBlock >= gpRedVolume->ulBlockCount ) ||
        ( pState == NULL ) )
    {
        REDERROR();
        ret = -RED_EINVAL;
    }
    else
    {
        bool fBitCurrent;

        ret = RedImapBlockGet( gpRedCoreVol->bCurMR, ulBlock, &fBitCurrent );

        if( ret == 0 )
        {
            bool fBitOld;

            ret = RedImapBlockGet( 1U - gpRedCoreVol->bCurMR, ulBlock, &fBitOld );

            if( ret == 0 )
            {
                if( fBitCurrent )
                {
                    if( fBitOld )
                    {
                        *pState = ALLOCSTATE_USED;
                    }
                    else
                    {
                        *pState = ALLOCSTATE_NEW;
                    }
                }
                else
                {
                    if( fBitOld )
                    {
                        *pState = ALLOCSTATE_AFREE;
                    }
                    else
                    {
                        *pState = ALLOCSTATE_FREE;
                    }
                }
            }
        }
    }

    return ret;
}
