/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2012 Amaury Pouly
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/

#define _POSIX_C_SOURCE 200809L /* for strdup */
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <stdarg.h>
#include <strings.h>

#include "crypto.h"
#include "elf.h"
#include "sb1.h"
#include "misc.h"

#define ROUND_UP(val, round) ((((val) + (round) - 1) / (round)) * (round))

/**
 * Globals
 */

char *g_output_file;
bool g_critical;
bool g_final;
bool g_strict = true;
uint32_t g_jump_arg;

/**
 * Helpers
 */

typedef char* (*get_next_arg_t)(void *user);

struct cmd_line_next_arg_user_t
{
    int argc;
    char **argv;
};

static char *cmd_line_next_arg(void *user)
{
    struct cmd_line_next_arg_user_t *uu = user;
    if(uu->argc == 0)
        return NULL;
    uu->argc--;
    uu->argv++;
    return *(uu->argv - 1);
}

static int sb1_add_inst(struct sb1_file_t *sb, struct sb1_inst_t *insts, int nr_insts)
{
    sb->insts = augment_array(sb->insts, sizeof(struct sb1_inst_t), sb->nr_insts,
        insts, nr_insts);
    sb->nr_insts += nr_insts;
    return 0;
}

static int sb1_add_load(struct sb1_file_t *sb, void *data, int size, uint32_t addr)
{
    while(size > 0)
    {
        int len = MIN(size, SB1_CMD_MAX_LOAD_SIZE);
        struct sb1_inst_t inst;
        memset(&inst, 0, sizeof(inst));
        inst.cmd = SB1_INST_LOAD;
        inst.size = len;
        inst.addr = addr;
        inst.critical = g_critical;
        inst.data = xmalloc(len);
        memcpy(inst.data, data, len);
        if(g_debug)
            printf("Add instruction: load %#x bytes at %#x\n", len, addr);
        int ret = sb1_add_inst(sb, &inst, 1);
        if(ret < 0)
            return ret;
        data += len;
        size -= len;
        addr += len;
    }
    return 0;
}

static int sb1_add_switch(struct sb1_file_t *sb, uint32_t driver)
{
    struct sb1_inst_t inst;
    memset(&inst, 0, sizeof(inst));
    inst.cmd = SB1_INST_MODE;
    inst.critical = g_critical;
    inst.mode = driver;
    if(g_debug)
        printf("Add instruction: switch driver to  %#x\n", driver);
    g_final = true;
    return sb1_add_inst(sb, &inst, 1);
}

static int sb1_add_sdram(struct sb1_file_t *sb, uint32_t cs, uint32_t size)
{
    struct sb1_inst_t inst;
    memset(&inst, 0, sizeof(inst));
    inst.cmd = SB1_INST_SDRAM;
    inst.critical = g_critical;
    inst.sdram.chip_select = cs;
    inst.sdram.size_index = sb1_sdram_index_by_size(size);
    if(sb1_sdram_index_by_size(size) < 0)
        bug("Unknown SDRAM size: %d MB\n", size);
    if(g_debug)
        printf("Add instruction: init SDRAM (chip select=%d, size=%d MB)\n", cs, size);
    return sb1_add_inst(sb, &inst, 1);
}

static int sb1_add_call(struct sb1_file_t *sb, uint32_t addr, uint32_t arg)
{
    struct sb1_inst_t inst;
    memset(&inst, 0, sizeof(inst));
    inst.cmd = SB1_INST_CALL;
    inst.critical = g_critical;
    inst.addr = addr;
    inst.argument = arg;
    if(g_debug)
        printf("Add instruction: call %#x with argument %#x\n", addr, arg);
    return sb1_add_inst(sb, &inst, 1);
}

static int sb1_add_jump(struct sb1_file_t *sb, uint32_t addr, uint32_t arg)
{
    struct sb1_inst_t inst;
    memset(&inst, 0, sizeof(inst));
    inst.cmd = SB1_INST_JUMP;
    inst.critical = g_critical;
    inst.addr = addr;
    inst.argument = arg;
    if(g_debug)
        printf("Add instruction: jump %#x with argument %#x\n", addr, arg);
    g_final = true;
    return sb1_add_inst(sb, &inst, 1);
}

static int sb1_add_fill(struct sb1_file_t *sb, uint32_t pattern, uint32_t size, uint32_t addr)
{
    while(size > 0)
    {
        int len = MIN(size, SB1_CMD_MAX_FILL_SIZE);
        struct sb1_inst_t inst;
        memset(&inst, 0, sizeof(inst));
        inst.cmd = SB1_INST_FILL;
        inst.critical = g_critical;
        inst.size = len;
        inst.addr = addr;
        inst.pattern = pattern;
        inst.datatype = SB1_DATATYPE_UINT32;
        if(g_debug)
            printf("Add instruction: fill %#x bytes with pattern %#x at address %#x\n",
                size, pattern, addr);
        int ret = sb1_add_inst(sb, &inst, 1);
        if(ret < 0)
            return ret;
        size -= len;
        addr += len;
    }

    return 0;
}

/**
 * SB file modification
 */

static void generate_default_sb_version(struct sb1_version_t *ver)
{
    ver->major = ver->minor = ver->revision = 0x9999;
}

static struct sb1_file_t *create_sb1_file(void)
{
    struct sb1_file_t *sb = xmalloc(sizeof(struct sb1_file_t));
    memset(sb, 0, sizeof(struct sb1_file_t));

    /* default versions and key, apply_args() will overwrite if specified */
    generate_default_sb_version(&sb->product_ver);
    generate_default_sb_version(&sb->component_ver);
    sb1_get_default_key(&sb->key);

    return sb;
}

static void *load_file(const char *filename, int *size)
{
    FILE *fd = fopen(filename, "rb");
    if(fd == NULL)
        bug("cannot open '%s' for reading\n", filename);
    if(g_debug)
        printf("Loading binary file '%s'...\n", filename);
    fseek(fd, 0, SEEK_END);
    *size = ftell(fd);
    fseek(fd, 0, SEEK_SET);
    void *data = xmalloc(*size);
    fread(data, 1, *size, fd);
    fclose(fd);
    return data;
}

static bool parse_sb_sub_version(uint16_t *ver, char *str)
{
    *ver = 0;
    for(int i = 0; str[i]; i++)
    {
        if(i >= 4)
            return false;
        if(str[i] < '0' || str[i] > '9')
            return false;
        *ver = *ver << 4 | (str[i] - '0');
    }
    return true;
}

static bool parse_sb_version(struct sb1_version_t *ver, char *str)
{
    char *p = strchr(str, '.');
    char *q = strchr(p + 1, '.');
    if(p == NULL || q == NULL) return false;
    *p = *q = 0;
    return parse_sb_sub_version(&ver->major, str) &&
            parse_sb_sub_version(&ver->minor, p + 1) &&
            parse_sb_sub_version(&ver->revision, q + 1);
}

/**
 * Command line parsing
 */

#define MAX_NR_ARGS 2

#define ARG_STR     0
#define ARG_UINT    1

#define CMD_FN(name) \
    int name(struct sb1_file_t *sb, union cmd_arg_t args[MAX_NR_ARGS])

union cmd_arg_t
{
    char *str;
    unsigned long uint;
};

typedef int (*process_arg_t)(struct sb1_file_t *sb, union cmd_arg_t args[MAX_NR_ARGS]);

struct cmd_entry_t
{
    const char *name;
    int nr_args;
    int arg_type[MAX_NR_ARGS];
    process_arg_t fn;
};

/* Callbacks */

static void usage(void);

CMD_FN(cmd_help)
{
    (void) args;
    (void) sb;
    usage();
    return 0;
}

CMD_FN(cmd_debug)
{
    (void) args;
    (void) sb;
    g_debug = true;
    return 0;
}

CMD_FN(cmd_drive_tag)
{
    sb->drive_tag = args[0].uint;
    return 0;
}

CMD_FN(cmd_load_binary)
{
    int size;
    void *data = load_file(args[0].str, &size);
    int ret = sb1_add_load(sb, data, size, args[1].uint);
    free(data);
    return ret;
}

CMD_FN(cmd_output)
{
    (void) sb;
    g_output_file = strdup(args[0].str);
    return 0;
}

CMD_FN(cmd_switch)
{
    return sb1_add_switch(sb, args[0].uint);
}

CMD_FN(cmd_sdram)
{
    return sb1_add_sdram(sb, args[0].uint, args[1].uint);
}

CMD_FN(cmd_critical)
{
    (void) sb;
    (void) args;
    g_critical = true;
    return 0;
}

CMD_FN(cmd_clear_critical)
{
    (void) sb;
    (void) args;
    g_critical = false;
    return 0;
}

CMD_FN(cmd_strict)
{
    (void) sb;
    (void) args;
    g_strict = true;
    return 0;
}

CMD_FN(cmd_clear_strict)
{
    (void) sb;
    (void) args;
    g_strict = false;
    return 0;
}

CMD_FN(cmd_call)
{
    /* FIXME: the proprietary sbtoelf always sets argument to 0 ?! */
    return sb1_add_call(sb, args[0].uint, g_jump_arg);
}

CMD_FN(cmd_jump)
{
    return sb1_add_jump(sb, args[0].uint, g_jump_arg);
}

CMD_FN(cmd_jumparg)
{
    (void) sb;
    g_jump_arg = args[0].uint;
    return 0;
}

static int load_elf(struct sb1_file_t *sb, const char *filename, int act)
{
    struct elf_params_t elf;
    FILE *fd = fopen(filename, "rb");
    if(fd == NULL)
        bug("cannot open '%s'\n", filename);
    if(g_debug)
        printf("Loading elf file '%s'...\n", filename);
    elf_init(&elf);
    bool loaded = elf_read_file(&elf, elf_std_read, generic_std_printf, fd);
    fclose(fd);
    if(!loaded)
        bug("error loading elf file '%s'\n", filename);
    elf_sort_by_address(&elf);

    struct elf_section_t *esec = elf.first_section;
    while(esec)
    {
        if(esec->type == EST_LOAD)
            sb1_add_load(sb, esec->section, esec->size, esec->addr);
        else if(esec->type == EST_FILL)
            sb1_add_fill(sb, esec->pattern, esec->size, esec->addr);
        esec = esec->next;
    }

    int ret = 0;
    if(act == SB1_INST_JUMP || act == SB1_INST_CALL)
    {
        if(!elf.has_start_addr)
            bug("Cannot jump/call: '%s' has no start address!\n", filename);
        if(act == SB1_INST_JUMP)
            ret = sb1_add_jump(sb, elf.start_addr, g_jump_arg);
        else
            ret = sb1_add_call(sb, elf.start_addr, g_jump_arg);
    }

    elf_release(&elf);

    return ret;
}

CMD_FN(cmd_load)
{
    return load_elf(sb, args[0].str, SB1_INST_LOAD);
}

CMD_FN(cmd_loadjump)
{
    return load_elf(sb, args[0].str, SB1_INST_JUMP);
}

CMD_FN(cmd_loadjumpreturn)
{
    return load_elf(sb, args[0].str, SB1_INST_CALL);
}

CMD_FN(cmd_rom)
{
    sb->rom_version = args[0].uint;
    return 0;
}

CMD_FN(cmd_product)
{
    if(!parse_sb_version(&sb->product_ver, args[0].str))
        bug("Invalid version string '%s'\n", args[0].str);
    return 0;
}

CMD_FN(cmd_component)
{
    if(!parse_sb_version(&sb->component_ver, args[0].str))
        bug("Invalid version string '%s'\n", args[0].str);
    return 0;
}

CMD_FN(cmd_keyfile)
{
    if(!add_keys_from_file(args[0].str))
        bug("Cannot add keys from file '%s'\n", args[0].str);
    for(int i = 0; i < g_nr_keys; i++)
        if(g_key_array[i].method == CRYPTO_XOR_KEY)
        {
            memcpy(&sb->key, &g_key_array[i], sizeof(sb->key));
            break;
        }
    return 0;
}

#define CMD(name,fn,nr_args,...) {name,nr_args,{__VA_ARGS__},fn},
struct cmd_entry_t g_cmds[] =
{
    CMD("-d", cmd_debug, 0)
    CMD("-debugon", cmd_debug, 0)
    CMD("-h", cmd_help, 0)
    CMD("-?", cmd_help, 0)
    CMD("-load-binary", cmd_load_binary, 2, ARG_STR, ARG_UINT)
    CMD("-drive-tag", cmd_drive_tag, 1, ARG_UINT)
    CMD("-o", cmd_output, 1, ARG_STR)
    CMD("-w", cmd_switch, 1, ARG_UINT)
    CMD("-switchdriver", cmd_switch, 1, ARG_UINT)
    CMD("-sdram", cmd_sdram, 2, ARG_UINT, ARG_UINT)
    CMD("-c", cmd_critical, 0)
    CMD("-critical", cmd_critical, 0)
    CMD("-C", cmd_clear_critical, 0)
    CMD("-noncritical", cmd_clear_critical, 0)
    CMD("-n", cmd_strict, 0)
    CMD("-strict", cmd_strict, 0)
    CMD("-N", cmd_clear_strict, 0)
    CMD("-nonstrict", cmd_clear_strict, 0)
    CMD("-call", cmd_call, 1, ARG_UINT)
    CMD("-jump", cmd_jump, 1, ARG_UINT)
    CMD("-jumparg", cmd_jumparg, 1, ARG_UINT)
    CMD("-f", cmd_load, 1, ARG_STR)
    CMD("-load", cmd_load, 1, ARG_STR)
    CMD("-r", cmd_loadjumpreturn, 1, ARG_STR)
    CMD("-loadjumpreturn", cmd_loadjumpreturn, 1, ARG_STR)
    CMD("-j", cmd_loadjump, 1, ARG_STR)
    CMD("-loadjump", cmd_loadjump, 1, ARG_STR)
    CMD("-R", cmd_rom, 1, ARG_UINT)
    CMD("-rom", cmd_rom, 1, ARG_UINT)
    CMD("-p", cmd_product, 1, ARG_STR)
    CMD("-product", cmd_product, 1, ARG_STR)
    CMD("-v", cmd_component, 1, ARG_STR)
    CMD("-component", cmd_component, 1, ARG_STR)
    CMD("-k", cmd_keyfile, 1, ARG_STR)
};
#undef CMD

#define NR_CMDS (int)(sizeof(g_cmds) / sizeof(g_cmds[0]))

static int apply_args(struct sb1_file_t *sb, get_next_arg_t next, void *user)
{
    while(true)
    {
        /* next command ? */
        char *cmd = next(user);
        if(cmd == NULL)
            break;
        /* switch */
        int i = 0;
        while(i < NR_CMDS && strcmp(cmd, g_cmds[i].name) != 0)
            i++;
        if(i == NR_CMDS)
            bug("Unknown option '%s'\n", cmd);
        union cmd_arg_t args[MAX_NR_ARGS];
        for(int j = 0; j < g_cmds[i].nr_args; j++)
        {
            args[j].str = next(user);
            if(args[j].str == NULL)
                bug("Option '%s' requires %d arguments, only %d given\n", cmd, g_cmds[i].nr_args, j);
            if(g_cmds[i].arg_type[j] == ARG_UINT)
            {
                char *end;
                args[j].uint = strtoul(args[j].str, &end, 0);
                if(*end)
                    bug("Option '%s' expects an integer as argument %d\n", cmd, j + 1);
            }
        }
        int ret = g_cmds[i].fn(sb, args);
        if(ret < 0)
            return ret;
    }
    return 0;
}

static void usage(void)
{
    printf("Usage: elftosb1 [options]\n");
    printf("Options:\n");
    printf("  -h/-?/-help\t\t\tDisplay this message\n");
    printf("  -o <file>\t\t\tSet output file\n");
    printf("  -d/-debugon\t\t\tEnable debug output\n");
    printf("  -k <file>\t\t\tSet key file\n");
    printf("  -load-binary <file> <addr>\tLoad a binary file at a specified address\n");
    printf("  -drive-tag <tag>\t\tSpecify drive tag\n");
    printf("  -w/-switchdriver <driver>\tSwitch driver\n");
    printf("  -sdram <chip select> <size>\tInit SDRAM\n");
    printf("  -jumparg <uint>\t\tSet jumpparg for jump and jumpreturn\n");
    printf("  -f/-load <file>\t\tLoad a ELF file\n");
    printf("  -r/-loadjumpreturn <file>\tLoad a ELF file and call it\n");
    printf("  -j/-loadjump <file>\t\tLoad a ELF file and jump to it\n");
    printf("  -R/-rom <uint>\t\tSet ROM version\n");
    printf("  -p/-product <ver>\t\tSet product version (xxx.xxx.xxx)\n");
    printf("  -v/-component <ver>\t\tSet component version (xxx.xxx.xxx)\n");
    printf("  -c/-critical\t\t\tSet critical flag\n");
    printf("  -C/-noncritical\t\tClear critical flag\n");
    printf("  -n/-strict\t\t\tSet strict flag\n");
    printf("  -N/-nonstrict\t\t\tClear strict flag\n");
    printf("  -call <addr>\t\t\tCall code at a specified address\n");
    printf("  -jump <addr>\t\t\tJump to code at a specified address\n");

    exit(1);
}

int main(int argc, char **argv)
{
    if(argc <= 1)
        usage();

    struct sb1_file_t *sb = create_sb1_file();

    struct cmd_line_next_arg_user_t u;
    u.argc = argc - 1;
    u.argv = argv + 1;
    int ret = apply_args(sb, &cmd_line_next_arg, &u);
    if(ret < 0)
    {
        sb1_free(sb);
        return ret;
    }

    if(!g_output_file)
        bug("You must specify an output file\n");
    if(!g_final)
    {
        if(g_strict)
            bug("There is no final command in this command stream!\n");
        else
            printf("Warning: there is no final command in this command stream!\n");
    }

    enum sb1_error_t err = sb1_write_file(sb, g_output_file);
    if(err != SB1_SUCCESS)
        printf("Error: %d\n", err);

    return ret;
}

