/*
 * Copyright (c) 2006-2020, RT-Thread Development Team
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Change Logs:
 * Date           Author       Notes
 * 2020/08/21     ShaoJinchun  first version
 */

#include <rtthread.h>
#include <dfs.h>
#include <dfs_fs.h>
#include <dfs_file.h>

#include "dfs_cromfs.h"

#include <stdint.h>

#include "zlib.h"

/**********************************/

#define CROMFS_PATITION_HEAD_SIZE 256
#define CROMFS_DIRENT_CACHE_SIZE  8

#define CROMFS_MAGIC   "CROMFSMG"

#define CROMFS_CT_ASSERT(name, x) \
    struct assert_##name {char ary[2 * (x) - 1];}

#define CROMFS_POS_ROOT  (0x0UL)
#define CROMFS_POS_ERROR (0x1UL)

typedef struct
{
    uint8_t magic[8];        /* CROMFS_MAGIC */
    uint32_t version;
    uint32_t partition_attr; /* expand, now reserved 0 */
    uint32_t partition_size; /* with partition head */
    uint32_t root_dir_pos;   /* root dir pos */
    uint32_t root_dir_size;
} partition_head_data;

typedef struct
{
    partition_head_data head;
    uint8_t padding[CROMFS_PATITION_HEAD_SIZE - sizeof(partition_head_data)];
} partition_head;

#define CROMFS_DIRENT_ATTR_DIR  0x1UL
#define CROMFS_DIRENT_ATTR_FILE 0x0UL

typedef struct
{
    uint16_t attr;              /* dir or file add other */
    uint16_t name_size;         /* name real size */
    uint32_t file_size;         /* file data size */
    uint32_t file_origin_size;  /* file size before compress */
    uint32_t parition_pos;      /* offset of data */
    uint8_t  name[0];           /* name data */
} cromfs_dirent;

#define CROMFS_ALIGN_SIZE_BIT  4
#define CROMFS_ALIGN_SIZE      (1UL << CROMFS_ALIGN_SIZE_BIT)  /* must be same as sizeof cromfs_dirent */
#define CROMFS_ALIGN_SIZE_MASK (CROMFS_ALIGN_SIZE - 1)

CROMFS_CT_ASSERT(align_size, CROMFS_ALIGN_SIZE == sizeof(cromfs_dirent));

typedef union
{
    cromfs_dirent dirent;
    uint8_t name[CROMFS_ALIGN_SIZE];
} cromfs_dirent_item;

/**********************************/

typedef struct
{
    rt_list_t list;
    uint32_t partition_pos;
    uint32_t size;
    uint8_t *buff;
} cromfs_dirent_cache;

typedef struct st_cromfs_info
{
    rt_device_t device;
    uint32_t partition_size;
    uint32_t bytes_per_sector;
    uint32_t (*read_bytes)(struct st_cromfs_info *ci, uint32_t pos, void *buf, uint32_t size);
    partition_head_data part_info;
    struct rt_mutex lock;
    struct cromfs_avl_struct *cromfs_avl_root;
    rt_list_t cromfs_dirent_cache_head;
    int cromfs_dirent_cache_nr;
} cromfs_info;

typedef struct
{
    uint32_t ref;
    uint32_t partition_pos;
    cromfs_info *ci;
    uint32_t size;
    uint8_t *buff;
    uint32_t partition_size;
    int data_valid;
} file_info;

/**********************************/

#define avl_key_t       uint32_t
#define AVL_EMPTY       (struct cromfs_avl_struct *)0
#define avl_maxheight   32
#define heightof(tree)  ((tree) == AVL_EMPTY ? 0 : (tree)->avl_height)

struct cromfs_avl_struct
{
    struct cromfs_avl_struct *avl_left;
    struct cromfs_avl_struct *avl_right;
    int    avl_height;
    avl_key_t avl_key;
    file_info *fi;
};

static void cromfs_avl_remove(struct cromfs_avl_struct *node_to_delete, struct cromfs_avl_struct **ptree);
static void cromfs_avl_insert(struct cromfs_avl_struct *new_node, struct cromfs_avl_struct **ptree);
static struct cromfs_avl_struct* cromfs_avl_find(avl_key_t key, struct cromfs_avl_struct *ptree);

static void cromfs_avl_rebalance(struct cromfs_avl_struct ***nodeplaces_ptr, int count)
{
    for (;count > 0; count--)
    {
        struct cromfs_avl_struct **nodeplace = *--nodeplaces_ptr;
        struct cromfs_avl_struct *node = *nodeplace;
        struct cromfs_avl_struct *nodeleft = node->avl_left;
        struct cromfs_avl_struct *noderight = node->avl_right;
        int heightleft = heightof(nodeleft);
        int heightright = heightof(noderight);
        if (heightright + 1 < heightleft)
        {
            struct cromfs_avl_struct * nodeleftleft = nodeleft->avl_left;
            struct cromfs_avl_struct * nodeleftright = nodeleft->avl_right;
            int heightleftright = heightof(nodeleftright);
            if (heightof(nodeleftleft) >= heightleftright)
            {
                node->avl_left = nodeleftright;
                nodeleft->avl_right = node;
                nodeleft->avl_height = 1 + (node->avl_height = 1 + heightleftright);
                *nodeplace = nodeleft;
            }
            else
            {
                nodeleft->avl_right = nodeleftright->avl_left;
                node->avl_left = nodeleftright->avl_right;
                nodeleftright->avl_left = nodeleft;
                nodeleftright->avl_right = node;
                nodeleft->avl_height = node->avl_height = heightleftright;
                nodeleftright->avl_height = heightleft;
                *nodeplace = nodeleftright;
            }
        }
        else if (heightleft + 1 < heightright)
        {
            struct cromfs_avl_struct *noderightright = noderight->avl_right;
            struct cromfs_avl_struct *noderightleft = noderight->avl_left;
            int heightrightleft = heightof(noderightleft);
            if (heightof(noderightright) >= heightrightleft)
            {
                node->avl_right = noderightleft;
                noderight->avl_left = node;
                noderight->avl_height = 1 + (node->avl_height = 1 + heightrightleft);
                *nodeplace = noderight;
            }
            else
            {
                noderight->avl_left = noderightleft->avl_right;
                node->avl_right = noderightleft->avl_left;
                noderightleft->avl_right = noderight;
                noderightleft->avl_left = node;
                noderight->avl_height = node->avl_height = heightrightleft;
                noderightleft->avl_height = heightright;
                *nodeplace = noderightleft;
            }
        }
        else {
            int height = (heightleft<heightright ? heightright : heightleft) + 1;
            if (height == node->avl_height)
            {
                break;
            }
            node->avl_height = height;
        }
    }
}

static void cromfs_avl_remove(struct cromfs_avl_struct *node_to_delete, struct cromfs_avl_struct **ptree)
{
    avl_key_t key = node_to_delete->avl_key;
    struct cromfs_avl_struct **nodeplace = ptree;
    struct cromfs_avl_struct **stack[avl_maxheight];
    uint32_t stack_count = 0;
    struct cromfs_avl_struct ***stack_ptr = &stack[0]; /* = &stack[stackcount] */
    struct cromfs_avl_struct **nodeplace_to_delete;
    for (;;)
    {
        struct cromfs_avl_struct *node = *nodeplace;
        if (node == AVL_EMPTY)
        {
            return;
        }
        *stack_ptr++ = nodeplace;
        stack_count++;
        if (key == node->avl_key)
        {
            break;
        }
        if (key < node->avl_key)
        {
            nodeplace = &node->avl_left;
        }
        else
        {
            nodeplace = &node->avl_right;
        }
    }
    nodeplace_to_delete = nodeplace;
    if (node_to_delete->avl_left == AVL_EMPTY)
    {
        *nodeplace_to_delete = node_to_delete->avl_right;
        stack_ptr--;
        stack_count--;
    }
    else
    {
        struct cromfs_avl_struct *** stack_ptr_to_delete = stack_ptr;
        struct cromfs_avl_struct ** nodeplace = &node_to_delete->avl_left;
        struct cromfs_avl_struct * node;
        for (;;)
        {
            node = *nodeplace;
            if (node->avl_right == AVL_EMPTY)
            {
                break;
            }
            *stack_ptr++ = nodeplace;
            stack_count++;
            nodeplace = &node->avl_right;
        }
        *nodeplace = node->avl_left;
        node->avl_left = node_to_delete->avl_left;
        node->avl_right = node_to_delete->avl_right;
        node->avl_height = node_to_delete->avl_height;
        *nodeplace_to_delete = node;
        *stack_ptr_to_delete = &node->avl_left;
    }
    cromfs_avl_rebalance(stack_ptr,stack_count);
}

static void cromfs_avl_insert(struct cromfs_avl_struct *new_node, struct cromfs_avl_struct **ptree)
{
    avl_key_t key = new_node->avl_key;
    struct cromfs_avl_struct **nodeplace = ptree;
    struct cromfs_avl_struct **stack[avl_maxheight];
    int stack_count = 0;
    struct cromfs_avl_struct ***stack_ptr = &stack[0]; /* = &stack[stackcount] */
    for (;;)
    {
        struct cromfs_avl_struct * node = *nodeplace;
        if (node == AVL_EMPTY)
        {
            break;
        }
        *stack_ptr++ = nodeplace;
        stack_count++;
        if (key < node->avl_key)
        {
            nodeplace = &node->avl_left;
        }
        else
        {
            nodeplace = &node->avl_right;
        }
    }
    new_node->avl_left = AVL_EMPTY;
    new_node->avl_right = AVL_EMPTY;
    new_node->avl_height = 1;
    *nodeplace = new_node;
    cromfs_avl_rebalance(stack_ptr,stack_count);
}

static struct cromfs_avl_struct* cromfs_avl_find(avl_key_t key, struct cromfs_avl_struct* ptree)
{
    for (;;)
    {
        if (ptree == AVL_EMPTY)
        {
            return (struct cromfs_avl_struct *)0;
        }
        if (key == ptree->avl_key)
        {
            break;
        }
        if (key < ptree->avl_key)
        {
            ptree = ptree->avl_left;
        }
        else
        {
            ptree = ptree->avl_right;
        }
    }
    return ptree;
}

/**********************************/

static uint32_t cromfs_read_bytes(cromfs_info *ci, uint32_t pos, void *buf, uint32_t size)
{
    if (pos >= ci->partition_size || pos + size > ci->partition_size)
    {
        return 0;
    }
    return ci->read_bytes(ci, pos, buf, size);
}

static uint32_t cromfs_noblk_read_bytes(cromfs_info *ci, uint32_t pos, void *buf, uint32_t size)
{
    uint32_t ret = 0;

    ret = rt_device_read(ci->device, pos, buf, size);
    if (ret != size)
    {
        return 0;
    }
    else
    {
        return ret;
    }
}

static uint32_t cromfs_blk_read_bytes(cromfs_info *ci, uint32_t pos, void *buf, uint32_t size)
{
    uint32_t ret = 0;
    uint32_t size_bak = size;
    uint32_t start_blk = 0;
    uint32_t end_blk = 0;
    uint32_t off_s = 0;
    uint32_t sector_nr = 0;
    uint8_t *block_buff = NULL;
    uint32_t ret_len = 0;

    if (!size || !buf)
    {
        return 0;
    }
    block_buff = (uint8_t *)malloc(2 * ci->bytes_per_sector);
    if (!block_buff)
    {
        return 0;
    }
    start_blk = pos / ci->bytes_per_sector;
    off_s = pos % ci->bytes_per_sector;
    end_blk = (pos + size - 1) / ci->bytes_per_sector;

    sector_nr = end_blk - start_blk;
    if (sector_nr < 2)
    {
        ret_len = rt_device_read(ci->device, start_blk, block_buff, sector_nr + 1);
        if (ret_len != sector_nr + 1)
        {
            goto end;
        }
        memcpy(buf, block_buff + off_s, size);
    }
    else
    {
        ret_len = rt_device_read(ci->device, start_blk, block_buff, 1);
        if (ret_len != 1)
        {
            goto end;
        }
        memcpy(buf, block_buff + off_s, ci->bytes_per_sector - off_s);
        off_s = (ci->bytes_per_sector - off_s);
        size -= off_s;
        sector_nr--;
        start_blk++;
        if (sector_nr)
        {
            ret_len = rt_device_read(ci->device, start_blk, (char*)buf + off_s, sector_nr);
            if (ret_len != sector_nr)
            {
                goto end;
            }
            start_blk += sector_nr;
            off_s += (sector_nr * ci->bytes_per_sector);
            size -= (sector_nr * ci->bytes_per_sector);
        }
        ret_len = rt_device_read(ci->device, start_blk, block_buff, 1);
        if (ret_len != 1)
        {
            goto end;
        }
        memcpy((char*)buf + off_s, block_buff, size);
    }
    ret = size_bak;
end:
    free(block_buff);
    return ret;
}

/**********************************/

static uint8_t *cromfs_dirent_cache_get(cromfs_info *ci, uint32_t pos, uint32_t size)
{
    rt_list_t *l = NULL;
    cromfs_dirent_cache *dir = NULL;
    uint32_t len = 0;

    /* find */
    for (l = ci->cromfs_dirent_cache_head.next; l != &ci->cromfs_dirent_cache_head; l = l->next)
    {
        dir = (cromfs_dirent_cache *)l;
        if (dir->partition_pos == pos)
        {
            RT_ASSERT(dir->size == size);
            rt_list_remove(l);
            rt_list_insert_after(&ci->cromfs_dirent_cache_head, l);
            return dir->buff;
        }
    }
    /* not found */
    if (ci->cromfs_dirent_cache_nr >= CROMFS_DIRENT_CACHE_SIZE)
    {
        l = ci->cromfs_dirent_cache_head.prev;
        dir = (cromfs_dirent_cache *)l;
        rt_list_remove(l);
        free(dir->buff);
        free(dir);
        ci->cromfs_dirent_cache_nr--;
    }
    dir = (cromfs_dirent_cache *)malloc(sizeof *dir);
    if (!dir)
    {
        return NULL;
    }
    dir->buff = (uint8_t *)malloc(size);
    if (!dir->buff)
    {
        free(dir);
        return NULL;
    }
    len = cromfs_read_bytes(ci, pos, dir->buff, size);
    if (len != size)
    {
        free(dir->buff);
        free(dir);
        return NULL;
    }
    rt_list_insert_after(&ci->cromfs_dirent_cache_head, (rt_list_t *)dir);
    ci->cromfs_dirent_cache_nr++;
    dir->partition_pos = pos;
    dir->size = size;
    return dir->buff;
}

static void cromfs_dirent_cache_destroy(cromfs_info *ci)
{
    rt_list_t *l = NULL;
    cromfs_dirent_cache *dir = NULL;

    while ((l = ci->cromfs_dirent_cache_head.next) != &ci->cromfs_dirent_cache_head)
    {
        rt_list_remove(l);
        dir = (cromfs_dirent_cache *)l;
        free(dir->buff);
        free(dir);
        ci->cromfs_dirent_cache_nr--;
    }
}

/**********************************/

static int dfs_cromfs_mount(struct dfs_filesystem *fs, unsigned long rwflag, const void *data)
{
    struct rt_device_blk_geometry geometry;
    uint32_t len = 0;
    cromfs_info *ci = NULL;

    ci = (cromfs_info *)malloc(sizeof *ci);
    if (!ci)
    {
        return -ENOMEM;
    }

    memset(ci, 0, sizeof *ci);
    ci->device = fs->dev_id;
    ci->partition_size = UINT32_MAX;
    if (ci->device->type == RT_Device_Class_Block)
    {
        rt_device_control(ci->device, RT_DEVICE_CTRL_BLK_GETGEOME, &geometry);
        ci->bytes_per_sector = geometry.bytes_per_sector;
        ci->read_bytes = cromfs_blk_read_bytes;
    }
    else
    {
        ci->read_bytes = cromfs_noblk_read_bytes;
    }

    len = cromfs_read_bytes(ci, 0, &ci->part_info, sizeof ci->part_info);
    if (len != sizeof ci->part_info ||
            memcmp(ci->part_info.magic, CROMFS_MAGIC, sizeof ci->part_info.magic) != 0)
    {
        free(ci);
        return -RT_ERROR;
    }
    ci->partition_size = ci->part_info.partition_size;
    fs->data = ci;

    rt_mutex_init(&ci->lock, "crom", RT_IPC_FLAG_FIFO);
    ci->cromfs_avl_root = NULL;

    rt_list_init(&ci->cromfs_dirent_cache_head);
    ci->cromfs_dirent_cache_nr = 0;

    return RT_EOK;
}

static int dfs_cromfs_unmount(struct dfs_filesystem *fs)
{
    rt_err_t result = RT_EOK;
    cromfs_info *ci = NULL;

    ci = (cromfs_info *)fs->data;

    result =  rt_mutex_take(&ci->lock, RT_WAITING_FOREVER);
    if (result != RT_EOK)
    {
        return -RT_ERROR;
    }

    cromfs_dirent_cache_destroy(ci);

    while (ci->cromfs_avl_root)
    {
        struct cromfs_avl_struct *node;
        file_info *fi = NULL;

        node = ci->cromfs_avl_root;
        fi = node->fi;
        cromfs_avl_remove(node, &ci->cromfs_avl_root);
        free(node);
        if (fi->buff)
        {
            free(fi->buff);
        }
        free(fi);
    }

    rt_mutex_detach(&ci->lock);

    free(ci);

    return RT_EOK;
}

static int dfs_cromfs_ioctl(struct dfs_file *file, int cmd, void *args)
{
    return -EIO;
}

static uint32_t cromfs_lookup(cromfs_info *ci, const char *path, int* is_dir, uint32_t *size, uint32_t *osize)
{
    uint32_t cur_size = 0, cur_pos = 0, cur_osize = 0;
    const char *subpath = NULL, *subpath_end = NULL;
    void *di_mem = NULL;
    int isdir = 0;

    if (path[0] == '\0')
    {
        return CROMFS_POS_ERROR;
    }

    cur_size = ci->part_info.root_dir_size;
    cur_osize = 0;
    cur_pos = ci->part_info.root_dir_pos;
    isdir = 1;

    subpath_end = path;
    while (1)
    {
        cromfs_dirent_item *di_iter = NULL;
        int found = 0;

        /* skip /// */
        while (*subpath_end && *subpath_end == '/')
        {
            subpath_end++;
        }
        subpath = subpath_end;
        while ((*subpath_end != '/') && *subpath_end)
        {
            subpath_end++;
        }
        if (*subpath == '\0')
        {
            break;
        }

        /* if not dir or empty dir, error */
        if (!isdir || !cur_size)
        {
            return CROMFS_POS_ERROR;
        }

        /* find subpath */
        di_mem = cromfs_dirent_cache_get(ci, cur_pos, cur_size);
        if (!di_mem)
        {
            return CROMFS_POS_ERROR;
        }

        found = 0;
        di_iter = (cromfs_dirent_item *)di_mem;
        while (1)
        {
            uint32_t name_len = subpath_end - subpath;
            uint32_t name_block = 0;

            if (di_iter->dirent.name_size == name_len)
            {
                if (memcmp(di_iter->dirent.name, subpath, name_len) == 0)
                {
                    found = 1;
                    cur_size = di_iter->dirent.file_size;
                    cur_osize = di_iter->dirent.file_origin_size;
                    cur_pos = di_iter->dirent.parition_pos;
                    if (di_iter->dirent.attr == CROMFS_DIRENT_ATTR_DIR)
                    {
                        isdir = 1;
                    }
                    else
                    {
                        isdir = 0;
                    }
                    break;
                }
            }
            name_block = (di_iter->dirent.name_size + CROMFS_ALIGN_SIZE_MASK) >> CROMFS_ALIGN_SIZE_BIT;
            di_iter += (1 + name_block);
            if ((uint32_t)di_iter - (uint32_t)di_mem >= cur_size)
            {
                break;
            }
        }
        if (!found)
        {
            return CROMFS_POS_ERROR;
        }
    }
    *size = cur_size;
    *osize = cur_osize;
    *is_dir = isdir;
    return cur_pos;
}

static uint32_t dfs_cromfs_lookup(cromfs_info *ci, const char *path, int* is_dir, uint32_t *size, uint32_t *osize)
{
    rt_err_t result = RT_EOK;
    uint32_t ret = 0;

    result =  rt_mutex_take(&ci->lock, RT_WAITING_FOREVER);
    if (result != RT_EOK)
    {
        return CROMFS_POS_ERROR;
    }
    ret = cromfs_lookup(ci, path, is_dir, size, osize);
    rt_mutex_release(&ci->lock);
    return ret;
}

static int fill_file_data(file_info *fi)
{
    int ret = -1;
    cromfs_info *ci = NULL;
    void *compressed_file_buff = NULL;
    uint32_t size = 0, osize = 0;

    if (!fi->data_valid)
    {
        RT_ASSERT(fi->buff != NULL);

        ci = fi->ci;
        osize = fi->size;
        size = fi->partition_size;

        compressed_file_buff = (void *)malloc(size);
        if (!compressed_file_buff)
        {
            goto end;
        }
        if (cromfs_read_bytes(ci, fi->partition_pos, compressed_file_buff, size) != size)
        {
            goto end;
        }
        if (uncompress((uint8_t *)fi->buff, (uLongf *)&osize, (uint8_t *)compressed_file_buff, size) != Z_OK)
        {
            goto end;
        }
        fi->data_valid = 1;
    }
    ret = 0;
end:
    if (compressed_file_buff)
    {
        free(compressed_file_buff);
    }
    return ret;
}

static int dfs_cromfs_read(struct dfs_file *file, void *buf, size_t count)
{
    rt_err_t result = RT_EOK;
    struct dfs_filesystem *fs = NULL;
    file_info *fi = NULL;
    cromfs_info *ci = NULL;
    uint32_t length = 0;

    fs = (struct dfs_filesystem *)file->vnode->fs;
    ci = (cromfs_info *)fs->data;
    fi = (file_info *)file->vnode->data;

    if (count < file->vnode->size - file->pos)
    {
        length = count;
    }
    else
    {
        length = file->vnode->size - file->pos;
    }

    if (length > 0)
    {
        RT_ASSERT(fi->size != 0);

        if (fi->buff)
        {
            int fill_ret = 0;

            result =  rt_mutex_take(&ci->lock, RT_WAITING_FOREVER);
            if (result != RT_EOK)
            {
                return 0;
            }
            fill_ret = fill_file_data(fi);
            rt_mutex_release(&ci->lock);
            if (fill_ret < 0)
            {
                return 0;
            }

            memcpy(buf, fi->buff + file->pos, length);
        }
        else
        {
            void *di_mem = NULL;

            result =  rt_mutex_take(&ci->lock, RT_WAITING_FOREVER);
            if (result != RT_EOK)
            {
                return 0;
            }
            di_mem = cromfs_dirent_cache_get(ci, fi->partition_pos, fi->size);
            if (di_mem)
            {
                memcpy(buf, (char*)di_mem + file->pos, length);
            }
            rt_mutex_release(&ci->lock);
            if (!di_mem)
            {
                return 0;
            }
        }
        /* update file current position */
        file->pos += length;
    }

    return length;
}

static int dfs_cromfs_lseek(struct dfs_file *file, off_t offset)
{
    if (offset <= file->vnode->size)
    {
        file->pos = offset;
        return file->pos;
    }
    return -EIO;
}

static file_info *get_file_info(cromfs_info *ci, uint32_t partition_pos, int inc_ref)
{
    struct cromfs_avl_struct* node = cromfs_avl_find(partition_pos, ci->cromfs_avl_root);

    if (node)
    {
        if (inc_ref)
        {
            node->fi->ref++;
        }
        return node->fi;
    }
    return NULL;
}

static file_info *inset_file_info(cromfs_info *ci, uint32_t partition_pos, int is_dir, uint32_t size, uint32_t osize)
{
    file_info *fi = NULL;
    void *file_buff = NULL;
    struct cromfs_avl_struct *node = NULL;

    fi = (file_info *)malloc(sizeof *fi);
    if (!fi)
    {
        goto err;
    }
    fi->partition_pos = partition_pos;
    fi->ci = ci;
    if (is_dir)
    {
        fi->size = size;
    }
    else
    {
        fi->size = osize;
        fi->partition_size = size;
        fi->data_valid = 0;
        if (osize)
        {
            file_buff = (void *)malloc(osize);
            if (!file_buff)
            {
                goto err;
            }
        }
    }
    fi->buff = file_buff;
    fi->ref = 1;

    node = (struct cromfs_avl_struct *)malloc(sizeof *node);
    if (!node)
    {
        goto err;
    }
    node->avl_key = partition_pos;
    node->fi = fi;
    cromfs_avl_insert(node, &ci->cromfs_avl_root);
    return fi;
err:
    if (file_buff)
    {
        free(file_buff);
    }
    if (fi)
    {
        free(fi);
    }
    return NULL;
}

static void deref_file_info(cromfs_info *ci, uint32_t partition_pos)
{
    struct cromfs_avl_struct* node = cromfs_avl_find(partition_pos, ci->cromfs_avl_root);
    file_info *fi = NULL;

    if (node)
    {
        node->fi->ref--;
        if (node->fi->ref == 0)
        {
            fi = node->fi;
            cromfs_avl_remove(node, &ci->cromfs_avl_root);
            free(node);
            if (fi->buff)
            {
                free(fi->buff);
            }
            free(fi);
        }
    }
}

static int dfs_cromfs_close(struct dfs_file *file)
{
    file_info *fi = NULL;
    struct dfs_filesystem *fs = NULL;
    cromfs_info *ci = NULL;
    rt_err_t result = 0;

    RT_ASSERT(file->vnode->ref_count > 0);
    if (file->vnode->ref_count > 1)
    {
        return 0;
    }

    fi = (file_info *)file->vnode->data;
    fs = (struct dfs_filesystem *)file->vnode->fs;
    ci = (cromfs_info *)fs->data;

    result =  rt_mutex_take(&ci->lock, RT_WAITING_FOREVER);
    if (result != RT_EOK)
    {
        return -RT_ERROR;
    }
    deref_file_info(ci, fi->partition_pos);
    rt_mutex_release(&ci->lock);
    file->vnode->data = NULL;
    return RT_EOK;
}

static int dfs_cromfs_open(struct dfs_file *file)
{
    int ret = 0;
    struct dfs_filesystem *fs = NULL;
    file_info *fi = NULL;
    cromfs_info *ci = NULL;
    uint32_t file_pos = 0;
    uint32_t size = 0, osize = 0;
    int is_dir = 0;
    rt_err_t result = RT_EOK;

    if (file->flags & (O_CREAT | O_WRONLY | O_APPEND | O_TRUNC | O_RDWR))
    {
        return -EINVAL;
    }

    RT_ASSERT(file->vnode->ref_count > 0);
    if (file->vnode->ref_count > 1)
    {
        if (file->vnode->type == FT_DIRECTORY
                && !(file->flags & O_DIRECTORY))
        {
            return -ENOENT;
        }
        file->pos = 0;
        return 0;
    }

    fs = file->vnode->fs;
    ci = (cromfs_info *)fs->data;

    file_pos = dfs_cromfs_lookup(ci, file->vnode->path, &is_dir, &size, &osize);
    if (file_pos == CROMFS_POS_ERROR)
    {
        ret = -ENOENT;
        goto end;
    }

    /* entry is a directory file type */
    if (is_dir)
    {
        if (!(file->flags & O_DIRECTORY))
        {
            ret = -ENOENT;
            goto end;
        }
        file->vnode->type = FT_DIRECTORY;
    }
    else
    {
        /* entry is a file, but open it as a directory */
        if (file->flags & O_DIRECTORY)
        {
            ret = -ENOENT;
            goto end;
        }
        file->vnode->type = FT_REGULAR;
    }

    result =  rt_mutex_take(&ci->lock, RT_WAITING_FOREVER);
    if (result != RT_EOK)
    {
        ret = -EINTR;
        goto end;
    }

    fi = get_file_info(ci, file_pos, 1);
    if (!fi)
    {
        fi = inset_file_info(ci, file_pos, is_dir, size, osize);
    }
    rt_mutex_release(&ci->lock);
    if (!fi)
    {
        ret = -ENOENT;
        goto end;
    }

    file->vnode->data = fi;
    if (is_dir)
    {
        file->vnode->size = size;
    }
    else
    {
        file->vnode->size = osize;
    }
    file->pos = 0;

    ret = RT_EOK;
end:
    return ret;
}

static int dfs_cromfs_stat(struct dfs_filesystem *fs, const char *path, struct stat *st)
{
    uint32_t size = 0, osize = 0;
    int is_dir = 0;
    cromfs_info *ci = NULL;
    uint32_t file_pos = 0;

    ci = (cromfs_info *)fs->data;

    file_pos = dfs_cromfs_lookup(ci, path, &is_dir, &size, &osize);
    if (file_pos == CROMFS_POS_ERROR)
    {
        return -ENOENT;
    }

    st->st_dev = 0;
    st->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH |
        S_IWUSR | S_IWGRP | S_IWOTH;

    if (is_dir)
    {
        st->st_mode &= ~S_IFREG;
        st->st_mode |= S_IFDIR | S_IXUSR | S_IXGRP | S_IXOTH;
        st->st_size = size;
    }
    else
    {
        st->st_size = osize;
    }

    st->st_mtime = 0;

    return RT_EOK;
}

static int dfs_cromfs_getdents(struct dfs_file *file, struct dirent *dirp, uint32_t count)
{
    uint32_t index = 0;
    uint8_t *name = NULL;
    struct dirent *d = NULL;
    file_info *fi = NULL;
    cromfs_info *ci = NULL;
    cromfs_dirent_item *dirent = NULL, *sub_dirent = NULL;
    void *di_mem = NULL;
    rt_err_t result = RT_EOK;

    fi = (file_info *)file->vnode->data;
    ci = fi->ci;

    RT_ASSERT(fi->buff == NULL);

    if (!fi->size)
    {
        return -EINVAL;
    }

    dirent = (cromfs_dirent_item *)malloc(fi->size);
    if (!dirent)
    {
        return -ENOMEM;
    }

    result =  rt_mutex_take(&ci->lock, RT_WAITING_FOREVER);
    if (result != RT_EOK)
    {
        free(dirent);
        return -EINTR;
    }
    di_mem = cromfs_dirent_cache_get(ci, fi->partition_pos, fi->size);
    if (di_mem)
    {
        memcpy(dirent, di_mem, fi->size);
    }
    rt_mutex_release(&ci->lock);
    if (!di_mem)
    {
        free(dirent);
        return -ENOMEM;
    }

    /* make integer count */
    count = (count / sizeof(struct dirent));
    if (count == 0)
    {
        free(dirent);
        return -EINVAL;
    }

    for (index = 0; index < count && file->pos < file->vnode->size; index++)
    {
        uint32_t name_size = 0;

        d = dirp + index;
        sub_dirent = &dirent[file->pos >> CROMFS_ALIGN_SIZE_BIT];
        name = sub_dirent->dirent.name;

        /* fill dirent */
        if (sub_dirent->dirent.attr == CROMFS_DIRENT_ATTR_DIR)
        {
            d->d_type = DT_DIR;
        }
        else
        {
            d->d_type = DT_REG;
        }

        d->d_namlen = sub_dirent->dirent.name_size;
        d->d_reclen = (rt_uint16_t)sizeof(struct dirent);
        memcpy(d->d_name, (char *)name, sub_dirent->dirent.name_size);
        d->d_name[sub_dirent->dirent.name_size] = '\0';

        name_size = (sub_dirent->dirent.name_size + CROMFS_ALIGN_SIZE_MASK) & ~CROMFS_ALIGN_SIZE_MASK;
        /* move to next position */
        file->pos += (name_size + sizeof *sub_dirent);
    }

    free(dirent);

    return index * sizeof(struct dirent);
}

static const struct dfs_file_ops _crom_fops =
{
    dfs_cromfs_open,
    dfs_cromfs_close,
    dfs_cromfs_ioctl,
    dfs_cromfs_read,
    NULL,
    NULL,
    dfs_cromfs_lseek,
    dfs_cromfs_getdents,
};

static const struct dfs_filesystem_ops _cromfs =
{
    "crom",
    DFS_FS_FLAG_DEFAULT,
    &_crom_fops,

    dfs_cromfs_mount,
    dfs_cromfs_unmount,
    NULL,
    NULL,

    NULL,
    dfs_cromfs_stat,
    NULL,
};

int dfs_cromfs_init(void)
{
    /* register crom file system */
    dfs_register(&_cromfs);
    return 0;
}
INIT_COMPONENT_EXPORT(dfs_cromfs_init);
