#ifndef NO_LUA
#include "gm9internalfs.h"
#include "fs.h"
#include "ui.h"
#include "utils.h"
#include "sha.h"
#include "nand.h"
#include "language.h"
#include "hid.h"
#include "game.h"
#include "gamecart.h"

#define _MAX_FOR_DEPTH  16

static u8 no_data_hash_256[32] = { SHA256_EMPTY_HASH };
static u8 no_data_hash_1[32] = { SHA1_EMPTY_HASH };

static bool PathIsDirectory(const char* path) {
    FRESULT res;
    FILINFO fno;
    res = fvx_stat(path, &fno);
    if (res != FR_OK) return false;
    return fno.fattrib & AM_DIR;
}

static void CreateStatTable(lua_State* L, FILINFO* fno) {
    lua_createtable(L, 0, 4); // create nested table
    lua_pushstring(L, fno->fname);
    lua_setfield(L, -2, "name");
    lua_pushstring(L, (fno->fattrib & AM_DIR) ? "dir" : "file");
    lua_setfield(L, -2, "type");
    lua_pushinteger(L, fno->fsize);
    lua_setfield(L, -2, "size");
    lua_pushboolean(L, fno->fattrib & AM_RDO);
    lua_setfield(L, -2, "read_only");
    // ... and leave this table on the stack for the caller to deal with
}

static int internalfs_move(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 2, "_fs.move");
    const char* path_src = luaL_checkstring(L, 1);
    const char* path_dst = luaL_checkstring(L, 2);
    FILINFO fno;

    CheckWritePermissionsLuaError(L, path_src);
    CheckWritePermissionsLuaError(L, path_dst);

    u32 flags = BUILD_PATH;
    if (extra) {
        flags = GetFlagsFromTable(L, 3, flags, NO_CANCEL | SILENT | OVERWRITE_ALL | SKIP_ALL);
    }

    if (!(flags & OVERWRITE_ALL) && (fvx_stat(path_dst, &fno) == FR_OK)) {
        return luaL_error(L, "destination already exists on %s -> %s and {overwrite=true} was not used", path_src, path_dst);
    }

    if (!(PathMoveCopy(path_dst, path_src, &flags, true))) {
        return luaL_error(L, "PathMoveCopy failed on %s -> %s", path_src, path_dst);
    }
    return 0;
}

static int internalfs_remove(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 1, "_fs.remove");
    const char* path = luaL_checkstring(L, 1);

    CheckWritePermissionsLuaError(L, path);

    u32 flags = 0;
    if (extra) {
        flags = GetFlagsFromTable(L, 2, flags, RECURSIVE);
    }

    if (!(flags & RECURSIVE)) {
        if (PathIsDirectory(path)) {
            return luaL_error(L, "requested directory remove without {recursive=true} on %s", path);
        }
    }

    if (!(PathDelete(path))) {
        return luaL_error(L, "PathDelete failed on %s", path);
    }

    return 0;
}

static int internalfs_copy(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 2, "_fs.copy");
    const char* path_src = luaL_checkstring(L, 1);
    const char* path_dst = luaL_checkstring(L, 2);
    FILINFO fno;

    CheckWritePermissionsLuaError(L, path_dst);

    u32 flags = BUILD_PATH;
    if (extra) {
        flags = GetFlagsFromTable(L, 3, flags, CALC_SHA | USE_SHA1 | NO_CANCEL | SILENT | OVERWRITE_ALL | SKIP_ALL | APPEND_ALL | RECURSIVE);
    }

    if (!(flags & RECURSIVE)) {
        if (PathIsDirectory(path_src)) {
            return luaL_error(L, "requested directory copy without {recursive=true} on %s -> %s", path_src, path_dst);
        }
    }

    if (!(flags & OVERWRITE_ALL) && (fvx_stat(path_dst, &fno) == FR_OK)) {
        return luaL_error(L, "destination already exists on %s -> %s and {overwrite=true} was not used", path_src, path_dst);
    }

    if (!(PathMoveCopy(path_dst, path_src, &flags, false))) {
        return luaL_error(L, "PathMoveCopy failed on %s -> %s", path_src, path_dst);
    }

    return 0;
}

static int internalfs_mkdir(lua_State* L) {
    CheckLuaArgCount(L, 1, "_fs.mkdir");
    const char* path = luaL_checkstring(L, 1);

    CheckWritePermissionsLuaError(L, path);

    FRESULT res = fvx_rmkdir(path);
    if (res != FR_OK) {
        return luaL_error(L, "could not mkdir %s (%d)", path, res);
    }

    return 0;
}

static int internalfs_list_dir(lua_State* L) {
    CheckLuaArgCount(L, 1, "_fs.list_dir");
    const char* path = luaL_checkstring(L, 1);
    lua_newtable(L);

    DIR dir;
    FILINFO fno;

    FRESULT res = fvx_opendir(&dir, path);
    if (res != FR_OK) {
        lua_pop(L, 1); // remove final table from stack
        return luaL_error(L, "could not opendir %s (%d)", path, res);
    }

    for (int i = 1; true; i++) {
        res = fvx_readdir(&dir, &fno);
        if (res != FR_OK) {
            fvx_closedir(&dir);
            lua_pop(L, 1); // remove final table from stack
            return luaL_error(L, "could not readdir %s (%d)", path, res);
        }
        if (fno.fname[0] == 0) break;
        CreateStatTable(L, &fno);
        lua_seti(L, -2, i); // add nested table to final table
    }
    fvx_closedir(&dir);

    return 1;
}

static int internalfs_stat(lua_State* L) {
    CheckLuaArgCount(L, 1, "_fs.stat");
    const char* path = luaL_checkstring(L, 1);
    FILINFO fno;

    FRESULT res = fvx_stat(path, &fno);
    if (res != FR_OK) {
        return luaL_error(L, "could not stat %s (%d)", path, res);
    }
    CreateStatTable(L, &fno);
    return 1;
}

// TODO: make this manually check for permissions recursively
static int internalfs_fix_cmacs(lua_State* L) {
    CheckLuaArgCount(L, 1, "_fs.fix_cmacs");
    const char* path = luaL_checkstring(L, 1);

    ShowString("%s", STR_FIXING_CMACS_PLEASE_WAIT);
    if (RecursiveFixFileCmac(path) != 0) {
        return luaL_error(L, "fixcmac failed");
    }

    return 0;
}

static int internalfs_create_dbs(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 1, "_fs.create_dbs");
    const char* destdrv = luaL_checkstring(L, 1);

    u32 flags = 0;
    if (extra) {
        flags = GetFlagsFromTable(L, 2, flags, SILENT | OVERWRITE_ALL);
    }

    if (CreateDbFilesForDrive(destdrv, (flags & SILENT), (flags & OVERWRITE_ALL)) != 0) {
        return luaL_error(L, "createdbs failed");
    }

    return 0;
}

static int internalfs_stat_fs(lua_State* L) {
    CheckLuaArgCount(L, 1, "_fs.stat_fs");
    const char* path = luaL_checkstring(L, 1);

    u64 freespace = GetFreeSpace(path);
    u64 totalspace = GetTotalSpace(path);
    u64 usedspace = totalspace - freespace;

    lua_createtable(L, 0, 3);
    lua_pushinteger(L, freespace);
    lua_setfield(L, -2, "free");
    lua_pushinteger(L, totalspace);
    lua_setfield(L, -2, "total");
    lua_pushinteger(L, usedspace);
    lua_setfield(L, -2, "used");

    return 1;
}

static int internalfs_dir_info(lua_State* L) {
    CheckLuaArgCount(L, 1, "_fs.dir_info");
    const char* path = luaL_checkstring(L, 1);

    u64 tsize = 0;
    u32 tdirs = 0;
    u32 tfiles = 0;
    if (!DirInfo(path, &tsize, &tdirs, &tfiles)) {
        return luaL_error(L, "error when running DirInfo");
    }

    lua_createtable(L, 0, 3);
    lua_pushinteger(L, tsize);
    lua_setfield(L, -2, "size");
    lua_pushinteger(L, tdirs);
    lua_setfield(L, -2, "dirs");
    lua_pushinteger(L, tfiles);
    lua_setfield(L, -2, "files");

    return 1;
}

static int FileDirSelector(lua_State* L, const char* path_orig, const char* prompt, bool is_dir, bool include_dirs, bool explorer) {
    bool ret;
    char path[_VAR_CNT_LEN] = { 0 };
    char choice[_VAR_CNT_LEN] = { 0 };
    strncpy(path, path_orig, _VAR_CNT_LEN);
    if (strncmp(path, "Z:", 2) == 0) {
        return luaL_error(L, "forbidden drive");
    } else if (!is_dir) {
        u32 flags_ext = include_dirs ? 0 : NO_DIRS;
        char *npattern = strrchr(path, '/');
        if (!npattern) {
            return luaL_error(L, "invalid path");
        }
        *(npattern++) = '\0';
        ret = FileSelector(choice, prompt, path, npattern, flags_ext, explorer);
    } else {
        ret = FileSelector(choice, prompt, path, NULL, NO_FILES | SELECT_DIRS, explorer);
    }

    if (ret) {
        lua_pushstring(L, choice);
    } else {
        lua_pushnil(L);
    }
    return 1;
}

static int internalfs_ask_select_file(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 2, "_fs.ask_select_file");
    const char* prompt = luaL_checkstring(L, 1);
    const char* path = luaL_checkstring(L, 2);

    u32 flags = 0;
    if (extra) {
        flags = GetFlagsFromTable(L, 3, flags, INCLUDE_DIRS | EXPLORER);
    };

    return FileDirSelector(L, path, prompt, false, (flags & INCLUDE_DIRS), (flags & EXPLORER));
}

static int internalfs_ask_select_dir(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 2, "_fs.ask_select_dir");
    const char* prompt = luaL_checkstring(L, 1);
    const char* path = luaL_checkstring(L, 2);

    u32 flags = 0;
    if (extra) {
        flags = GetFlagsFromTable(L, 3, flags, EXPLORER);
    };

    return FileDirSelector(L, path, prompt, true, true, (flags & EXPLORER));
}

static int internalfs_find(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 1, "_fs.find");
    const char* pattern = luaL_checkstring(L, 1);
    char path[_VAR_CNT_LEN] = { 0 };

    u32 flags = 0;
    if (extra) {
        flags = GetFlagsFromTable(L, 2, flags, FIND_FIRST);
    }

    u8 mode = (flags & FIND_FIRST) ? FN_LOWEST : FN_HIGHEST;
    FRESULT res = fvx_findpath(path, pattern, mode);
    if (res == FR_NO_PATH) {
        lua_pushnil(L);
    } else if (res != FR_OK) {
        return luaL_error(L, "failed to find %s (%d)", pattern, res);
    } else {
        lua_pushstring(L, path);
    }
    return 1;
}

// this should probably be rewritten
static int internalfs_find_all(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 2, "_fs.find_all");
    const char* dir = luaL_checkstring(L, 1);
    const char* pattern = luaL_checkstring(L, 2);

    u32 flags = 0;
    if (extra) {
        flags = GetFlagsFromTable(L, 3, flags, RECURSIVE);
    }

    char forpath[_VAR_CNT_LEN] = { 0 };

    // without re-implementing for_handler, i need to give it a "*" pattern
    // and then manually compare each filename to see if it matches
    // so that a recursive search actually works
    bool forstatus = for_handler(NULL, dir, "*", flags & RECURSIVE);
    if (!forstatus) {
        return luaL_error(L, "could not open directory");
    }

    lua_newtable(L);
    int i = 1;
    char* slash;

    while (true) {
        forstatus = for_handler(forpath, NULL, NULL, false);
        if (!forstatus) {
            forpath[0] = '\0';
        }
        if (!forpath[0]) {
            // finish for_handler
            for_handler(NULL, NULL, NULL, false);
            break;
        } else {
            slash = strrchr(forpath, '/');
            if (!slash) bkpt; // this should never, ever happen
            if (fvx_match_name(slash+1, pattern) == FR_OK) {
                lua_pushstring(L, forpath);
                lua_seti(L, -2, i++);
            }
        }
    }

    return 1;
}

static int internalfs_find_not(lua_State* L) {
    CheckLuaArgCount(L, 1, "_fs.find_not");
    const char* pattern = luaL_checkstring(L, 1);
    char path[_VAR_CNT_LEN] = { 0 };

    FRESULT res = fvx_findnopath(path, pattern);
    if (res != FR_OK) {
        return luaL_error(L, "failed to find %s (%d)", pattern, res);
    }

    lua_pushstring(L, path);
    return 1;
}

static int internalfs_exists(lua_State* L) {
    CheckLuaArgCount(L, 1, "_fs.exists");
    const char* path = luaL_checkstring(L, 1);
    FILINFO fno;

    lua_pushboolean(L, (fvx_stat(path, &fno) == FR_OK));
    return 1;
}

static int internalfs_is_dir(lua_State* L) {
    CheckLuaArgCount(L, 1, "_fs.is_dir");
    const char* path = luaL_checkstring(L, 1);

    lua_pushboolean(L, PathIsDirectory(path));
    return 1;
}

static int internalfs_is_file(lua_State* L) {
    CheckLuaArgCount(L, 1, "_fs.is_file");
    const char* path = luaL_checkstring(L, 1);
    FILINFO fno;

    FRESULT res = fvx_stat(path, &fno);
    if (res != FR_OK) {
        lua_pushboolean(L, false);
    } else {
        lua_pushboolean(L, !(fno.fattrib & AM_DIR));
    }
    return 1;
}

static int internalfs_read_file(lua_State* L) {
    CheckLuaArgCount(L, 3, "_fs.read_file");
    const char* path = luaL_checkstring(L, 1);
    lua_Integer offset = luaL_checkinteger(L, 2);
    lua_Integer size = luaL_checkinteger(L, 3);

    char *buf = malloc(size);
    if (!buf) {
        return luaL_error(L, "could not allocate memory to read file");
    }
    UINT bytes_read = 0;
    FRESULT res = fvx_qread(path, buf, offset, size, &bytes_read);
    if (res != FR_OK) {
        free(buf);
        return luaL_error(L, "could not read %s (%d)", path, res);
    }
    lua_pushlstring(L, buf, bytes_read);
    free(buf);
    return 1;
}

static int internalfs_write_file(lua_State* L) {
    CheckLuaArgCount(L, 3, "_fs.write_file");
    const char* path = luaL_checkstring(L, 1);
    lua_Integer offset = luaL_checkinteger(L, 2);
    size_t data_length = 0;
    const char* data = luaL_checklstring(L, 3, &data_length);

    CheckWritePermissionsLuaError(L, path);

    UINT bytes_written = 0;
    FRESULT res = fvx_qwrite(path, data, offset, data_length, &bytes_written);
    if (res != FR_OK) {
        return luaL_error(L, "error writing %s (%d)", path, res);
    }

    lua_pushinteger(L, bytes_written);
    return 1;
}

static int internalfs_fill_file(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 4, "_fs.fill_file");
    const char* path = luaL_checkstring(L, 1);
    lua_Integer offset = luaL_checkinteger(L, 2);
    lua_Integer size = luaL_checkinteger(L, 3);
    lua_Integer byte = luaL_checkinteger(L, 4);

    u8 real_byte = byte & 0xFF;

    u32 flags = ALLOW_EXPAND;
    if (extra) {
        flags = GetFlagsFromTable(L, 4, flags, NO_CANCEL);
    };

    if ((byte < 0) || (byte > 0xFF)) {
        return luaL_error(L, "byte is not between 0x00 and 0xFF (got: %I)", byte);
    }

    CheckWritePermissionsLuaError(L, path);

    if (!(FileSetByte(path, offset, size, real_byte, &flags))) {
        return luaL_error(L, "FileSetByte failed on %s", path);
    }

    return 0;
}

static int internalfs_make_dummy_file(lua_State* L) {
    CheckLuaArgCount(L, 2, "_fs.make_dummy_file");
    const char* path = luaL_checkstring(L, 1);
    lua_Integer size = luaL_checkinteger(L, 2);

    CheckWritePermissionsLuaError(L, path);

    if (!(FileCreateDummy(path, NULL, size))) {
        return luaL_error(L, "FileCreateDummy failed on %s", path);
    }

    return 0;
}

static int internalfs_truncate(lua_State* L) {
    CheckLuaArgCount(L, 2, "_fs.truncate");
    const char* path = luaL_checkstring(L, 1);
    lua_Integer size = luaL_checkinteger(L, 2);
    FIL fp;
    FRESULT res;

    CheckWritePermissionsLuaError(L, path);

    res = f_open(&fp, path, FA_READ | FA_WRITE);
    if (res != FR_OK) {
        return luaL_error(L, "failed to open %s (note: this only works on FAT filesystems, not virtual)", path);
    }

    res = f_lseek(&fp, size);
    if (res != FR_OK) {
        f_close(&fp);
        return luaL_error(L, "failed to seek on %s", path);
    }

    res = f_truncate(&fp);
    if (res != FR_OK) {
        f_close(&fp);
        return luaL_error(L, "failed to truncate %s", path);
    }

    f_close(&fp);
    return 0;
}

static int internalfs_img_mount(lua_State* L) {
    CheckLuaArgCount(L, 1, "_fs.img_mount");
    const char* path = luaL_checkstring(L, 1);

    bool res = InitImgFS(path);
    if (!res) {
        return luaL_error(L, "failed to mount %s", path);
    }

    return 0;
}

static int internalfs_img_umount(lua_State* L) {
    CheckLuaArgCount(L, 0, "_fs.img_umount");

    InitImgFS(NULL);

    return 0;
}

static int internalfs_get_img_mount(lua_State* L) {
    CheckLuaArgCount(L, 0, "_fs.get_img_mount");

    char path[256] = { 0 };
    strncpy(path, GetMountPath(), 256);
    if (path[0] == 0) {
        // since lua treats "" as true, return a nil to make if/else easier
        lua_pushnil(L);
    } else {
        lua_pushstring(L, path);
    }

    return 1;
}

// TODO: what if someone does offset != 0 but size = 0 (end of file)?
static int internalfs_hash_file(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 3, "_fs.hash_file");
    const char* path = luaL_checkstring(L, 1);
    lua_Integer offset = luaL_checkinteger(L, 2);
    lua_Integer size = luaL_checkinteger(L, 3);
    FRESULT res;
    FILINFO fno;

    if (size == 0) {
        res = fvx_stat(path, &fno);
        if (res != FR_OK) {
            return luaL_error(L, "failed to stat %s", path);
        }

        size = fno.fsize;
    }

    u32 flags = 0;
    if (extra) {
        flags = GetFlagsFromTable(L, 4, flags, USE_SHA1);
    }

    const u8 hashlen = (flags & USE_SHA1) ? 20 : 32;
    u8 hash_fil[0x20];

    if (size == 0) {
        // shortcut by just returning the hash of empty data
        memcpy(hash_fil, (flags & USE_SHA1) ? no_data_hash_1 : no_data_hash_256, hashlen);
    } else if (!(FileGetSha(path, hash_fil, offset, size, (flags & USE_SHA1)))) {
        return luaL_error(L, "FileGetSha failed on %s", path);
    }

    lua_pushlstring(L, (char*)hash_fil, hashlen);
    return 1;
}

static int internalfs_hash_data(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 1, "_fs.hash_data");
    size_t data_length = 0;
    const char* data = luaL_checklstring(L, 1, &data_length);

    u32 flags = 0;
    if (extra) {
        flags = GetFlagsFromTable(L, 2, flags, USE_SHA1);
    }

    const u8 hashlen = (flags & USE_SHA1) ? 20 : 32;
    u8 hash_fil[0x20];

    if (data_length == 0) {
        // shortcut by just returning the hash of empty data
        memcpy(hash_fil, (flags & USE_SHA1) ? no_data_hash_1 : no_data_hash_256, hashlen);
    } else {
        sha_quick(hash_fil, data, data_length, (flags & USE_SHA1) ? SHA1_MODE : SHA256_MODE);
    }

    lua_pushlstring(L, (char*)hash_fil, hashlen);
    return 1;
}

static int internalfs_allow(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 1, "_fs.allow");
    const char* path = luaL_checkstring(L, 1);
    u32 flags = 0;
    bool allowed;
    if (extra) {
        flags = GetFlagsFromTable(L, 2, 0, ASK_ALL);
    }

    if (flags & ASK_ALL) {
        allowed = CheckDirWritePermissions(path);
    } else {
        allowed = CheckWritePermissions(path);
    }
    lua_pushboolean(L, allowed);
    return 1;
};

static int internalfs_verify(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 1, "_fs.verify");
    const char* path = luaL_checkstring(L, 1);
    bool res;

    u32 flags = 0;
    if (extra) {
        flags = GetFlagsFromTable(L, 2, flags, SIG_CHECK);
    }

    u64 filetype = IdentifyFileType(path);
    if (filetype & IMG_NAND) res = (ValidateNandDump(path) == 0);
    else res = (VerifyGameFile(path, flags & SIG_CHECK) == 0);

    lua_pushboolean(L, res);
    return 1;
}

static int internalfs_sd_is_mounted(lua_State* L) {
    CheckLuaArgCount(L, 0, "_fs.sd_is_mounted");

    lua_pushboolean(L, CheckSDMountState());
    return 1;
}

static int internalfs_sd_switch(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 0, "_fs.sd_switch");
    const char* message;

    if (extra) {
        message = luaL_checkstring(L, 1);
    } else {
        message = "Please switch the SD card now.";
    }

    bool ret;

    DeinitExtFS();
    if (!(ret = CheckSDMountState())) {
        return luaL_error(L, "%s", STR_SCRIPTERR_SD_NOT_MOUNTED);
    }

    u32 pad_state;
    DeinitSDCardFS();
    ShowString("%s\n \n%s", message, STR_EJECT_SD_CARD);
    while (!((pad_state = InputWait(0)) & (BUTTON_B|SD_EJECT)));
    if (pad_state & SD_EJECT) {
        ShowString("%s\n \n%s", message, STR_INSERT_SD_CARD);
        while (!((pad_state = InputWait(0)) & (BUTTON_B|SD_INSERT)));
    }
    if (pad_state & BUTTON_B) {
        return luaL_error(L, "user canceled");
    }

    InitSDCardFS();
    AutoEmuNandBase(true);
    InitExtFS();

    return 0;
}

static int internalfs_key_dump(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 1, "_fs.key_dump");
    const char* opts[] = {SEEDINFO_NAME, TIKDB_NAME_ENC, TIKDB_NAME_DEC, NULL};
    int opt = luaL_checkoption(L, 1, NULL, opts);
    bool ret = false;

    u32 flags = 0;
    if (extra) {
        flags = GetFlagsFromTable(L, 2, 0, OVERWRITE_ALL);
    }

    if (opt == 1 || opt == 2) {
        bool tik_dec = opt == 2;
        if (flags & OVERWRITE_ALL) fvx_unlink(tik_dec ? OUTPUT_PATH "/" TIKDB_NAME_DEC : OUTPUT_PATH "/" TIKDB_NAME_ENC);
        if (BuildTitleKeyInfo(NULL, tik_dec, false) == 0) {
            ShowString(STR_BUILDING_TO_OUT_ARG, OUTPUT_PATH, opts[opt]);
            if (((BuildTitleKeyInfo("1:/dbs/ticket.db", tik_dec, false) == 0) ||
                 (BuildTitleKeyInfo("4:/dbs/ticket.db", tik_dec, false) == 0)) &&
                (BuildTitleKeyInfo(NULL, tik_dec, true) == 0))
                ret = true;
        }
    } else if (opt == 0) {
        if (flags & OVERWRITE_ALL) fvx_unlink(OUTPUT_PATH "/" SEEDINFO_NAME);
        if (BuildSeedInfo(NULL, false) == 0) {
            ShowString(STR_BUILDING_TO_OUT_ARG, OUTPUT_PATH, opts[opt]);
            if (((BuildSeedInfo("1:", false) == 0) ||
                 (BuildSeedInfo("4:", false) == 0)) &&
                (BuildSeedInfo(NULL, true) == 0))
                ret = true;
        }
    }

    if (!ret) {
        return luaL_error(L, "building %s failed", opts[opt]);
    }

    return 0;
}

static int internalfs_cart_dump(lua_State* L) {
    bool extra = CheckLuaArgCountPlusExtra(L, 2, "_fs.cart_dump");
    const char* path = luaL_checkstring(L, 1);
    u64 fsize = (u64)luaL_checkinteger(L, 2);
    bool ret = false;
    const char* errstr = "";

    u32 flags = 0;
    if (extra) {
        flags = GetFlagsFromTable(L, 3, flags, ENCRYPTED);
    }

    CartData* cdata = (CartData*) malloc(sizeof(CartData));
    u8* buf = (u8*) malloc(STD_BUFFER_SIZE);
    ret = false;
    if (!cdata || !buf) {
        errstr = "out of memory";
    } else if (InitCartRead(cdata) != 0){
        errstr = "cart init fail";
    } else {
        SetSecureAreaEncryption(flags & ENCRYPTED);
        fvx_unlink(path);
        ret = true;
        errstr = "cart dump failed or canceled";
        for (u64 p = 0; p < fsize; p += STD_BUFFER_SIZE) {
            u64 len = min((fsize - p), STD_BUFFER_SIZE);
            ShowProgress(p, fsize, path);
            if (!ShowProgress(p, fsize, path) ||
                (ReadCartBytes(buf, p, len, cdata, false) != 0) ||
                (fvx_qwrite(path, buf, p, len, NULL) != FR_OK)) {
                ret = false;
                break;
            }
        }
    }
    free(buf);
    free(cdata);

    if (!ret) {
        return luaL_error(L, "%s", errstr);
    }
    return 0;
}

static const luaL_Reg internalfs_lib[] = {
    {"move", internalfs_move},
    {"remove", internalfs_remove},
    {"copy", internalfs_copy},
    {"mkdir", internalfs_mkdir},
    {"list_dir", internalfs_list_dir},
    {"stat", internalfs_stat},
    {"stat_fs", internalfs_stat_fs},
    {"dir_info", internalfs_dir_info},
    {"ask_select_file", internalfs_ask_select_file},
    {"ask_select_dir", internalfs_ask_select_dir},
    {"find", internalfs_find},
    {"find_all", internalfs_find_all},
    {"find_not", internalfs_find_not},
    {"exists", internalfs_exists},
    {"is_dir", internalfs_is_dir},
    {"is_file", internalfs_is_file},
    {"read_file", internalfs_read_file},
    {"write_file", internalfs_write_file},
    {"fill_file", internalfs_fill_file},
    {"make_dummy_file", internalfs_make_dummy_file},
    {"truncate", internalfs_truncate},
    {"img_mount", internalfs_img_mount},
    {"img_umount", internalfs_img_umount},
    {"get_img_mount", internalfs_get_img_mount},
    {"hash_file", internalfs_hash_file},
    {"hash_data", internalfs_hash_data},
    {"verify", internalfs_verify},
    {"allow", internalfs_allow},
    {"sd_is_mounted", internalfs_sd_is_mounted},
    {"sd_switch", internalfs_sd_switch},
    {"fix_cmacs", internalfs_fix_cmacs},
    {"create_dbs", internalfs_create_dbs},
    {"key_dump", internalfs_key_dump},
    {"cart_dump", internalfs_cart_dump},
    {NULL, NULL}
};

int gm9lua_open_internalfs(lua_State* L) {
    luaL_newlib(L, internalfs_lib);
    return 1;
}
#endif
