/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 * $Id:$
 *
 * Copyright (C) 2015 by Cástor Muñoz
 *
 * 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.
 *
 ****************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>

#include "mks5lboot.h"

/* Win32 compatibility */
#ifndef O_BINARY
#define O_BINARY 0
#endif

#define DEFAULT_LOOP_PERIOD 1  /* seconds */

#define ERROR(format, ...) \
    do { \
        snprintf(errstr, errstrsize, "[ERR] "format, __VA_ARGS__); \
        goto error; \
    } while(0)

static int write_file(char *outfile, unsigned char* buf,
                            int bufsize, char* errstr, int errstrsize)
{
    int fd = open(outfile, O_CREAT|O_TRUNC|O_WRONLY|O_BINARY, 0666);
    if (fd < 0)
        ERROR("Could not open %s for writing", outfile);

    if (write(fd, buf, bufsize) != bufsize)
        ERROR("Could not write file %s", outfile);

    return 1;

error:
    return 0;
}

static unsigned char *read_file(char *infile, int *bufsize,
                                    char* errstr, int errstrsize)
{
    unsigned char *buf;
    int fd;
    struct stat s;

    fd = open(infile, O_RDONLY|O_BINARY);
    if (fd < 0)
        ERROR("Could not open %s for reading", infile);

    if (fstat(fd, &s) < 0)
        ERROR("Checking size of input file %s", infile);

    *bufsize = s.st_size;

    buf = malloc(*bufsize);
    if (buf == NULL)
        ERROR("Could not allocate memory for %s", infile);

    if (read(fd, buf, *bufsize) != *bufsize)
        ERROR("Could not read file %s", infile);

    return buf;

error:
    return NULL;
}

static void sleep_ms(unsigned int ms)
{
   struct timespec req;
   req.tv_sec = ms / 1000;
   req.tv_nsec = (ms % 1000) * 1000000;
   nanosleep(&req, NULL);
}

static void usage(void)
{
    fprintf(stderr,
    "Usage:\n"
    "  mks5lboot --bl-inst <bootloader.ipod> [-p <pid>] [--single]\n"
    "            --bl-uninst <platform> [-p <pid>]\n"
    "            --dfuscan [--loop [<sec>]] [-p <pid>]\n"
    "            --dfusend <infile.dfu> [-p <pid>]\n"
    "            --dfureset [--loop [<sec>]] [-p <pid>]\n"
    "            --mkdfu-inst <bootloader.ipod> <outfile.dfu> [--single]\n"
    "            --mkdfu-uninst <platform> <outfile.dfu>\n"
    "            --mkdfu-raw <filename.bin> <outfile.dfu>\n"
    "\n"
    "Commands:\n"
    "  --bl-inst    Install file <bootloader.ipod> into an iPod device\n"
    "               (same as --mkdfu-inst and --dfusend).\n"
    "  --bl-uninst  Remove a bootloader from an iPod device (same as\n"
    "               --mkdfu-uninst and --dfusend).\n"
    "\n"
    "  --dfuscan    scan for DFU USB devices and outputs the status.\n"
    "  --dfusend    send DFU image <infile.dfu> to the device.\n"
    "  --dfureset   reset DFU USB device bus.\n"
    "\n"
    "  --mkdfu-inst    Build a DFU image containing an installer for\n"
    "                  <bootloader.ipod>, save it as <outfile.dfu>.\n"
    "  --mkdfu-uninst  Build a DFU image containing an uninstaler for\n"
    "                  <platform> devices, save it as <outfile.dfu>.\n"
    "  --mkdfu-raw     Build a DFU image containing raw executable\n"
    "                  code, save it ass <outfile.dfu>. <infile.bin>\n"
    "                  is the code you want to run, it is loaded at\n"
    "                  address 0x%08x and executed.\n"
    "\n"
    "  <bootloader.ipod> is the rockbox bootloader that you want to\n"
    "  install (previously scrambled with tools/scramble utility).\n"
    "\n"
    "  <platform> is the name of the platform (type of device) for\n"
    "  which the DFU uninstaller will be built. Currently supported\n"
    "  platform names are:\n"
    "    ipod6g:  iPod Classic 6G\n"
    "\n"
    "Options:\n"
    "  -p, --pid <pid>   Use a specific <pid> (Product Id) USB device,\n"
    "                    if this option is ommited then it uses the\n"
    "                    first USB DFU device found.\n"
    "  -l, --loop <sec>  Run the command every <sec> seconds, default\n"
    "                    period (<sec> ommited) is %d seconds.\n"
    "  -S, --single      Be careful using this option. The bootloader\n"
    "                    is installed for single boot, the original\n"
    "                    Apple NOR boot is destroyed (if it exists),\n"
    "                    and only Rockbox can be used.\n"
    , DFU_LOADADDR + BIN_OFFSET
    , DEFAULT_LOOP_PERIOD);

    exit(1);
}

int main(int argc, char* argv[])
{
    char *dfuoutfile = NULL;
    char *dfuinfile = NULL;
    char *dfu_arg = NULL;
    int dfu_type = DFU_NONE;
    int n_cmds = 0;
    int scan = 0;
    int pid = 0;
    int reset = 0;
    int loop = 0;
    int single = 0;
    char errstr[200];
    unsigned char *dfubuf;
    int dfusize;

    fprintf(stderr,
    "mks5lboot Version " VERSION "\n"
    "This is free software; see the source for copying conditions.  There is NO\n"
    "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"
    "\n");
    fflush(stderr);

    while (--argc)
    {
        argv++;
        if (!memcmp(*argv, "--bl", 4)) {
            if      (!strcmp(*argv+4, "-inst"))   dfu_type = DFU_INST;
            else if (!strcmp(*argv+4, "-uninst")) dfu_type = DFU_UNINST;
            else usage();
            if (!--argc) usage();
            dfu_arg = *++argv;
            n_cmds++;
        }
        else if (!memcmp(*argv, "--mkdfu", 7)) {
            if      (!strcmp(*argv+7, "-inst"))   dfu_type = DFU_INST;
            else if (!strcmp(*argv+7, "-uninst")) dfu_type = DFU_UNINST;
            else if (!strcmp(*argv+7, "-raw"))    dfu_type = DFU_RAW;
            else usage();
            if (!--argc) usage();
            dfu_arg = *++argv;
            if (!--argc) usage();
            dfuoutfile = *++argv;
            n_cmds++;
        }
        else if (!strcmp(*argv, "--dfusend")) {
            if (!--argc) usage();
            dfuinfile = *++argv;
            n_cmds++;
        }
        else if (!strcmp(*argv, "--dfuscan")) {
            scan = 1;
            n_cmds++;
        }
        else if (!strcmp(*argv, "--dfureset")) {
            scan = 1;
            reset = 1;
            n_cmds++;
        }
        else if (!strcmp(*argv, "--pid") || !strcmp(*argv, "-p")) {
            if (!--argc) usage();
            if (sscanf(*++argv, "%x", &pid) != 1) usage();
        }
        else if (!strcmp(*argv, "--loop") || !strcmp (*argv, "-l")) {
            if (!(argc-1) || *(argv+1)[0] == '-') {
                loop = DEFAULT_LOOP_PERIOD;
            }
            else {
                if ((sscanf(*++argv, "%d", &loop) != 1) || !loop) usage();
                argc--;
            }
        }
        else if (!strcmp(*argv, "--single") || !strcmp(*argv, "-S")) {
            single = 1;
        }
        else if (!strcmp(*argv, "--debug")) {
            ipoddfu_debug(1);
        }
        else
            usage();
    }

    if (n_cmds != 1)
        usage();

    if ((dfu_type == DFU_INST) && single)
        dfu_type = DFU_INST_SINGLE;

    if (scan) {
        int cnt = 0;
        while (1) {
            int state, res;
            if (loop) printf("[%d] ", cnt);
            else      printf("[INFO] ");
            printf("DFU %s:\n", reset ? "reset":"scan");
            res = ipoddfu_scan(pid, &state, reset, errstr, sizeof(errstr));
            if (res == 0)
                printf("%s\n", errstr);
            else
                printf("[INFO] DFU device state: %d\n", state);
            if (!loop)
                exit(!res);
            fflush(stdout);
            sleep_ms(loop*1000);
            cnt += loop;
        }
    }

    if (dfuinfile)
        dfubuf = read_file(dfuinfile, &dfusize, errstr, sizeof(errstr));
    else
        dfubuf = mkdfu(dfu_type, dfu_arg, &dfusize, errstr, sizeof(errstr));

    if (!dfubuf)
        goto error;

    if (dfuoutfile) {
        if (write_file(dfuoutfile, dfubuf, dfusize, errstr, sizeof(errstr))) {
            printf("[INFO] Created file %s (%d bytes)\n", dfuoutfile, dfusize);
            exit(0);
        }
    }
    else {
        if (ipoddfu_send(pid, dfubuf, dfusize, errstr, sizeof(errstr))) {
            printf("[INFO] DFU image sent successfully (%d bytes)\n", dfusize);
            exit(0);
        }
    }

error:
    printf("%s\n", errstr);
    exit(1);
}
