#include "nds.h"
#include "fat.h"
#include "file.h"

static bool FS_ENABLE = false;
static u8 buffer[255];


void init_filesystem() {
    FS_ENABLE = fatInitDefault();
}

bool filesystem_enabled() {
    return FS_ENABLE;
}

void fs_close(FileDevice *fs) {
    fclose(fs->file);
    fs->file = NULL;
    closedir(fs->dir);
    fs->dir = NULL;
    fs->open = false;
    fs->success = false;
    fs->is_dir = false;
    fs->child_is_dir = false;
    pb_clear(&fs->path);
    pb_clear(&fs->child_path);
    fs->pointer = 0;
    fs->length = 0;
    fs->child = NULL;
    fs->dir_i = 0;
}

void fs_clear(FileDevice *fs) {
    pb_clear(&fs->entry);
    pb_clear(&fs->action);
}

void fs_open_entry(FileDevice* fs, u8 *path) {
    fs->success = false;
    // Prevent non-absolute paths from being opened.
    if (path[0] != '/') {
        return;
    }
    // Remove trailing slash if any, without removing leading slash.
    for (int i=2; i<255; i++) {
        if (path[i] == 0) {
            if (path[i-1] == '/') {
                path[i-1] = 0;
            }
            break;
        }
    }
    // Prevent paths with relative components from being opened.
    for (int i=0; i<255; i++) {
        if (path[i] == '/') {
            if (path[i+1]=='.') {
                // Check for '.' components.
                if (path[i+2]=='/' || path[i+2]==0) {
                    return;
                }
                // Check for '..' components.
                if (path[i+2]=='.' && (path[i+3]=='/' || path[i+3]==0)) {
                    return;
                }
            }
        } else if (path[i] == 0) {
            break;
        }
    }


    DIR *tmpdir = opendir((char*) path);
    if (tmpdir) {
        fs_close(fs);
        fs->dir = tmpdir;
        fs->open = true;
        fs->success = true;
        fs->is_dir = true;
        fs->dir_i = 0;
        // Find length of directory
        while ((fs->child = readdir(fs->dir))) {
            fs->dir_i++;
        };
        fs->length = fs->dir_i;
        fs_select_child(fs, 0);
        pb_populate(&fs->path, path);
        return;
    }

    // MEMORY LEAK HERE ONLY
    // for (int i=0; i<500; i++) {
    //     fs->tmpfile = fopen((char*) path, "r+");
    //     fclose(fs->tmpfile);
    // }
    // END OF MEMORY LEAK CODE

    // FILE CODE MUST COME AFTER DIR CODE TO AVOID MEMORY LEAK
    FILE *tmpfile = fopen((char*) path, "r+");
    if (tmpfile) {
        fs_close(fs);
        fs->file = tmpfile;
        fs->open = true;
        fs->success = true;
        fs->is_dir = false;
        // TODO: Only read length when we need to. Keep a bool around.
        // fseek(fs->file, 0, SEEK_END);
        // fs->length = ftell(fs->file);
        // rewind(fs->file);
        pb_populate(&fs->path, path);
        return;
    }
}

void fs_select_child(FileDevice *fs, u32 pointer) {
    fs->pointer = pointer;
    rewinddir(fs->dir);
    fs->dir_i = 0;

    for (fs->dir_i=0; fs->dir_i<pointer+1; fs->dir_i++) {
        fs->child = readdir(fs->dir);
    };
    if (fs->child) {
        pb_populate(&fs->child_path, (u8*) &fs->child->d_name);
        fs->child_is_dir = fs->child->d_type == DT_DIR;
    } else {
        pb_clear(&fs->child_path);
        fs->child_is_dir = false;
    }
}

void fs_push_entry(FileDevice *fs, u8 byte) {
    if (pb_push_byte(&fs->entry, byte)) {
        fs_close(fs);
        if (pb_is_terminated(&fs->entry) && fs->entry.mem[0] != 0) {
            fs_open_entry(fs, &fs->entry.mem[0]);
        }
        fs_clear(fs);
    }
}

void fs_push_action(FileDevice *fs, u8 byte) {
    if (pb_push_byte(&fs->action, byte)) {
        if (!pb_is_terminated(&fs->action)) return;
        bool action = fs->action.mem[0] != 0;
        if (fs->open) {
            if (action) {
                fs->success = rename((char*) fs->path.mem, (char*) fs->action.mem);
            } else {
                // TODO: DELETE
            }
        } else if (action) {
            FILE *tmpfile = fopen((char*) fs->action.mem, "w");
            fs->success = tmpfile != NULL;
            fclose(tmpfile);
        }
        fs_close(fs);
        fs_clear(fs);
    }
}


u8 fs_read_byte(FileDevice *fs) {
    return fgetc(fs->file);
}

void fs_write_byte(FileDevice *fs, u8 byte) {
    fputc(byte, fs->file);
}

void fs_ascend(FileDevice *fs) {
    fs->success = false;
    if (fs->open) {
        // Don't ascend if we're already at the root.
        if (fs->path.mem[1] == 0) {
            return;
        }
        // Path is guaranteed to have no trailing slash.
        memcpy(&buffer, &fs->path.mem[0], 255);
        u8 slash_i = 0;
        for (int i=0; i<255; i++) {
            if (buffer[i] == '/') {
                slash_i = i;
            } else if (buffer[i] == 0) {
                // Trim to leave the slash
                buffer[slash_i+1] = 0;
                break;
            }
        }
        fs_open_entry(fs, &buffer[0]);
    } else {
        fs_open_entry(fs, (u8*) "/");
    }
}

void fs_descend(FileDevice *fs) {
    fs->success = false;
    if (!(fs->open && fs->is_dir && fs->child)) {
        return;
    }
    memcpy(&buffer, &fs->path.mem[0], 255);
    // Find location of terminating null in buffer.
    int start = 0;
    for (int i=0; i<255; i++) {
        start = i;
        if (buffer[i] == 0) break;
    }
    // Find length of child name.
    int len = 0;
    for (int i=0; i<255; i++) {
        len = i;
        if (fs->child->d_name[i] == 0) break;
    }
    // Check that buffer has capacity for child name.
    if (start+len > 255) {
        fs->success = false;
        return;
    }
    // Add a slash if not at root.
    if (start != 1) {
        buffer[start] = '/';
        start++;
    }
    // Copy child name into buffer.
    for (int i=0; i<len; i++) {
        buffer[start+i] = fs->child->d_name[i];
    }
    fs_open_entry(fs, &buffer[0]);
}

void fs_seek(FileDevice *fs) {
    if (fs->open) {
        if (fs->is_dir) {
            fs_select_child(fs, fs->pointer_write);
        } else {
            fseek(fs->file, fs->pointer_write, SEEK_SET);
            fs->pointer = ftell(fs->file);
        }
    }
}

void fs_resize(FileDevice *fs) {
    if (fs->open && !fs->is_dir) {
        // truncate(fs->file);
        // TODO
    }
}