/**
 * Allocates a block entry.  If allocation fails due to memory exhaustion,
 * garbage collection is performed and the allocation is retried.  This
 * process is repeated until allocation is successful or all areas have been
 * garbage collected.
 *
 * @param out_block_entry           On success, the address of the allocated
 *                                      block gets written here.
 *
 * @return                          0 on successful allocation;
 *                                  FS_ENOMEM on memory exhaustion;
 *                                  other nonzero on garbage collection error.
 */
int
nffs_block_entry_reserve(struct nffs_hash_entry **out_block_entry)
{
    int rc;

    do {
        *out_block_entry = nffs_block_entry_alloc();
    } while (nffs_misc_gc_if_oom(*out_block_entry, &rc));

    return rc;
}
/**
 * Appends a new block to an inode block chain.
 *
 * @param inode_entry           The inode to append a block to.
 * @param data                  The contents of the new block.
 * @param len                   The number of bytes of data to write.
 *
 * @return                      0 on success; nonzero on failure.
 */
static int
nffs_write_append(struct nffs_cache_inode *cache_inode, const void *data,
                 uint16_t len)
{
    struct nffs_inode_entry *inode_entry;
    struct nffs_hash_entry *entry;
    struct nffs_disk_block disk_block;
    uint32_t area_offset;
    uint8_t area_idx;
    int rc;

    entry = nffs_block_entry_alloc();
    if (entry == NULL) {
        return FS_ENOMEM;
    }

    inode_entry = cache_inode->nci_inode.ni_inode_entry;

    disk_block.ndb_magic = NFFS_BLOCK_MAGIC;
    disk_block.ndb_id = nffs_hash_next_block_id++;
    disk_block.ndb_seq = 0;
    disk_block.ndb_inode_id = inode_entry->nie_hash_entry.nhe_id;
    if (inode_entry->nie_last_block_entry == NULL) {
        disk_block.ndb_prev_id = NFFS_ID_NONE;
    } else {
        disk_block.ndb_prev_id = inode_entry->nie_last_block_entry->nhe_id;
    }
    disk_block.ndb_data_len = len;
    nffs_crc_disk_block_fill(&disk_block, data);

    rc = nffs_block_write_disk(&disk_block, data, &area_idx, &area_offset);
    if (rc != 0) {
        return rc;
    }

    entry->nhe_id = disk_block.ndb_id;
    entry->nhe_flash_loc = nffs_flash_loc(area_idx, area_offset);
    nffs_hash_insert(entry);

    inode_entry->nie_last_block_entry = entry;

    /* Update cached inode with the new file size. */
    cache_inode->nci_file_size += len;

    /* Add appended block to the cache. */
    nffs_cache_seek(cache_inode, cache_inode->nci_file_size - 1, NULL);

    return 0;
}
/**
 * Populates the nffs RAM state with the memory representation of the specified
 * disk data block.
 *
 * @param disk_block            The source disk block to insert.
 * @param area_idx              The ID of the area containing the block.
 * @param area_offset           The area_offset within the area of the block.
 *
 * @return                      0 on success; nonzero on failure.
 */
static int
nffs_restore_block(const struct nffs_disk_block *disk_block, uint8_t area_idx,
                   uint32_t area_offset)
{
    struct nffs_inode_entry *inode_entry;
    struct nffs_hash_entry *entry;
    struct nffs_block block;
    int do_replace;
    int new_block;
    int rc;

    new_block = 0;

    /* Check the block's CRC.  If the block is corrupt, discard it.  If this
     * block would have superseded another, the old block becomes current.
     */
    rc = nffs_crc_disk_block_validate(disk_block, area_idx, area_offset);
    if (rc != 0) {
        goto err;
    }

    entry = nffs_hash_find_block(disk_block->ndb_id);
    if (entry != NULL) {
        rc = nffs_block_from_hash_entry_no_ptrs(&block, entry);
        if (rc != 0) {
            goto err;
        }

        rc = nffs_restore_block_gets_replaced(&block, disk_block, &do_replace);
        if (rc != 0) {
            goto err;
        }

        if (!do_replace) {
            /* The new block is superseded by the old; nothing to do. */
            return 0;
        }

        nffs_block_delete_from_ram(entry);
    }

    entry = nffs_block_entry_alloc();
    if (entry == NULL) {
        rc = FS_ENOMEM;
        goto err;
    }
    new_block = 1;
    entry->nhe_id = disk_block->ndb_id;
    entry->nhe_flash_loc = nffs_flash_loc(area_idx, area_offset);

    /* The block is ready to be inserted into the hash. */

    inode_entry = nffs_hash_find_inode(disk_block->ndb_inode_id);
    if (inode_entry == NULL) {
        rc = nffs_restore_dummy_inode(disk_block->ndb_inode_id, &inode_entry);
        if (rc != 0) {
            goto err;
        }
    }

    if (inode_entry->nie_last_block_entry == NULL ||
        inode_entry->nie_last_block_entry->nhe_id == disk_block->ndb_prev_id) {

        inode_entry->nie_last_block_entry = entry;
    }

    nffs_hash_insert(entry);

    if (disk_block->ndb_id >= nffs_hash_next_block_id) {
        nffs_hash_next_block_id = disk_block->ndb_id + 1;
    }

    /* Make sure the maximum block data size is not set lower than the size of
     * an existing block.
     */
    if (disk_block->ndb_data_len > nffs_restore_largest_block_data_len) {
        nffs_restore_largest_block_data_len = disk_block->ndb_data_len;
    }

    return 0;

err:
    if (new_block) {
        nffs_block_entry_free(entry);
    }
    return rc;
}
/**
 * Populates the nffs RAM state with the memory representation of the specified
 * disk data block.
 *
 * @param disk_block            The source disk block to insert.
 * @param area_idx              The ID of the area containing the block.
 * @param area_offset           The area_offset within the area of the block.
 *
 * @return                      0 on success; nonzero on failure.
 */
static int
nffs_restore_block(const struct nffs_disk_block *disk_block, uint8_t area_idx,
                   uint32_t area_offset)
{
    struct nffs_inode_entry *inode_entry;
    struct nffs_hash_entry *entry;
    struct nffs_block block;
    int do_replace;
    int new_block;
    int rc;

    new_block = 0;

    /* Check the block's CRC.  If the block is corrupt, discard it.  If this
     * block would have superseded another, the old block becomes current.
     */
    rc = nffs_crc_disk_block_validate(disk_block, area_idx, area_offset);
    if (rc != 0) {
        goto err;
    }

    entry = nffs_hash_find_block(disk_block->ndb_id);
    if (entry != NULL) {

        rc = nffs_block_from_hash_entry_no_ptrs(&block, entry);
        if (rc != 0 && rc != FS_ENOENT) {
            goto err;
        }

        /*
         * If the old block reference is for a 'dummy' block, it was added
         * because the owning inode's lastblock was not yet restored.
         * Update the block hash entry and inode to reference the entry.
         */
        if (nffs_block_is_dummy(entry)) {

            assert(entry->nhe_id == disk_block->ndb_id);

            /*
             * Entry is no longer dummy as it references the correct location
             */
            entry->nhe_flash_loc = nffs_flash_loc(area_idx, area_offset);

            inode_entry = nffs_hash_find_inode(disk_block->ndb_inode_id);

            /*
             * Turn off flags in previously restored inode recording the
             * allocation of a dummy block
             */
            if (inode_entry) {
                nffs_inode_unsetflags(inode_entry, NFFS_INODE_FLAG_DUMMYLSTBLK);
            }
        }

        rc = nffs_restore_block_gets_replaced(&block, disk_block,
                                              &do_replace);
        if (rc != 0) {
            goto err;
        }

        if (!do_replace) {
            /* The new block is superseded by the old; nothing to do. */
            return 0;
        }

        /*
         * update the existing hash entry to reference the new flash location
         */
        entry->nhe_flash_loc = nffs_flash_loc(area_idx, area_offset);

    } else {
        entry = nffs_block_entry_alloc();
        if (entry == NULL) {
            rc = FS_ENOMEM;
            goto err;
        }
        new_block = 1;
        entry->nhe_id = disk_block->ndb_id;
        entry->nhe_flash_loc = nffs_flash_loc(area_idx, area_offset);

        /* The block is ready to be inserted into the hash. */

        nffs_hash_insert(entry);

        if (disk_block->ndb_id >= nffs_hash_next_block_id) {
            nffs_hash_next_block_id = disk_block->ndb_id + 1;
        }
    }

    /* Make sure the maximum block data size is not set lower than the size of
     * an existing block.
     */
    if (disk_block->ndb_data_len > nffs_restore_largest_block_data_len) {
        nffs_restore_largest_block_data_len = disk_block->ndb_data_len;
    }

    NFFS_LOG(DEBUG, "restoring block; id=0x%08x seq=%u inode_id=%u prev_id=%u "
             "data_len=%u\n",
             (unsigned int)disk_block->ndb_id,
             (unsigned int)disk_block->ndb_seq,
             (unsigned int)disk_block->ndb_inode_id,
             (unsigned int)disk_block->ndb_prev_id,
             disk_block->ndb_data_len);

    inode_entry = nffs_hash_find_inode(disk_block->ndb_inode_id);

    if (inode_entry == NULL) {
        /*
         * Owning inode not yet restored.
         * Allocate a dummy inode which temporarily owns this block.
         * It is not yet linked to a parent.
         */
        rc = nffs_restore_dummy_inode(disk_block->ndb_inode_id, &inode_entry);
        if (rc != 0) {
            goto err;
        }
        /*
         * Record that this inode was created because a block was restored
         * before the inode
         */
        nffs_inode_setflags(inode_entry, NFFS_INODE_FLAG_DUMMYINOBLK);
        inode_entry->nie_last_block_entry = entry;
    } else {
        if (nffs_inode_getflags(inode_entry, NFFS_INODE_FLAG_DELETED)) {
            /*
             * don't restore blocks for deleted inodes
             */
            rc = FS_ENOENT;
            goto err;
        }
    }

    return 0;

err:
    if (new_block) {
        nffs_hash_remove(entry);
        nffs_block_entry_free(entry);
    }
    return rc;
}