#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "../../include/grubmenu.h"

static int
skip_line(FILE *infile) {
    int c;

    do {
        c= fgetc(infile);
        if(c < 0) return c;
    } while(c != '\n');

    return 0;
}

static int
skip_whitespace(FILE *infile, int multiline) {
    int c;
    do {
        c = fgetc(infile);
        if(c < 0) return c;
        if(!multiline && c == '\n') break;
    } while(isspace(c));
    ungetc(c, infile);

    return 0;
}

static int
peek(FILE *f) {
    int c= fgetc(f);
    if(c >= 0) ungetc(c, f);
    return c;
}

#define MAXBUF 255

static int
read_token(FILE *infile, char *buf) {
    int c= peek(infile);
    if(c == EOF) return 0;
    if(c < 0) return c;

    while(isspace(c) || c == '#') {
        skip_whitespace(infile, 1);
        if(c == '#') skip_line(infile);
        c= peek(infile);
        if(c == EOF) return 0;
        if(c < 0) return c;
    }

    size_t i= 0;
    c= fgetc(infile);
    while(!isspace(c)) {
        assert(i < MAXBUF);
        buf[i]= c;
        i++;
        c= fgetc(infile);
        if(c == EOF) break;
        if(c < 0) return c;
    }
    if(c >= 0) ungetc(c, infile);

    buf[i]= '\0';
    return i + 1;
}

static int
read_line(FILE *infile, char *buf) {
    int c;

    int len= skip_whitespace(infile, 0);
    if(len < 0) return len;

    size_t i= 0;
    do {
        c= fgetc(infile);
        if(c < 0) return c;
        assert(i < MAXBUF);
        if(c != '\n') {
            buf[i]= c;
            i++;
        }
    } while(c != '\n');

    buf[i]= '\0';
    return i + 1;
}

typedef int (*reader_t)(FILE *, char *);

static int
read_string(FILE *f, reader_t reader, char **bufptr) {
    char buf[MAXBUF+1];

    size_t len= reader(f, buf);
    if(len < 0) return len;

    *bufptr= malloc(len);
    if(!*bufptr) {
        perror("malloc");
        return 0;
    }

    memcpy(*bufptr, buf, len);
    return len;
}

struct menu_lst *
read_menu_lst(const char *path) {
    int len;

    FILE *infile= fopen(path, "r");
    if(!infile) {
        perror("fopen");
        return NULL;
    }

    struct menu_lst *menu= calloc(sizeof(struct menu_lst), 1);
    if(!menu) {
        perror("malloc");
        return NULL;
    }

    char cmd[MAXBUF+1];
    while((len= read_token(infile, cmd)) > 0) {
        if(!strcmp(cmd, "title")) {
            len= read_string(infile, read_token, &menu->title);
            if(len <= 0) {
                fprintf(stderr, "Missing title value.\n");
                break;
            }
        }
        else if(!strcmp(cmd, "kernel")) {
            len= read_string(infile, read_token, &menu->kernel.path);
            if(len <= 0) {
                fprintf(stderr, "Missing kernel path.\n");
                break;
            }

            len= read_string(infile, read_line, &menu->kernel.args);
            if(len <= 0) {
                fprintf(stderr, "Missing kernel arguments.\n");
                break;
            }
        }
        else if(!strcmp(cmd, "image")) {
            len= read_string(infile, read_line, &menu->image);
            if(len <= 0) {
                fprintf(stderr, "Missing image specifier.\n");
                break;
            }
        }
        // handle "module" and "modulenounzip"
        else if(!strncmp(cmd, "module", 6)) {
            menu->nmodules++;

            menu->modules=
                realloc(menu->modules,
                        menu->nmodules * sizeof(struct menu_module));
            if(!menu->modules) {
                perror("realloc");
                return NULL;
            }

            struct menu_module *mod= &menu->modules[menu->nmodules - 1];

            len= read_string(infile, read_token, &mod->path);
            if(len <= 0) {
                fprintf(stderr, "Missing module path.\n");
                break;
            }

            len= read_string(infile, read_line, &mod->args);
            if(len <= 0) {
                fprintf(stderr, "Missing module arguments.\n");
                break;
            }
        }
        else if(!strcmp(cmd, "mmap")) {
            menu->mmap_len++;

            menu->mmap=
                realloc(menu->mmap,
                        menu->mmap_len * sizeof(struct menu_mmap_entry));
            if(!menu->mmap) {
                perror("realloc");
                return NULL;
            }

            struct menu_mmap_entry *entry= &menu->mmap[menu->mmap_len - 1];

            len= read_string(infile, read_token, &entry->name);
            if(len <= 0) {
                fprintf(stderr, "Missing MMAP entry name.\n");
                break;
            }

            char buf[MAXBUF+1];
            len= read_token(infile, buf);
            if(len <= 0) {
                fprintf(stderr, "Missing MMAP start address.\n");
                break;
            }
            errno= 0;
            entry->base= strtoull(buf, NULL, 0);
            if(errno) {
                fprintf(stderr, "Invalid MMAP start address.\n");
                break;
            }

            len= read_token(infile, buf);
            if(len <= 0) {
                fprintf(stderr, "Missing MMAP length.\n");
                break;
            }
            errno= 0;
            entry->length= strtoull(buf, NULL, 0);
            if(errno) {
                fprintf(stderr, "Invalid MMAP length.\n");
                break;
            }

            len= read_token(infile, buf);
            if(len <= 0) {
                fprintf(stderr, "Missing MMAP ID.\n");
                break;
            }
            errno= 0;
            entry->type= strtoul(buf, NULL, 0);
            if(errno) {
                fprintf(stderr, "Invalid MMAP ID.\n");
                break;
            }
        }
        else if(!strcmp(cmd, "timeout")) {
            char buf[MAXBUF+1];
            len= read_token(infile, buf);
            if(len <= 0) {
                fprintf(stderr, "Missing timeout value.\n");
                break;
            }
            errno= 0;
            menu->timeout= strtoul(buf, NULL, 0);
            if(errno) {
                fprintf(stderr, "Invalid timeout value.\n");
                break;
            }
        }
        else if(!strcmp(cmd, "bootdriver")) {
            len= read_string(infile, read_token, &menu->boot_driver.path);
            if(len <= 0) {
                fprintf(stderr, "Missing timeout value.\n");
                break;
            }
        }
        else if(!strcmp(cmd, "cpudriver")) {
            len= read_string(infile, read_token, &menu->kernel.path);
            if(len <= 0) {
                fprintf(stderr, "Missing timeout value.\n");
                break;
            }
            len= read_string(infile, read_line, &menu->kernel.args);
            if(len <= 0) {
                fprintf(stderr, "Missing module arguments.\n");
                break;
            }
        }
        else if(!strcmp(cmd, "root")) {
            char buf[MAXBUF+1];
            len= read_token(infile, buf);
            if(len <= 0) {
                fprintf(stderr, "Missing timeout value.\n");
                break;
            }
        }
        else {
            fprintf(stderr, "Unknown command: %s\n", cmd);
        }
    }
    if(len < 0) {
        perror("read_token");
        return NULL;
    }

    if(fclose(infile)) {
        perror("fclose");
        return NULL;
    }

    return menu;
}
