/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

#include <assert.h>
#include "os/mynewt.h"
#include "flash_map/flash_map.h"
#include "hal/hal_bsp.h"
#include "hal/hal_flash_int.h"
#include "nffs/nffs.h"
#include "nffs_priv.h"

/**
 * Determines if the file system contains a valid root directory.  For the root
 * directory to be valid, it must be present and have the following traits:
 *     o ID equal to NFFS_ID_ROOT_DIR.
 *     o No parent inode.
 *
 * @return                      0 if there is a valid root directory;
 *                              FS_ECORRUPT if there is not a valid root
 *                                  directory;
 *                              nonzero on other error.
 */
int
nffs_misc_validate_root_dir(void)
{
    struct nffs_inode inode;
    int rc;

    if (nffs_root_dir == NULL) {
        return FS_ECORRUPT;
    }

    if (nffs_root_dir->nie_hash_entry.nhe_id != NFFS_ID_ROOT_DIR) {
        return FS_ECORRUPT;
    }

    rc = nffs_inode_from_entry(&inode, nffs_root_dir);
    /*
     * nffs_root_dir is automatically flagged a "dummy" inode but it's special
     */
    if (rc != 0 && rc != FS_ENOENT) {
        return rc;
    }

    if (inode.ni_parent != NULL) {
        return FS_ECORRUPT;
    }

    return 0;
}

/**
 * Determines if the system contains a valid scratch area.  For a scratch area
 * to be valid, it must be at least as large as the other areas in the file
 * system.
 *
 * @return                      0 if there is a valid scratch area;
 *                              FS_ECORRUPT otherwise.
 */
int
nffs_misc_validate_scratch(void)
{
    uint32_t scratch_len;
    int i;

    if (nffs_scratch_area_idx == NFFS_AREA_ID_NONE) {
        /* No scratch area. */
        return FS_ECORRUPT;
    }

    scratch_len = nffs_areas[nffs_scratch_area_idx].na_length;
    for (i = 0; i < nffs_num_areas; i++) {
        if (nffs_areas[i].na_length > scratch_len) {
            return FS_ECORRUPT;
        }
    }

    return 0;
}

/**
 * Performs a garbage cycle to free up memory, if necessary.  This function
 * should be called repeatedly until either:
 *   o The subsequent allocation is successful, or
 *   o Garbage collection is not successfully performed (indicated by a return
 *     code other than FS_EAGAIN).
 *
 * This function determines if garbage collection is necessary by inspecting
 * the value of the supplied "resource" parameter.  If resource is null, that
 * implies that allocation failed.
 *
 * This function will not initiate garbage collection if all areas have already
 * been collected in an attempt to free memory for the allocation in question.
 *
 * @param resource              The result of the allocation attempt; null
 *                                  implies that garbage collection is
 *                                  necessary.
 * @param out_rc                The status of this operation gets written here.
 *                                   0: garbage collection was successful or
 *                                       unnecessary.
 *                                   FS_EFULL: Garbage collection was not
 *                                       performed because all areas have
 *                                       already been collected.
 *                                   Other nonzero: garbage collection failed.
 *
 * @return                      FS_EAGAIN if garbage collection was
 *                                  successfully performed and the allocation
 *                                  should be retried;
 *                              Other value if the allocation should not be
 *                                  retried; the value of the out_rc parameter
 *                                  indicates whether allocation was successful
 *                                  or there was an error.
 */
int
nffs_misc_gc_if_oom(void *resource, int *out_rc)
{
    /**
     * Keeps track of the number of repeated garbage collection cycles.
     * Necessary for ensuring GC stops after all areas have been collected.
     */
    static uint8_t total_gc_cycles;

    if (resource != NULL) {
        /* Allocation succeeded.  Reset cycle count in preparation for the next
         * allocation failure.
         */
        total_gc_cycles = 0;
        *out_rc = 0;
        return 0;
    }

    /* If every area has already been garbage collected, there is nothing else
     * that can be done ("- 1" to account for the scratch area).
     */
    if (total_gc_cycles >= nffs_num_areas - 1) {
        *out_rc = FS_ENOMEM;
        return 0;
    }

    /* Attempt a garbage collection on the next area. */
    *out_rc = nffs_gc(NULL);
    total_gc_cycles++;
    STATS_INC(nffs_stats, nffs_gccnt);
    if (*out_rc != 0) {
        return 0;
    }

    /* Indicate that garbage collection was successfully performed. */
    return 1;
}

/**
 * Reserves the specified number of bytes within the specified area.
 *
 * @param area_idx              The index of the area to reserve from.
 * @param space                 The number of bytes of free space required.
 * @param out_area_offset       On success, the offset within the area gets
 *                                  written here.
 *
 * @return                      0 on success;
 *                              FS_EFULL if the area has insufficient free
 *                                  space.
 */
static int
nffs_misc_reserve_space_area(uint8_t area_idx, uint16_t space,
                             uint32_t *out_area_offset)
{
    const struct nffs_area *area;
    uint32_t available;

    area = nffs_areas + area_idx;
    available = area->na_length - area->na_cur;
    if (available >= space) {
        *out_area_offset = area->na_cur;
        return 0;
    }

    return FS_EFULL;
}

/**
 * Finds an area that can accommodate an object of the specified size.  If no
 * such area exists, this function performs a garbage collection cycle.
 *
 * @param space                 The number of bytes of free space required.
 * @param out_area_idx          On success, the index of the suitable area gets
 *                                  written here.
 * @param out_area_offset       On success, the offset within the suitable area
 *                                  gets written here.
 *
 * @return                      0 on success; nonzero on failure.
 */
int
nffs_misc_reserve_space(uint16_t space,
                        uint8_t *out_area_idx, uint32_t *out_area_offset)
{
    uint8_t area_idx;
    int rc;
    int i;

    /* Find the first area with sufficient free space. */
    for (i = 0; i < nffs_num_areas; i++) {
        if (i != nffs_scratch_area_idx) {
            rc = nffs_misc_reserve_space_area(i, space, out_area_offset);
            if (rc == 0) {
                *out_area_idx = i;
                return 0;
            }
        }
    }

    /* No area can accommodate the request.  Garbage collect until an area
     * has enough space.
     */
    rc = nffs_gc_until(space, &area_idx);
    if (rc != 0) {
        return rc;
    }

    /* Now try to reserve space.  If insufficient space was reclaimed with
     * garbage collection, the above call would have failed, so this should
     * succeed.
     */
    rc = nffs_misc_reserve_space_area(area_idx, space, out_area_offset);
    assert(rc == 0);

    *out_area_idx = area_idx;

    return rc;
}

int
nffs_misc_set_num_areas(uint8_t num_areas)
{
    if (num_areas == 0) {
        free(nffs_areas);
        nffs_areas = NULL;
    } else {
        nffs_areas = realloc(nffs_areas, num_areas * sizeof *nffs_areas);
        if (nffs_areas == NULL) {
            return FS_ENOMEM;
        }
    }

    nffs_num_areas = num_areas;

    return 0;
}

/**
 * Calculates the data length of the largest block that could fit in an area of
 * the specified size.
 */
static uint32_t
nffs_misc_area_capacity_one(uint32_t area_length)
{
    return area_length -
           sizeof (struct nffs_disk_area) -
           sizeof (struct nffs_disk_block);
}

/**
 * Calculates the data length of the largest block that could fit as a pair in
 * an area of the specified size.
 */
static uint32_t
nffs_misc_area_capacity_two(uint32_t area_length)
{
    return (area_length - sizeof (struct nffs_disk_area)) / 2 -
           sizeof (struct nffs_disk_block);
}

/**
 * Calculates and sets the maximum block data length that the system supports.
 * The result of the calculation is the greatest number which satisfies all of
 * the following restrictions:
 *     o No more than half the size of the smallest area.
 *     o No more than 2048.
 *     o No smaller than the data length of any existing data block.
 *
 * @param min_size              The minimum allowed data length.  This is the
 *                                  data length of the largest block currently
 *                                  in the file system.
 *
 * @return                      0 on success; nonzero on failure.
 */
int
nffs_misc_set_max_block_data_len(uint16_t min_data_len)
{
    uint32_t smallest_area;
    uint32_t half_smallest;
    int i;

    smallest_area = -1;
    for (i = 0; i < nffs_num_areas; i++) {
        if (nffs_areas[i].na_length < smallest_area) {
            smallest_area = nffs_areas[i].na_length;
        }
    }

    /* Don't allow a data block size bigger than the smallest area. */
    if (nffs_misc_area_capacity_one(smallest_area) < min_data_len) {
        return FS_ECORRUPT;
    }

    half_smallest = nffs_misc_area_capacity_two(smallest_area);
    if (half_smallest < NFFS_BLOCK_MAX_DATA_SZ_MAX) {
        nffs_block_max_data_sz = half_smallest;
    } else {
        nffs_block_max_data_sz = NFFS_BLOCK_MAX_DATA_SZ_MAX;
    }

    if (nffs_block_max_data_sz < min_data_len) {
        nffs_block_max_data_sz = min_data_len;
    }

    return 0;
}

int
nffs_misc_create_lost_found_dir(void)
{
    int rc;

    rc = nffs_path_new_dir("/lost+found", &nffs_lost_found_dir);
    switch (rc) {
    case 0:
        return 0;

    case FS_EEXIST:
        rc = nffs_path_find_inode_entry("/lost+found", &nffs_lost_found_dir);
        return rc;

    default:
        return rc;
    }
}


/**
 * Fully resets the nffs RAM representation.
 *
 * @return                      0 on success; nonzero on failure.
 */
int
nffs_misc_reset(void)
{
    int rc;

    nffs_cache_clear();

    rc = os_mempool_init(&nffs_file_pool, nffs_config.nc_num_files,
                         sizeof (struct nffs_file), nffs_file_mem,
                         "nffs_file_pool");
    if (rc != 0) {
        return FS_EOS;
    }

    rc = os_mempool_init(&nffs_inode_entry_pool, nffs_config.nc_num_inodes,
                         sizeof (struct nffs_inode_entry), nffs_inode_mem,
                         "nffs_inode_entry_pool");
    if (rc != 0) {
        return FS_EOS;
    }

    rc = os_mempool_init(&nffs_block_entry_pool, nffs_config.nc_num_blocks,
                         sizeof (struct nffs_hash_entry), nffs_block_entry_mem,
                         "nffs_block_entry_pool");
    if (rc != 0) {
        return FS_EOS;
    }

    rc = os_mempool_init(&nffs_cache_inode_pool,
                         nffs_config.nc_num_cache_inodes,
                         sizeof (struct nffs_cache_inode),
                         nffs_cache_inode_mem, "nffs_cache_inode_pool");
    if (rc != 0) {
        return FS_EOS;
    }

    rc = os_mempool_init(&nffs_cache_block_pool,
                         nffs_config.nc_num_cache_blocks,
                         sizeof (struct nffs_cache_block),
                         nffs_cache_block_mem, "nffs_cache_block_pool");
    if (rc != 0) {
        return FS_EOS;
    }

    rc = os_mempool_init(&nffs_dir_pool,
                         nffs_config.nc_num_dirs,
                         sizeof (struct nffs_dir),
                         nffs_dir_mem, "nffs_dir_pool");
    if (rc != 0) {
        return FS_EOS;
    }

    rc = nffs_hash_init();
    if (rc != 0) {
        return rc;
    }

    free(nffs_areas);
    nffs_areas = NULL;
    nffs_num_areas = 0;

    nffs_root_dir = NULL;
    nffs_lost_found_dir = NULL;
    nffs_scratch_area_idx = NFFS_AREA_ID_NONE;

    nffs_hash_next_file_id = NFFS_ID_FILE_MIN;
    nffs_hash_next_dir_id = NFFS_ID_DIR_MIN;
    nffs_hash_next_block_id = NFFS_ID_BLOCK_MIN;

    return 0;
}

/**
 * Indicates whether a valid filesystem has been initialized, either via
 * detection or formatting.
 *
 * @return                  1 if a file system is present; 0 otherwise.
 */
int
nffs_misc_ready(void)
{
    return nffs_root_dir != NULL;
}


/*
 * Turn flash region into a set of areas for NFFS use.
 *
 * Limit the number of regions we return to be less than *cnt.
 * If sector count within region exceeds that, collect multiple sectors
 * to a region.
 */
int
nffs_misc_desc_from_flash_area(int id, int *cnt, struct nffs_area_desc *nad)
{
    int i, j;
    const struct hal_flash *hf;
    const struct flash_area *fa;
    int max_cnt, move_on;
    int first_idx, last_idx;
    uint32_t start, size;
    uint32_t min_size;
    int rc;

    first_idx = last_idx = -1;
    max_cnt = *cnt;
    *cnt = 0;

    rc = flash_area_open(id, &fa);
    if (rc != 0) {
        return -1;
    }

    hf = hal_bsp_flash_dev(fa->fa_device_id);
    for (i = 0; i < hf->hf_sector_cnt; i++) {
        hf->hf_itf->hff_sector_info(hf, i, &start, &size);
        if (start >= fa->fa_off && start < fa->fa_off + fa->fa_size) {
            if (first_idx == -1) {
                first_idx = i;
            }
            last_idx = i;
            *cnt = *cnt + 1;
        }
    }
    if (*cnt > max_cnt) {
        min_size = fa->fa_size / max_cnt;
    } else {
        min_size = 0;
    }
    *cnt = 0;

    move_on = 1;
    for (i = first_idx, j = 0; i < last_idx + 1; i++) {
        hf->hf_itf->hff_sector_info(hf, i, &start, &size);
        if (move_on) {
            nad[j].nad_flash_id = fa->fa_device_id;
            nad[j].nad_offset = start;
            nad[j].nad_length = size;
            *cnt = *cnt + 1;
            move_on = 0;
        } else {
            nad[j].nad_length += size;
        }
        if (nad[j].nad_length >= min_size) {
            j++;
            move_on = 1;
        }
    }
    nad[*cnt].nad_length = 0;
    return 0;
}
