/*
 * This file is part of the MicroPython project, http://micropython.org/
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2022 Damien P. George
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

// ROMFS filesystem format
// =======================
//
// ROMFS is a flexible and extensible filesystem format designed to represent a
// directory hierarchy with files, where those files are read-only and their data
// can be memory mapped.
//
// Concepts:
// - varuint: An unsigned integer that is encoded in a variable number of bytes. It is
//   stored big-endian with the high bit of the byte set if there are following bytes.
// - record: A variable sized element with a type.  It is stored as two varuint's and then
//   a payload.  The first varuint is the record kind and the second varuint is the
//   payload length (which may be zero bytes long).
//
// A ROMFS filesystem is a record with record kind 0x14a6b1, chosen so the encoded value
// is 0xd2-0xcd-0x31 which is "RM1" with the first two bytes having their high bit set.
// If the ROMFS record's payload is non-empty then it contains records.
//
// Record types:
// - 0 = unused, can be used to detect corruption of the filesystem.
// - 1 = padding/comments, can contain any data in their payload.
// - 2 = verbatim data, used to store file data.
// - 3 = indirect data, pointer to offset within the ROMFS payload.
// - 4 = a directory: payload contains a varuint which is the length of the directory
//       name in bytes, then the name, then optional nested records for the contents
//       of the directory (including optional metadata).
// - 5 = a file: payload contains a varuint which is the length of the filename in bytes
//       then the name, then optional nested records.
//
// Remarks:
// - A varuint can be padded if needed by prepending with one or more 0x80 bytes.  This
//   padding does not change any semantics.
// - The size of the ROMFS record (including kind and length and payload) must be a
//   multiple of 2 (because it's not possible to add a padding record of one byte).
// - File data can be optionally aligned using padding records and/or indirect data
//   records.
// - There is no limit to the size of directory/file names or file data.
//
// Unknown record types must be skipped over.  They may in the future add optional
// features, while still retaining backwards compatibility.  Such features may be:
// - Alignment requirements of the ROMFS record.
// - Timestamps on directories/files.
// - A precomputed hash of a file, or other metadata.
// - An optimised lookup table indexing the directory hierarchy.

#include <string.h>

#include "py/bc.h"
#include "py/runtime.h"
#include "py/mperrno.h"
#include "extmod/vfs.h"
#include "extmod/vfs_rom.h"

#if MICROPY_VFS_ROM

#define ROMFS_SIZE_MIN (4)
#define ROMFS_HEADER_BYTE0 (0x80 | 'R')
#define ROMFS_HEADER_BYTE1 (0x80 | 'M')
#define ROMFS_HEADER_BYTE2 (0x00 | '1')

// Values for `record_kind_t`.
#define ROMFS_RECORD_KIND_UNUSED (0)
#define ROMFS_RECORD_KIND_PADDING (1)
#define ROMFS_RECORD_KIND_DATA_VERBATIM (2)
#define ROMFS_RECORD_KIND_DATA_POINTER (3)
#define ROMFS_RECORD_KIND_DIRECTORY (4)
#define ROMFS_RECORD_KIND_FILE (5)
#define ROMFS_RECORD_KIND_FILESYSTEM (0x14a6b1)

typedef mp_uint_t record_kind_t;

struct _mp_obj_vfs_rom_t {
    mp_obj_base_t base;
    mp_obj_t memory;
    const uint8_t *filesystem;
    const uint8_t *filesystem_end;
};

// Returns 0 for success, -1 for failure.
static int mp_decode_uint_checked(const uint8_t **ptr, const uint8_t *ptr_max, mp_uint_t *value_out) {
    mp_uint_t unum = 0;
    byte val;
    const uint8_t *p = *ptr;
    do {
        if (p >= ptr_max) {
            return -1;
        }
        val = *p++;
        unum = (unum << 7) | (val & 0x7f);
    } while ((val & 0x80) != 0);
    *ptr = p;
    *value_out = unum;
    return 0;
}

static record_kind_t extract_record(const uint8_t **fs, const uint8_t **fs_next, const uint8_t *fs_max) {
    mp_uint_t record_kind;
    if (mp_decode_uint_checked(fs, fs_max, &record_kind) != 0) {
        return ROMFS_RECORD_KIND_UNUSED;
    }
    mp_uint_t record_len;
    if (mp_decode_uint_checked(fs, fs_max, &record_len) != 0) {
        return ROMFS_RECORD_KIND_UNUSED;
    }
    *fs_next = *fs + record_len;
    return record_kind;
}

// Returns 0 for success, a negative integer for failure.
static int extract_data(mp_obj_vfs_rom_t *self, const uint8_t *fs, const uint8_t *fs_top, size_t *size_out, const uint8_t **data_out) {
    while (fs < fs_top) {
        const uint8_t *fs_next;
        record_kind_t record_kind = extract_record(&fs, &fs_next, fs_top);
        if (record_kind == ROMFS_RECORD_KIND_UNUSED) {
            // Corrupt filesystem.
            break;
        } else if (record_kind == ROMFS_RECORD_KIND_DATA_VERBATIM) {
            // Verbatim data.
            if (size_out != NULL) {
                *size_out = fs_next - fs;
                *data_out = fs;
            }
            return 0;
        } else if (record_kind == ROMFS_RECORD_KIND_DATA_POINTER) {
            // Pointer to data.
            mp_uint_t size;
            if (mp_decode_uint_checked(&fs, fs_next, &size) != 0) {
                break;
            }
            mp_uint_t offset;
            if (mp_decode_uint_checked(&fs, fs_next, &offset) != 0) {
                break;
            }
            if (size_out != NULL) {
                *size_out = size;
                *data_out = self->filesystem + offset;
            }
            return 0;
        } else {
            // Skip this record.
            fs = fs_next;
        }
    }
    return -MP_EIO;
}

// Searches for `path` in the filesystem.
// `path` must be null-terminated.
mp_import_stat_t mp_vfs_rom_search_filesystem(mp_obj_vfs_rom_t *self, const char *path, size_t *size_out, const uint8_t **data_out) {
    const uint8_t *fs = self->filesystem;
    const uint8_t *fs_top = self->filesystem_end;
    size_t path_len = strlen(path);
    if (*path == '/') {
        // An optional slash at the start of the path enters the top-level filesystem.
        ++path;
        --path_len;
    }
    while (path_len > 0 && fs < fs_top) {
        const uint8_t *fs_next;
        record_kind_t record_kind = extract_record(&fs, &fs_next, fs_top);
        if (record_kind == ROMFS_RECORD_KIND_UNUSED) {
            // Corrupt filesystem.
            return MP_IMPORT_STAT_NO_EXIST;
        } else if (record_kind == ROMFS_RECORD_KIND_DIRECTORY || record_kind == ROMFS_RECORD_KIND_FILE) {
            // A directory or file record.
            mp_uint_t name_len;
            if (mp_decode_uint_checked(&fs, fs_next, &name_len) != 0) {
                // Corrupt filesystem.
                return MP_IMPORT_STAT_NO_EXIST;
            }
            if ((name_len == path_len
                 || (name_len < path_len && path[name_len] == '/'))
                && memcmp(path, fs, name_len) == 0) {
                // Name matches, so enter this record.
                fs += name_len;
                fs_top = fs_next;
                path += name_len;
                path_len -= name_len;
                if (record_kind == ROMFS_RECORD_KIND_DIRECTORY) {
                    // Continue searching in this directory.
                    if (*path == '/') {
                        ++path;
                        --path_len;
                    }
                } else {
                    // Return this file.
                    if (path_len != 0) {
                        return MP_IMPORT_STAT_NO_EXIST;
                    }
                    if (extract_data(self, fs, fs_top, size_out, data_out) != 0) {
                        // Corrupt filesystem.
                        return MP_IMPORT_STAT_NO_EXIST;
                    }
                    return MP_IMPORT_STAT_FILE;
                }
            } else {
                // Skip this directory/file record.
                fs = fs_next;
            }
        } else {
            // Skip this record.
            fs = fs_next;
        }
    }
    if (path_len == 0) {
        if (size_out != NULL) {
            *size_out = fs_top - fs;
            *data_out = fs;
        }
        return MP_IMPORT_STAT_DIR;
    }
    return MP_IMPORT_STAT_NO_EXIST;
}

static mp_obj_t vfs_rom_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
    mp_arg_check_num(n_args, n_kw, 1, 1, false);

    mp_obj_vfs_rom_t *self = m_new_obj(mp_obj_vfs_rom_t);
    self->base.type = type;
    self->memory = args[0];

    // Get the ROMFS memory region.
    mp_buffer_info_t bufinfo;
    mp_get_buffer_raise(self->memory, &bufinfo, MP_BUFFER_READ);
    if (bufinfo.len < ROMFS_SIZE_MIN) {
        mp_raise_OSError(MP_ENODEV);
    }
    self->filesystem = bufinfo.buf;

    // Verify it is a ROMFS.
    if (!(self->filesystem[0] == ROMFS_HEADER_BYTE0
          && self->filesystem[1] == ROMFS_HEADER_BYTE1
          && self->filesystem[2] == ROMFS_HEADER_BYTE2)) {
        mp_raise_OSError(MP_ENODEV);
    }

    // The ROMFS is a record itself, so enter into it and compute its limit.
    record_kind_t record_kind = extract_record(&self->filesystem, &self->filesystem_end, self->filesystem + bufinfo.len);
    if (record_kind != ROMFS_RECORD_KIND_FILESYSTEM) {
        mp_raise_OSError(MP_ENODEV);
    }

    // Check the filesystem is within the limits of the input buffer.
    if (self->filesystem_end > (const uint8_t *)bufinfo.buf + bufinfo.len) {
        mp_raise_OSError(MP_ENODEV);
    }

    return MP_OBJ_FROM_PTR(self);
}

static mp_obj_t vfs_rom_mount(mp_obj_t self_in, mp_obj_t readonly, mp_obj_t mkfs) {
    (void)self_in;
    (void)readonly;
    if (mp_obj_is_true(mkfs)) {
        mp_raise_OSError(MP_EPERM);
    }
    return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_3(vfs_rom_mount_obj, vfs_rom_mount);

// mp_vfs_rom_file_open is implemented in vfs_rom_file.c.
static MP_DEFINE_CONST_FUN_OBJ_3(vfs_rom_open_obj, mp_vfs_rom_file_open);

static mp_obj_t vfs_rom_chdir(mp_obj_t self_in, mp_obj_t path_in) {
    mp_obj_vfs_rom_t *self = MP_OBJ_TO_PTR(self_in);
    const char *path = mp_vfs_rom_get_path_str(self, path_in);
    if (path[0] == '/' && path[1] == '\0') {
        // Allow chdir to the root of the filesystem.
    } else {
        // Don't allow chdir to any subdirectory (not currently implemented).
        mp_raise_OSError(MP_EOPNOTSUPP);
    }
    return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_2(vfs_rom_chdir_obj, vfs_rom_chdir);

static mp_obj_t vfs_rom_getcwd(mp_obj_t self_in) {
    (void)self_in;
    // The current directory is always the root of the ROMFS.
    return MP_OBJ_NEW_QSTR(MP_QSTR_);
}
static MP_DEFINE_CONST_FUN_OBJ_1(vfs_rom_getcwd_obj, vfs_rom_getcwd);

typedef struct _vfs_rom_ilistdir_it_t {
    mp_obj_base_t base;
    mp_fun_1_t iternext;
    mp_obj_vfs_rom_t *vfs_rom;
    bool is_str;
    const uint8_t *index;
    const uint8_t *index_top;
} vfs_rom_ilistdir_it_t;

static mp_obj_t vfs_rom_ilistdir_it_iternext(mp_obj_t self_in) {
    vfs_rom_ilistdir_it_t *self = MP_OBJ_TO_PTR(self_in);

    while (self->index < self->index_top) {
        const uint8_t *index_next;
        record_kind_t record_kind = extract_record(&self->index, &index_next, self->index_top);
        uint32_t type;
        mp_uint_t name_len;
        size_t data_len;
        if (record_kind == ROMFS_RECORD_KIND_UNUSED) {
            // Corrupt filesystem.
            self->index = self->index_top;
            break;
        } else if (record_kind == ROMFS_RECORD_KIND_DIRECTORY || record_kind == ROMFS_RECORD_KIND_FILE) {
            // A directory or file record.
            if (mp_decode_uint_checked(&self->index, index_next, &name_len) != 0) {
                // Corrupt filesystem.
                self->index = self->index_top;
                break;
            }
            if (record_kind == ROMFS_RECORD_KIND_DIRECTORY) {
                // A directory.
                type = MP_S_IFDIR;
                data_len = index_next - self->index - name_len;
            } else {
                // A file.
                type = MP_S_IFREG;
                const uint8_t *data_value;
                if (extract_data(self->vfs_rom, self->index + name_len, index_next, &data_len, &data_value) != 0) {
                    // Corrupt filesystem.
                    break;
                }
            }
        } else {
            // Skip this record.
            self->index = index_next;
            continue;
        }

        const uint8_t *name_str = self->index;
        self->index = index_next;

        // Make 4-tuple with info about this entry: (name, attr, inode, size)
        mp_obj_tuple_t *t = MP_OBJ_TO_PTR(mp_obj_new_tuple(4, NULL));

        if (self->is_str) {
            t->items[0] = mp_obj_new_str((const char *)name_str, name_len);
        } else {
            t->items[0] = mp_obj_new_bytes(name_str, name_len);
        }

        t->items[1] = MP_OBJ_NEW_SMALL_INT(type);
        t->items[2] = MP_OBJ_NEW_SMALL_INT(0);
        t->items[3] = mp_obj_new_int(data_len);

        return MP_OBJ_FROM_PTR(t);
    }

    return MP_OBJ_STOP_ITERATION;
}

static mp_obj_t vfs_rom_ilistdir(mp_obj_t self_in, mp_obj_t path_in) {
    mp_obj_vfs_rom_t *self = MP_OBJ_TO_PTR(self_in);
    vfs_rom_ilistdir_it_t *iter = m_new_obj(vfs_rom_ilistdir_it_t);
    iter->base.type = &mp_type_polymorph_iter;
    iter->iternext = vfs_rom_ilistdir_it_iternext;
    iter->vfs_rom = self;
    iter->is_str = mp_obj_get_type(path_in) == &mp_type_str;
    const char *path = mp_vfs_rom_get_path_str(self, path_in);
    size_t size;
    if (mp_vfs_rom_search_filesystem(self, path, &size, &iter->index) != MP_IMPORT_STAT_DIR) {
        mp_raise_OSError(MP_ENOENT);
    }
    iter->index_top = iter->index + size;
    return MP_OBJ_FROM_PTR(iter);
}
static MP_DEFINE_CONST_FUN_OBJ_2(vfs_rom_ilistdir_obj, vfs_rom_ilistdir);

static mp_obj_t vfs_rom_stat(mp_obj_t self_in, mp_obj_t path_in) {
    mp_obj_vfs_rom_t *self = MP_OBJ_TO_PTR(self_in);
    const char *path = mp_vfs_rom_get_path_str(self, path_in);
    size_t file_size;
    const uint8_t *file_data;
    mp_import_stat_t stat = mp_vfs_rom_search_filesystem(self, path, &file_size, &file_data);
    if (stat == MP_IMPORT_STAT_NO_EXIST) {
        mp_raise_OSError(MP_ENOENT);
    }
    mp_obj_tuple_t *t = MP_OBJ_TO_PTR(mp_obj_new_tuple(10, NULL));
    t->items[0] = MP_OBJ_NEW_SMALL_INT(stat == MP_IMPORT_STAT_FILE ? MP_S_IFREG : MP_S_IFDIR); // st_mode
    t->items[1] = MP_OBJ_NEW_SMALL_INT(0); // st_ino
    t->items[2] = MP_OBJ_NEW_SMALL_INT(0); // st_dev
    t->items[3] = MP_OBJ_NEW_SMALL_INT(0); // st_nlink
    t->items[4] = MP_OBJ_NEW_SMALL_INT(0); // st_uid
    t->items[5] = MP_OBJ_NEW_SMALL_INT(0); // st_gid
    t->items[6] = MP_OBJ_NEW_SMALL_INT(file_size); // st_size
    t->items[7] = MP_OBJ_NEW_SMALL_INT(0); // st_atime
    t->items[8] = MP_OBJ_NEW_SMALL_INT(0); // st_mtime
    t->items[9] = MP_OBJ_NEW_SMALL_INT(0); // st_ctime
    return MP_OBJ_FROM_PTR(t);
}
static MP_DEFINE_CONST_FUN_OBJ_2(vfs_rom_stat_obj, vfs_rom_stat);

static mp_obj_t vfs_rom_statvfs(mp_obj_t self_in, mp_obj_t path_in) {
    mp_obj_vfs_rom_t *self = MP_OBJ_TO_PTR(self_in);
    (void)path_in;
    size_t filesystem_len = self->filesystem_end - self->filesystem;
    mp_obj_tuple_t *t = MP_OBJ_TO_PTR(mp_obj_new_tuple(10, NULL));
    t->items[0] = MP_OBJ_NEW_SMALL_INT(1); // f_bsize
    t->items[1] = MP_OBJ_NEW_SMALL_INT(0); // f_frsize
    t->items[2] = mp_obj_new_int_from_uint(filesystem_len); // f_blocks
    t->items[3] = MP_OBJ_NEW_SMALL_INT(0); // f_bfree
    t->items[4] = MP_OBJ_NEW_SMALL_INT(0); // f_bavail
    t->items[5] = MP_OBJ_NEW_SMALL_INT(0); // f_files
    t->items[6] = MP_OBJ_NEW_SMALL_INT(0); // f_ffree
    t->items[7] = MP_OBJ_NEW_SMALL_INT(0); // f_favail
    t->items[8] = MP_OBJ_NEW_SMALL_INT(0); // f_flags
    t->items[9] = MP_OBJ_NEW_SMALL_INT(32767); // f_namemax
    return MP_OBJ_FROM_PTR(t);
}
static MP_DEFINE_CONST_FUN_OBJ_2(vfs_rom_statvfs_obj, vfs_rom_statvfs);

static const mp_rom_map_elem_t vfs_rom_locals_dict_table[] = {
    { MP_ROM_QSTR(MP_QSTR_mount), MP_ROM_PTR(&vfs_rom_mount_obj) },
    { MP_ROM_QSTR(MP_QSTR_umount), MP_ROM_PTR(&mp_identity_obj) },
    { MP_ROM_QSTR(MP_QSTR_open), MP_ROM_PTR(&vfs_rom_open_obj) },

    { MP_ROM_QSTR(MP_QSTR_chdir), MP_ROM_PTR(&vfs_rom_chdir_obj) },
    { MP_ROM_QSTR(MP_QSTR_getcwd), MP_ROM_PTR(&vfs_rom_getcwd_obj) },
    { MP_ROM_QSTR(MP_QSTR_ilistdir), MP_ROM_PTR(&vfs_rom_ilistdir_obj) },
    { MP_ROM_QSTR(MP_QSTR_stat), MP_ROM_PTR(&vfs_rom_stat_obj) },
    { MP_ROM_QSTR(MP_QSTR_statvfs), MP_ROM_PTR(&vfs_rom_statvfs_obj) },
};
static MP_DEFINE_CONST_DICT(vfs_rom_locals_dict, vfs_rom_locals_dict_table);

static mp_import_stat_t mp_vfs_rom_import_stat(void *self_in, const char *path) {
    mp_obj_vfs_rom_t *self = MP_OBJ_TO_PTR(self_in);
    return mp_vfs_rom_search_filesystem(self, path, NULL, NULL);
}

static const mp_vfs_proto_t vfs_rom_proto = {
    .import_stat = mp_vfs_rom_import_stat,
};

MP_DEFINE_CONST_OBJ_TYPE(
    mp_type_vfs_rom,
    MP_QSTR_VfsRom,
    MP_TYPE_FLAG_NONE,
    make_new, vfs_rom_make_new,
    protocol, &vfs_rom_proto,
    locals_dict, &vfs_rom_locals_dict
    );

#endif // MICROPY_VFS_ROM
