/*             ----> 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 Implementation of the Reliance Edge FSE API.
*/
#include <redfs.h>

#if REDCONF_API_FSE == 1

/** @defgroup red_group_fse The File System Essentials Interface
    @{
*/

#include <redvolume.h>
#include <redcoreapi.h>
#include <redfse.h>


static REDSTATUS FseEnter(uint8_t bVolNum);
static void FseLeave(void);


static bool gfFseInited;    /* Whether driver is initialized. */


/** @brief Initialize the Reliance Edge file system driver.

    Prepares the Reliance Edge file system driver to be used.  Must be the first
    Reliance Edge function to be invoked: no volumes can be mounted until the
    driver has been initialized.

    If this function is called when the Reliance Edge driver is already
    initialized, it does nothing and returns success.

    This function is not thread safe: attempting to initialize from multiple
    threads could leave things in a bad state.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0   Operation was successful.
*/
REDSTATUS RedFseInit(void)
{
    REDSTATUS ret;

    if(gfFseInited)
    {
        ret = 0;
    }
    else
    {
        ret = RedCoreInit();

        if(ret == 0)
        {
            gfFseInited = true;
        }
    }

    return ret;
}


/** @brief Uninitialize the Reliance Edge file system driver.

    Tears down the Reliance Edge file system driver.  Cannot be used until all
    Reliance Edge volumes are unmounted.  A subsequent call to RedFseInit()
    will initialize the driver again.

    If this function is called when the Reliance Edge driver is already
    uninitialized, it does nothing and returns success.

    This function is not thread safe: attempting to uninitialize from multiple
    threads could leave things in a bad state.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EBUSY  At least one volume is still mounted.
*/
REDSTATUS RedFseUninit(void)
{
    REDSTATUS ret = 0;

    if(!gfFseInited)
    {
        ret = 0;
    }
    else
    {
        uint8_t bVolNum;

      #if REDCONF_TASK_COUNT > 1U
        RedOsMutexAcquire();
      #endif

        for(bVolNum = 0U; bVolNum < REDCONF_VOLUME_COUNT; bVolNum++)
        {
            if(gaRedVolume[bVolNum].fMounted)
            {
                ret = -RED_EBUSY;
                break;
            }
        }

        if(ret == 0)
        {
            gfFseInited = false;
        }

      #if REDCONF_TASK_COUNT > 1U
        RedOsMutexRelease();
      #endif

        if(ret == 0)
        {
            ret = RedCoreUninit();
        }
    }

    return ret;
}


/** @brief Mount a file system volume.

    Prepares the file system volume to be accessed.  Mount will fail if the
    volume has never been formatted, or if the on-disk format is inconsistent
    with the compile-time configuration.

    If the volume is already mounted, this function does nothing and returns
    success.

    @param bVolNum  The volume number of the volume to be mounted.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EINVAL @p bVolNum is an invalid volume number; or the driver is
                        uninitialized.
    @retval -RED_EIO    Volume not formatted, improperly formatted, or corrupt.
*/
REDSTATUS RedFseMount(
    uint8_t     bVolNum)
{
    REDSTATUS   ret;

    ret = FseEnter(bVolNum);

    if(ret == 0)
    {
        if(!gpRedVolume->fMounted)
        {
            ret = RedCoreVolMount();
        }

        FseLeave();
    }

    return ret;
}


/** @brief Unmount a file system volume.

    This function discards the in-memory state for the file system and marks it
    as unmounted.  Subsequent attempts to access the volume will fail until the
    volume is mounted again.

    If unmount automatic transaction points are enabled, this function will
    commit a transaction point prior to unmounting.  If unmount automatic
    transaction points are disabled, this function will unmount without
    transacting, effectively discarding the working state.

    Before unmounting, this function will wait for any active file system
    thread to complete by acquiring the FS mutex.  The volume will be marked as
    unmounted before the FS mutex is released, so subsequent FS threads will
    possibly block and then see an error when attempting to access a volume
    which is unmounting or unmounted.

    If the volume is already unmounted, this function does nothing and returns
    success.

    @param bVolNum  The volume number of the volume to be unmounted.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EINVAL @p bVolNum is an invalid volume number; or the driver is
                        uninitialized.
    @retval -RED_EIO    I/O error during unmount automatic transaction point.
*/
REDSTATUS RedFseUnmount(
    uint8_t     bVolNum)
{
    REDSTATUS   ret;

    ret = FseEnter(bVolNum);

    if(ret == 0)
    {
        if(gpRedVolume->fMounted)
        {
            ret = RedCoreVolUnmount();
        }

        FseLeave();
    }

    return ret;
}


#if (REDCONF_READ_ONLY == 0) && (REDCONF_API_FSE_FORMAT == 1)
/** @brief Format a file system volume.

    Uses the statically defined volume configuration.  After calling this
    function, the volume needs to be mounted -- see RedFseMount().

    An error is returned if the volume is mounted.

    @param bVolNum  The volume number of the volume to be formatted.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EBUSY  The volume is mounted.
    @retval -RED_EINVAL @p bVolNum is an invalid volume number; or the driver is
                        uninitialized.
    @retval -RED_EIO    I/O error formatting the volume.
*/
REDSTATUS RedFseFormat(
    uint8_t     bVolNum)
{
    REDSTATUS   ret;

    ret = FseEnter(bVolNum);

    if(ret == 0)
    {
        ret = RedCoreVolFormat();

        FseLeave();
    }

    return ret;
}
#endif


/** @brief Read from a file.

    Data which has not yet been written, but which is before the end-of-file
    (sparse data), shall read as zeroes.  A short read -- where the number of
    bytes read is less than requested -- indicates that the requested read was
    partially or, if zero bytes were read, entirely beyond the end-of-file.

    If @p ullFileOffset is at or beyond the maximum file size, it is treated
    like any other read entirely beyond the end-of-file: no data is read and
    zero is returned.

    @param bVolNum          The volume number of the file to read.
    @param ulFileNum        The file number of the file to read.
    @param ullFileOffset    The file offset to read from.
    @param ulLength         The number of bytes to read.
    @param pBuffer          The buffer to populate with the data read.  Must be
                            at least ulLength bytes in size.

    @return The number of bytes read (nonnegative) or a negated ::REDSTATUS
            code indicating the operation result (negative).

    @retval >=0         The number of bytes read from the file.
    @retval -RED_EBADF  @p ulFileNum is not a valid file number.
    @retval -RED_EINVAL @p bVolNum is an invalid volume number or not mounted;
                        or @p pBuffer is `NULL`; or @p ulLength exceeds
                        INT32_MAX and cannot be returned properly.
    @retval -RED_EIO    A disk I/O error occurred.
*/
int32_t RedFseRead(
    uint8_t     bVolNum,
    uint32_t    ulFileNum,
    uint64_t    ullFileOffset,
    uint32_t    ulLength,
    void       *pBuffer)
{
    int32_t     ret;

    if(ulLength > (uint32_t)INT32_MAX)
    {
        ret = -RED_EINVAL;
    }
    else
    {
        ret = FseEnter(bVolNum);
    }

    if(ret == 0)
    {
        uint32_t ulReadLen = ulLength;

        ret = RedCoreFileRead(ulFileNum, ullFileOffset, &ulReadLen, pBuffer);

        FseLeave();

        if(ret == 0)
        {
            ret = (int32_t)ulReadLen;
        }
    }

    return ret;
}


#if REDCONF_READ_ONLY == 0
/** @brief Write to a file.

    If the write extends beyond the end-of-file, the file size will be
    increased.

    A short write -- where the number of bytes written is less than requested
    -- indicates either that the file system ran out of space but was still
    able to write some of the request; or that the request would have caused
    the file to exceed the maximum file size, but some of the data could be
    written prior to the file size limit.

    If an error is returned (negative return), either none of the data was
    written or a critical error occurred (like an I/O error) and the file
    system volume will be read-only.

    @param bVolNum          The volume number of the file to write.
    @param ulFileNum        The file number of the file to write.
    @param ullFileOffset    The file offset to write at.
    @param ulLength         The number of bytes to write.
    @param pBuffer          The buffer containing the data to be written.  Must
                            be at least ulLength bytes in size.

    @return The number of bytes written (nonnegative) or a negated ::REDSTATUS
            code indicating the operation result (negative).

    @retval >0          The number of bytes written to the file.
    @retval -RED_EBADF  @p ulFileNum is not a valid file number.
    @retval -RED_EFBIG  No data can be written to the given file offset since
                        the resulting file size would exceed the maximum file
                        size.
    @retval -RED_EINVAL @p bVolNum is an invalid volume number or not mounted;
                        or @p pBuffer is `NULL`; or @p ulLength exceeds
                        INT32_MAX and cannot be returned properly.
    @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_EROFS  The file system volume is read-only.
*/
int32_t RedFseWrite(
    uint8_t     bVolNum,
    uint32_t    ulFileNum,
    uint64_t    ullFileOffset,
    uint32_t    ulLength,
    const void *pBuffer)
{
    int32_t     ret;

    if(ulLength > (uint32_t)INT32_MAX)
    {
        ret = -RED_EINVAL;
    }
    else
    {
        ret = FseEnter(bVolNum);
    }

    if(ret == 0)
    {
        uint32_t ulWriteLen = ulLength;

        ret = RedCoreFileWrite(ulFileNum, ullFileOffset, &ulWriteLen, pBuffer);

        FseLeave();

        if(ret == 0)
        {
            ret = (int32_t)ulWriteLen;
        }
    }

    return ret;
}
#endif


#if (REDCONF_READ_ONLY == 0) && (REDCONF_API_FSE_TRUNCATE == 1)
/** @brief Truncate a file (set the file size).

    Allows the file size to be increased, decreased, or to remain the same.  If
    the file size is increased, the new area is sparse (will read as zeroes).
    If the file size is decreased, the data beyond the new end-of-file will
    return to free space once it is no longer part of the committed state
    (either immediately or after the next transaction point).

    This function can fail when the disk is full if @p ullNewFileSize is
    non-zero.  If decreasing the file size, this can be fixed by transacting and
    trying again: Reliance Edge guarantees that it is possible to perform a
    truncate of at least one file that decreases the file size after a
    transaction point.  If disk full transactions are enabled, this will happen
    automatically.

    @param bVolNum          The volume number of the file to truncate.
    @param ulFileNum        The file number of the file to truncate.
    @param ullNewFileSize   The new file size, in bytes.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EBADF  @p ulFileNum is not a valid file number.
    @retval -RED_EFBIG  @p ullNewFileSize exceeds the maximum file size.
    @retval -RED_EINVAL @p bVolNum is an invalid volume number or not mounted.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_ENOSPC Insufficient free space to perform the truncate.
    @retval -RED_EROFS  The file system volume is read-only.
*/
REDSTATUS RedFseTruncate(
    uint8_t     bVolNum,
    uint32_t    ulFileNum,
    uint64_t    ullNewFileSize)
{
    REDSTATUS   ret;

    ret = FseEnter(bVolNum);

    if(ret == 0)
    {
        ret = RedCoreFileTruncate(ulFileNum, ullNewFileSize);

        FseLeave();
    }

    return ret;
}
#endif


/** @brief Retrieve the size of a file.

    @param bVolNum      The volume number of the file whose size is being read.
    @param ulFileNum    The file number of the file whose size is being read.

    @return The size of the file (nonnegative) or a negated ::REDSTATUS code
            indicating the operation result (negative).

    @retval >=0         The size of the file.
    @retval -RED_EBADF  @p ulFileNum is not a valid file number.
    @retval -RED_EINVAL @p bVolNum is an invalid volume number or not mounted.
    @retval -RED_EIO    A disk I/O error occurred.
*/
int64_t RedFseSizeGet(
    uint8_t     bVolNum,
    uint32_t    ulFileNum)
{
    int64_t     ret;

    ret = FseEnter(bVolNum);

    if(ret == 0)
    {
        uint64_t ullSize;

        ret = RedCoreFileSizeGet(ulFileNum, &ullSize);

        FseLeave();

        if(ret == 0)
        {
            /*  Unless there is an on-disk format change, the maximum file size
                is guaranteed to be less than INT64_MAX, and so it can be safely
                returned in an int64_t.
            */
            REDASSERT(ullSize < (uint64_t)INT64_MAX);

            ret = (int64_t)ullSize;
        }
    }

    return ret;
}


#if (REDCONF_READ_ONLY == 0) && (REDCONF_API_FSE_TRANSMASKSET == 1)
/** @brief Update the transaction mask.

    The following events are available:

    - #RED_TRANSACT_UMOUNT
    - #RED_TRANSACT_WRITE
    - #RED_TRANSACT_TRUNCATE
    - #RED_TRANSACT_VOLFULL

    The #RED_TRANSACT_MANUAL macro (by itself) may be used to disable all
    automatic transaction events.  The #RED_TRANSACT_MASK macro is a bitmask of
    all transaction flags, excluding those representing excluded functionality.

    Attempting to enable events for excluded functionality will result in an
    error.

    @param bVolNum      The volume number of the volume whose transaction mask
                        is being changed.
    @param ulEventMask  A bitwise-OR'd mask of automatic transaction events to
                        be set as the current transaction mode.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EINVAL @p bVolNum is an invalid volume number or not mounted;
                        or @p ulEventMask contains invalid bits.
    @retval -RED_EROFS  The file system volume is read-only.
*/
REDSTATUS RedFseTransMaskSet(
    uint8_t     bVolNum,
    uint32_t    ulEventMask)
{
    REDSTATUS   ret;

    ret = FseEnter(bVolNum);

    if(ret == 0)
    {
        ret = RedCoreTransMaskSet(ulEventMask);

        FseLeave();
    }

    return ret;
}
#endif


#if REDCONF_API_FSE_TRANSMASKGET == 1
/** @brief Read the transaction mask.

    If the volume is read-only, the returned event mask is always zero.

    @param bVolNum      The volume number of the volume whose transaction mask
                        is being retrieved.
    @param pulEventMask Populated with a bitwise-OR'd mask of automatic
                        transaction events which represent the current
                        transaction mode for the volume.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EINVAL @p bVolNum is an invalid volume number or not mounted;
                        or @p pulEventMask is `NULL`.
*/
REDSTATUS RedFseTransMaskGet(
    uint8_t     bVolNum,
    uint32_t   *pulEventMask)
{
    REDSTATUS   ret;

    ret = FseEnter(bVolNum);

    if(ret == 0)
    {
        ret = RedCoreTransMaskGet(pulEventMask);

        FseLeave();
    }

    return ret;
}
#endif


#if REDCONF_READ_ONLY == 0
/** @brief Commit a transaction point.

    Reliance Edge is a transactional file system.  All modifications, of both
    metadata and filedata, are initially working state.  A transaction point
    is a process whereby the working state atomically becomes the committed
    state, replacing the previous committed state.  Whenever Reliance Edge is
    mounted, including after power loss, the state of the file system after
    mount is the most recent committed state.  Nothing from the committed
    state is ever missing, and nothing from the working state is ever included.

    @param bVolNum  The volume number of the volume to transact.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EINVAL @p bVolNum is an invalid volume number or not mounted.
    @retval -RED_EIO    A disk I/O error occurred.
    @retval -RED_EROFS  The file system volume is read-only.
*/
REDSTATUS RedFseTransact(
    uint8_t     bVolNum)
{
    REDSTATUS   ret;

    ret = FseEnter(bVolNum);

    if(ret == 0)
    {
        ret = RedCoreVolTransact();

        FseLeave();
    }

    return ret;
}
#endif

/** @} */

/** @brief Enter the file system driver.

    @param bVolNum  The volume to be accessed.

    @return A negated ::REDSTATUS code indicating the operation result.

    @retval 0           Operation was successful.
    @retval -RED_EINVAL The file system driver is uninitialized; or @p bVolNum
                        is not a valid volume number.
*/
static REDSTATUS FseEnter(
    uint8_t   bVolNum)
{
    REDSTATUS ret;

    if(gfFseInited)
    {
      #if REDCONF_TASK_COUNT > 1U
        RedOsMutexAcquire();
      #endif

        /*  This also serves to range-check the volume number (even in single
            volume configurations).
        */
        ret = RedCoreVolSetCurrent(bVolNum);

      #if REDCONF_TASK_COUNT > 1U
        if(ret != 0)
        {
            RedOsMutexRelease();
        }
      #endif
    }
    else
    {
        ret = -RED_EINVAL;
    }

    return ret;
}


/** @brief Leave the file system driver.
*/
static void FseLeave(void)
{
    REDASSERT(gfFseInited);

  #if REDCONF_TASK_COUNT > 1U
    RedOsMutexRelease();
  #endif
}


#endif /* REDCONF_API_FSE == 1 */

