/*
 * 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 <string.h>
#include "nffs/nffs.h"
#include "nffs_priv.h"

TAILQ_HEAD(nffs_cache_inode_list, nffs_cache_inode);
static struct nffs_cache_inode_list nffs_cache_inode_list =
    TAILQ_HEAD_INITIALIZER(nffs_cache_inode_list);

static void nffs_cache_reclaim_blocks(void);

static struct nffs_cache_block *
nffs_cache_block_alloc(void)
{
    struct nffs_cache_block *entry;

    entry = os_memblock_get(&nffs_cache_block_pool);
    if (entry != NULL) {
        memset(entry, 0, sizeof *entry);
    }

    return entry;
}

static void
nffs_cache_block_free(struct nffs_cache_block *entry)
{
    if (entry != NULL) {
        os_memblock_put(&nffs_cache_block_pool, entry);
    }
}

static struct nffs_cache_block *
nffs_cache_block_acquire(void)
{
    struct nffs_cache_block *cache_block;

    cache_block = nffs_cache_block_alloc();
    if (cache_block == NULL) {
        nffs_cache_reclaim_blocks();
        cache_block = nffs_cache_block_alloc();
    }

    assert(cache_block != NULL);

    return cache_block;
}

static int
nffs_cache_block_populate(struct nffs_cache_block *cache_block,
                          struct nffs_hash_entry *block_entry,
                          uint32_t end_offset)
{
    int rc;

    rc = nffs_block_from_hash_entry(&cache_block->ncb_block, block_entry);
    if (rc != 0) {
        return rc;
    }

    cache_block->ncb_file_offset = end_offset -
                                   cache_block->ncb_block.nb_data_len;

    return 0;
}

static struct nffs_cache_inode *
nffs_cache_inode_alloc(void)
{
    struct nffs_cache_inode *entry;

    entry = os_memblock_get(&nffs_cache_inode_pool);
    if (entry != NULL) {
        memset(entry, 0, sizeof *entry);
        TAILQ_INIT(&entry->nci_block_list);
    }

    return entry;
}

static void
nffs_cache_inode_free_blocks(struct nffs_cache_inode *cache_inode)
{
    struct nffs_cache_block *cache_block;

    while ((cache_block = TAILQ_FIRST(&cache_inode->nci_block_list)) != NULL) {
        TAILQ_REMOVE(&cache_inode->nci_block_list, cache_block, ncb_link);
        nffs_cache_block_free(cache_block);
    }
}

static void
nffs_cache_inode_free(struct nffs_cache_inode *entry)
{
    if (entry != NULL) {
        nffs_cache_inode_free_blocks(entry);
        os_memblock_put(&nffs_cache_inode_pool, entry);
    }
}

static struct nffs_cache_inode *
nffs_cache_inode_acquire(void)
{
    struct nffs_cache_inode *entry;

    entry = nffs_cache_inode_alloc();
    if (entry == NULL) {
        entry = TAILQ_LAST(&nffs_cache_inode_list, nffs_cache_inode_list);
        assert(entry != NULL);

        TAILQ_REMOVE(&nffs_cache_inode_list, entry, nci_link);
        nffs_cache_inode_free(entry);

        entry = nffs_cache_inode_alloc();
    }

    assert(entry != NULL);

    return entry;
}

static int
nffs_cache_inode_populate(struct nffs_cache_inode *cache_inode,
                         struct nffs_inode_entry *inode_entry)
{
    int rc;

    memset(cache_inode, 0, sizeof *cache_inode);

    rc = nffs_inode_from_entry(&cache_inode->nci_inode, inode_entry);
    if (rc != 0) {
        return rc;
    }

    rc = nffs_inode_calc_data_length(cache_inode->nci_inode.ni_inode_entry,
                                     &cache_inode->nci_file_size);
    if (rc != 0) {
        return rc;
    }

    return 0;
}

/**
 * Retrieves the block entry corresponding to the last cached block in the
 * specified inode's list.  If the inode has no cached blocks, this function
 * returns null.
 */
static struct nffs_hash_entry *
nffs_cache_inode_last_entry(struct nffs_cache_inode *cache_inode)
{
    struct nffs_cache_block *cache_block;

    if (TAILQ_EMPTY(&cache_inode->nci_block_list)) {
        return NULL;
    }

    cache_block = TAILQ_LAST(&cache_inode->nci_block_list,
                             nffs_cache_block_list);
    return cache_block->ncb_block.nb_hash_entry;
}

static struct nffs_cache_inode *
nffs_cache_inode_find(const struct nffs_inode_entry *inode_entry)
{
    struct nffs_cache_inode *cur;

    TAILQ_FOREACH(cur, &nffs_cache_inode_list, nci_link) {
        if (cur->nci_inode.ni_inode_entry == inode_entry) {
            return cur;
        }
    }

    return NULL;
}

void
nffs_cache_inode_range(const struct nffs_cache_inode *cache_inode,
                      uint32_t *out_start, uint32_t *out_end)
{
    struct nffs_cache_block *cache_block;

    cache_block = TAILQ_FIRST(&cache_inode->nci_block_list);
    if (cache_block == NULL) {
        *out_start = 0;
        *out_end = 0;
        return;
    }

    *out_start = cache_block->ncb_file_offset;

    cache_block = TAILQ_LAST(&cache_inode->nci_block_list,
                             nffs_cache_block_list);
    *out_end = cache_block->ncb_file_offset +
               cache_block->ncb_block.nb_data_len;
}

static void
nffs_cache_reclaim_blocks(void)
{
    struct nffs_cache_inode *cache_inode;

    TAILQ_FOREACH_REVERSE(cache_inode, &nffs_cache_inode_list,
                          nffs_cache_inode_list, nci_link) {
        if (!TAILQ_EMPTY(&cache_inode->nci_block_list)) {
            nffs_cache_inode_free_blocks(cache_inode);
            return;
        }
    }

    assert(0);
}

void
nffs_cache_inode_delete(const struct nffs_inode_entry *inode_entry)
{
    struct nffs_cache_inode *entry;

    entry = nffs_cache_inode_find(inode_entry);
    if (entry == NULL) {
        return;
    }

    TAILQ_REMOVE(&nffs_cache_inode_list, entry, nci_link);
    nffs_cache_inode_free(entry);
}

int
nffs_cache_inode_ensure(struct nffs_cache_inode **out_cache_inode,
                        struct nffs_inode_entry *inode_entry)
{
    struct nffs_cache_inode *cache_inode;
    int rc;

    cache_inode = nffs_cache_inode_find(inode_entry);
    if (cache_inode != NULL) {
        rc = 0;
        goto done;
    }

    cache_inode = nffs_cache_inode_acquire();
    rc = nffs_cache_inode_populate(cache_inode, inode_entry);
    if (rc != 0) {
        goto done;
    }

    TAILQ_INSERT_HEAD(&nffs_cache_inode_list, cache_inode, nci_link);

    rc = 0;

done:
    if (rc == 0) {
        *out_cache_inode = cache_inode;
    } else {
        nffs_cache_inode_free(cache_inode);
        *out_cache_inode = NULL;
    }
    return rc;
}

/**
 * Recaches all cached inodes.  All cached blocks are deleted from the cache
 * during this operation.  This function should be called after garbage
 * collection occurs to ensure the cache is consistent.
 *
 * @return                      0 on success; nonzero on failure.
 */
int
nffs_cache_inode_refresh(void)
{
    struct nffs_cache_inode *cache_inode;
    struct nffs_inode_entry *inode_entry;
    int rc;

    TAILQ_FOREACH(cache_inode, &nffs_cache_inode_list, nci_link) {
        /* Clear entire block list. */
        nffs_cache_inode_free_blocks(cache_inode);

        inode_entry = cache_inode->nci_inode.ni_inode_entry;
        rc = nffs_inode_from_entry(&cache_inode->nci_inode, inode_entry);
        if (rc != 0) {
            return rc;
        }

        /* File size remains valid. */
    }

    return 0;
}

static void
nffs_cache_log_block(struct nffs_cache_inode *cache_inode,
                     struct nffs_cache_block *cache_block)
{
    NFFS_LOG_DEBUG("id=%u inode=%u flash_off=0x%08x "
                    "file_off=%u len=%d (entry=%p)\n",
             (unsigned int)cache_block->ncb_block.nb_hash_entry->nhe_id,
             (unsigned int)cache_inode->nci_inode.ni_inode_entry->nie_hash_entry.nhe_id,
             (unsigned int)cache_block->ncb_block.nb_hash_entry->nhe_flash_loc,
             (unsigned int)cache_block->ncb_file_offset,
             cache_block->ncb_block.nb_data_len,
             cache_block->ncb_block.nb_hash_entry);
}

static void
nffs_cache_log_insert_block(struct nffs_cache_inode *cache_inode,
                            struct nffs_cache_block *cache_block,
                            int tail)
{
    NFFS_LOG_DEBUG("caching block (%s): ", tail ? "tail" : "head");
    nffs_cache_log_block(cache_inode, cache_block);
}

void
nffs_cache_insert_block(struct nffs_cache_inode *cache_inode,
                        struct nffs_cache_block *cache_block,
                        int tail)
{
    if (tail) {
        TAILQ_INSERT_TAIL(&cache_inode->nci_block_list, cache_block, ncb_link);
    } else {
        TAILQ_INSERT_HEAD(&cache_inode->nci_block_list, cache_block, ncb_link);
    }

    nffs_cache_log_insert_block(cache_inode, cache_block, tail);
}

/**
 * Finds the data block containing the specified offset within a file inode.
 * If the block is not yet cached, it gets cached as a result of this
 * operation.  This function modifies the inode's cached block list according
 * to the following procedure:
 *
 *  1. If none of the owning inode's blocks are currently cached, allocate a
 *     cached block entry and insert it into the inode's list.
 *  2. Else if the requested file offset is less than that of the first cached
 *     block, bridge the gap between the inode's sequence of cached blocks and
 *     the block that now needs to be cached.  This is accomplished by caching
 *     each block in the gap, finishing with the requested block.
 *  3. Else (the requested offset is beyond the end of the cache),
 *      a. If the requested offset belongs to the block that immediately
 *         follows the end of the cache, cache the block and append it to the
 *         list.
 *      b. Else, clear the cache, and populate it with the single entry
 *         corresponding to the requested block.
 *
 * @param cache_inode           The cached file inode to seek within.
 * @param seek_offset           The file offset to seek to.
 * @param out_cache_block       On success, the requested cached block gets
 *                                  written here; pass null if you don't need
 *                                  this.
 *
 * @return                      0 on success; nonzero on failure.
 */
int
nffs_cache_seek(struct nffs_cache_inode *cache_inode, uint32_t seek_offset,
                struct nffs_cache_block **out_cache_block)
{
    struct nffs_cache_block *cache_block;
    struct nffs_hash_entry *last_cached_entry;
    struct nffs_hash_entry *block_entry;
    struct nffs_hash_entry *pred_entry;
    struct nffs_block block;
    uint32_t cache_start;
    uint32_t cache_end;
    uint32_t block_start;
    uint32_t block_end;
    int rc;

    /* Empty files have no blocks that can be cached. */
    if (cache_inode->nci_file_size == 0) {
        return FS_ENOENT;
    }

    nffs_cache_inode_range(cache_inode, &cache_start, &cache_end);
    if (cache_end != 0 && seek_offset < cache_start) {
        /* Seeking prior to cache.  Iterate backwards from cache start. */
        cache_block = TAILQ_FIRST(&cache_inode->nci_block_list);
        block_entry = cache_block->ncb_block.nb_prev;
        block_end = cache_block->ncb_file_offset;
        cache_block = NULL;
    } else if (seek_offset < cache_end) {
        /* Seeking within cache.  Iterate backwards from cache end. */
        cache_block = TAILQ_LAST(&cache_inode->nci_block_list,
                                 nffs_cache_block_list);
        block_entry = cache_block->ncb_block.nb_hash_entry;
        block_end = cache_end;
    } else {
        /* Seeking beyond end of cache.  Iterate backwards from file end.  If
         * sought-after block is adjacent to cache end, its cache entry will
         * get appended to the current cache.  Otherwise, the current cache
         * will be freed and replaced with the single requested block.
         */
        cache_block = NULL;
        block_entry =
            cache_inode->nci_inode.ni_inode_entry->nie_last_block_entry;
        block_end = cache_inode->nci_file_size;
    }

    /* Scan backwards until we find the block containing the seek offest. */
    while (1) {
        if (block_end <= cache_start) {
            /* We are looking before the start of the cache.  Allocate a new
             * cache block and prepend it to the cache.
             */
            assert(cache_block == NULL);
            cache_block = nffs_cache_block_acquire();
            rc = nffs_cache_block_populate(cache_block, block_entry,
                                           block_end);
            if (rc != 0) {
                return rc;
            }

            nffs_cache_insert_block(cache_inode, cache_block, 0);
        }

        /* Calculate the file offset of the start of this block.  This is used
         * to determine if this block contains the sought-after offset.
         */
        if (cache_block != NULL) {
            /* Current block is cached. */
            block_start = cache_block->ncb_file_offset;
            pred_entry = cache_block->ncb_block.nb_prev;
        } else {
            /* We are looking beyond the end of the cache.  Read the data block
             * from flash.
             */
            rc = nffs_block_from_hash_entry(&block, block_entry);
            if (rc != 0) {
                return rc;
            }

            block_start = block_end - block.nb_data_len;
            pred_entry = block.nb_prev;
        }

        if (block_start <= seek_offset) {
            /* This block contains the requested address; iteration is
             * complete.
             */
           if (cache_block == NULL) {
                /* The block isn't cached, so it must come after the cache end.
                 * Append it to the cache if it directly follows.  Otherwise,
                 * erase the current cache and populate it with this single
                 * block.
                 */
                cache_block = nffs_cache_block_acquire();
                cache_block->ncb_block = block;
                cache_block->ncb_file_offset = block_start;

                last_cached_entry = nffs_cache_inode_last_entry(cache_inode);
                if (last_cached_entry != NULL &&
                    last_cached_entry == pred_entry) {

                    nffs_cache_insert_block(cache_inode, cache_block, 1);
                } else {
                    nffs_cache_inode_free_blocks(cache_inode);
                    nffs_cache_insert_block(cache_inode, cache_block, 0);
                }
            }

            if (out_cache_block != NULL) {
                *out_cache_block = cache_block;
            }
            break;
        }

        /* Prepare for next iteration. */
        if (cache_block != NULL) {
            cache_block = TAILQ_PREV(cache_block, nffs_cache_block_list,
                                     ncb_link);
        }
        block_entry = pred_entry;
        block_end = block_start;
    }

    return 0;
}

/**
 * Frees all cached inodes and blocks.
 */
void
nffs_cache_clear(void)
{
    struct nffs_cache_inode *entry;

    while ((entry = TAILQ_FIRST(&nffs_cache_inode_list)) != NULL) {
        TAILQ_REMOVE(&nffs_cache_inode_list, entry, nci_link);
        nffs_cache_inode_free(entry);
    }
}
