/*
 * 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_priv.h"
#include "nffs/nffs.h"
#include "fs/fs_if.h"

extern struct fs_ops nffs_ops;

static struct nffs_file *
nffs_file_alloc(void)
{
    struct nffs_file *file;

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

    return file;
}

static int
nffs_file_free(struct nffs_file *file)
{
    int rc;

    if (file != NULL) {
        rc = os_memblock_put(&nffs_file_pool, file);
        if (rc != 0) {
            return FS_EOS;
        }
    }

    return 0;
}

/**
 * Creates a new empty file and writes it to the file system.  If a file with
 * the specified path already exists, the behavior is undefined.
 *
 * @param parent                The parent directory to insert the new file in.
 * @param filename              The name of the file to create.
 * @param filename_len          The length of the filename, in characters.
 * @param is_dir                1 if this is a directory; 0 if it is a normal
 *                                  file.
 * @param out_inode_entry       On success, this points to the inode
 *                                  corresponding to the new file.
 *
 * @return                      0 on success; nonzero on failure.
 */
int
nffs_file_new(struct nffs_inode_entry *parent, const char *filename,
              uint8_t filename_len, int is_dir,
              struct nffs_inode_entry **out_inode_entry)
{
    struct nffs_disk_inode disk_inode;
    struct nffs_inode_entry *inode_entry;
    uint32_t offset;
    uint8_t area_idx;
    int rc;

    rc = nffs_inode_entry_reserve(&inode_entry);
    if (rc != 0) {
        goto err;
    }

    rc = nffs_misc_reserve_space(sizeof disk_inode + filename_len,
                                 &area_idx, &offset);
    if (rc != 0) {
        goto err;
    }

    memset(&disk_inode, 0, sizeof disk_inode);
    if (is_dir) {
        disk_inode.ndi_id = nffs_hash_next_dir_id++;
    } else {
        disk_inode.ndi_id = nffs_hash_next_file_id++;
    }
    disk_inode.ndi_seq = 0;
    disk_inode.ndi_lastblock_id = NFFS_ID_NONE;
    if (parent == NULL) {
        disk_inode.ndi_parent_id = NFFS_ID_NONE;
    } else {
        disk_inode.ndi_parent_id = parent->nie_hash_entry.nhe_id;
    }
    disk_inode.ndi_filename_len = filename_len;
    disk_inode.ndi_flags = 0;
    nffs_crc_disk_inode_fill(&disk_inode, filename);

    rc = nffs_inode_write_disk(&disk_inode, filename, area_idx, offset);
    if (rc != 0) {
        goto err;
    }

    inode_entry->nie_hash_entry.nhe_id = disk_inode.ndi_id;
    inode_entry->nie_hash_entry.nhe_flash_loc =
        nffs_flash_loc(area_idx, offset);
    inode_entry->nie_refcnt = 1;
    inode_entry->nie_last_block_entry = NULL;

    if (parent != NULL) {
        rc = nffs_inode_add_child(parent, inode_entry);
        if (rc != 0) {
            goto err;
        }
    } else {
        assert(disk_inode.ndi_id == NFFS_ID_ROOT_DIR);
        nffs_inode_setflags(inode_entry, NFFS_INODE_FLAG_INTREE);
    }

    nffs_hash_insert(&inode_entry->nie_hash_entry);
    *out_inode_entry = inode_entry;

    return 0;

err:
    nffs_inode_entry_free(inode_entry);
    return rc;
}

/**
 * Performs a file open operation.
 *
 * @param out_file          On success, a pointer to the newly-created file
 *                              handle gets written here.
 * @param path              The path of the file to open.
 * @param access_flags      Flags controlling file access; see nffs_open() for
 *                              details.
 *
 * @return                  0 on success; nonzero on failure.
 */
int
nffs_file_open(struct nffs_file **out_file, const char *path,
               uint8_t access_flags)
{
    struct nffs_path_parser parser;
    struct nffs_inode_entry *parent;
    struct nffs_inode_entry *inode;
    struct nffs_file *file;
    int rc;

    file = NULL;

    /* Reject invalid access flag combinations. */
    if (!(access_flags & (FS_ACCESS_READ | FS_ACCESS_WRITE))) {
        rc = FS_EINVAL;
        goto err;
    }
    if (access_flags & (FS_ACCESS_APPEND | FS_ACCESS_TRUNCATE) &&
        !(access_flags & FS_ACCESS_WRITE)) {

        rc = FS_EINVAL;
        goto err;
    }
    if (access_flags & FS_ACCESS_APPEND &&
        access_flags & FS_ACCESS_TRUNCATE) {

        rc = FS_EINVAL;
        goto err;
    }

    file = nffs_file_alloc();
    if (file == NULL) {
        rc = FS_ENOMEM;
        goto err;
    }

    nffs_path_parser_new(&parser, path);
    rc = nffs_path_find(&parser, &inode, &parent);
    if (rc == FS_ENOENT && parser.npp_token_type == NFFS_PATH_TOKEN_LEAF) {
        /* The path is valid, but the file does not exist.  This is an error
         * for read-only opens.
         */
        if (!(access_flags & FS_ACCESS_WRITE)) {
            goto err;
        }

        /* Make sure the parent directory exists. */
        if (parent == NULL) {
            goto err;
        }

        /* Create a new file at the specified path. */
        rc = nffs_file_new(parent, parser.npp_token, parser.npp_token_len, 0,
                           &file->nf_inode_entry);
        if (rc != 0) {
            goto err;
        }
    } else if (rc == 0) {
        /* The file already exists. */

        /* Reject an attempt to open a directory. */
        if (nffs_hash_id_is_dir(inode->nie_hash_entry.nhe_id)) {
            rc = FS_EINVAL;
            goto err;
        }

        if (access_flags & FS_ACCESS_TRUNCATE) {
            /* The user is truncating the file.  Unlink the old file and create
             * a new one in its place.
             */
            nffs_path_unlink(path);
            rc = nffs_file_new(parent, parser.npp_token, parser.npp_token_len,
                               0, &file->nf_inode_entry);
            if (rc != 0) {
                goto err;
            }
        } else {
            /* The user is not truncating the file.  Point the file handle to
             * the existing inode.
             */
            file->nf_inode_entry = inode;
        }
    } else {
        /* Invalid path. */
        goto err;
    }

    if (access_flags & FS_ACCESS_APPEND) {
        rc = nffs_inode_data_len(file->nf_inode_entry, &file->nf_offset);
        if (rc != 0) {
            goto err;
        }
    } else {
        file->nf_offset = 0;
    }
    nffs_inode_inc_refcnt(file->nf_inode_entry);
    file->nf_access_flags = access_flags;
    file->fops = &nffs_ops;

    *out_file = file;

    return 0;

err:
    nffs_file_free(file);
    return rc;
}

/**
 * Positions a file's read and write pointer at the specified offset.  The
 * offset is expressed as the number of bytes from the start of the file (i.e.,
 * seeking to 0 places the pointer at the first byte in the file).
 *
 * @param file              The file to reposition.
 * @param offset            The offset from the start of the file to seek to.
 *
 * @return                  0 on success; nonzero on failure.
 */
int
nffs_file_seek(struct nffs_file *file, uint32_t offset)
{ 
    uint32_t len;
    int rc;

    rc = nffs_inode_data_len(file->nf_inode_entry, &len);
    if (rc != 0) {
        return rc;
    }

    if (offset > len) {
        return FS_EOFFSET;
    }

    file->nf_offset = offset;
    return 0;
}

/**
 * Reads data from the specified file.  If more data is requested than remains
 * in the file, all available data is retrieved.  Note: this type of short read
 * results in a success return code.
 *
 * @param file              The file to read from.
 * @param len               The number of bytes to attempt to read.
 * @param out_data          The destination buffer to read into.
 * @param out_len           On success, the number of bytes actually read gets
 *                              written here.  Pass null if you don't care.
 *
 * @return                  0 on success; nonzero on failure.
 */
int
nffs_file_read(struct nffs_file *file, uint32_t len, void *out_data,
               uint32_t *out_len)
{
    uint32_t bytes_read;
    int rc;

    if (!nffs_misc_ready()) {
        return FS_EUNINIT;
    }

    if (!(file->nf_access_flags & FS_ACCESS_READ)) {
        return FS_EACCESS;
    }

    rc = nffs_inode_read(file->nf_inode_entry, file->nf_offset, len, out_data,
                        &bytes_read);
    if (rc != 0) {
        return rc;
    }

    file->nf_offset += bytes_read;
    if (out_len != NULL) {
        *out_len = bytes_read;
    }

    return 0;
}

/**
 * Closes the specified file and invalidates the file handle.  If the file has
 * already been unlinked, and this is the last open handle to the file, this
 * operation causes the file to be deleted.
 *
 * @param file              The file handle to close.
 *
 * @return                  0 on success; nonzero on failure.
 */
int
nffs_file_close(struct nffs_file *file)
{
    int rc;

    rc = nffs_inode_dec_refcnt(file->nf_inode_entry);
    if (rc != 0) {
        return rc;
    }

    rc = nffs_file_free(file);
    if (rc != 0) {
        return rc;
    }

    return 0;
}
