///////////////////////////////////////////////////////////////////////////////
// BOSSA
//
// Copyright (c) 2011-2018, ShumaTech
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the <organization> nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
///////////////////////////////////////////////////////////////////////////////
#include <string>
#include <exception>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <sys/time.h>
#include <unistd.h>

#include "CmdOpts.h"
#include "Samba.h"
#include "PortFactory.h"
#include "Device.h"
#include "Flasher.h"

using namespace std;

class BossaConfig
{
public:
    BossaConfig();
    virtual ~BossaConfig() {}

    bool erase;
    bool write;
    bool read;
    bool verify;
    bool offset;
    bool reset;
    bool port;
    bool boot;
    bool bor;
    bool bod;
    bool lock;
    bool unlock;
    bool security;
    bool info;
    bool debug;
    bool usbPort;
    bool arduinoErase;
    bool help;
    bool version;

    int readArg;
    int offsetArg;
    string portArg;
    int bootArg;
    int bodArg;
    int borArg;
    string lockArg;
    string unlockArg;
    bool usbPortArg;
};

BossaConfig::BossaConfig()
{
    erase = false;
    write = false;
    read = false;
    verify = false;
    port = false;
    boot = false;
    bod = false;
    bor = false;
    lock = false;
    security = false;
    info = false;
    usbPort = false;
    arduinoErase = false;
    help = false;
    version = false;

    readArg = 0;
    offsetArg = 0;
    bootArg = 1;
    bodArg = 1;
    borArg = 1;
    usbPortArg=1;

    reset = false;
}

class BossaObserver : public FlasherObserver
{
public:
    BossaObserver() : _lastTicks(-1) {}
    virtual ~BossaObserver() {}

    virtual void onStatus(const char *message, ...);
    virtual void onProgress(int num, int div);
private:
    int _lastTicks;
};

void
BossaObserver::onStatus(const char *message, ...)
{
    va_list ap;

    va_start(ap, message);
    vprintf(message, ap);
    va_end(ap);
}

void
BossaObserver::onProgress(int num, int div)
{
    int ticks;
    int bars = 30;

    ticks = num * bars / div;

    if (ticks == _lastTicks)
        return;

    printf("\r[");
    while (ticks-- > 0)
    {
        putchar('=');
        bars--;
    }
    while (bars-- > 0)
    {
        putchar(' ');
    }
    printf("] %d%% (%d/%d pages)", num * 100 / div, num, div);
    fflush(stdout);

    _lastTicks = 0;
}

static BossaConfig config;
static Option opts[] =
{
    {
      'e', "erase", &config.erase,
      { ArgNone },
      "erase the entire flash starting at the offset"
    },
    {
      'w', "write", &config.write,
      { ArgNone },
      "write FILE to the flash; accelerated when\n"
      "combined with erase option"
    },
    {
      'r', "read", &config.read,
      { ArgOptional, ArgInt, "SIZE", { &config.readArg } },
      "read SIZE from flash and store in FILE;\n"
      "read entire flash if SIZE not specified"
    },
    {
      'v', "verify", &config.verify,
      { ArgNone },
      "verify FILE matches flash contents"
    },
    {
      'o', "offset", &config.offset,
      { ArgRequired, ArgInt, "OFFSET", { &config.offsetArg } },
      "start erase/write/read/verify operation at flash OFFSET;\n"
      "OFFSET must be aligned to a flash page boundary"
    },
    {
      'p', "port", &config.port,
      { ArgRequired, ArgString, "PORT", { &config.portArg } },
      "use serial PORT to communicate to device;\n"
      "default behavior is to use first serial port"
    },
    {
      'b', "boot", &config.boot,
      { ArgOptional, ArgInt, "BOOL", { &config.bootArg } },
      "boot from ROM if BOOL is 0;\n"
      "boot from FLASH if BOOL is 1 [default];\n"
      "option is ignored on unsupported devices"
    },
    {
      'c', "bod", &config.bod,
      { ArgOptional, ArgInt, "BOOL", { &config.bodArg } },
      "no brownout detection if BOOL is 0;\n"
      "brownout detection is on if BOOL is 1 [default]"
    },
    {
      't', "bor", &config.bor,
      { ArgOptional, ArgInt, "BOOL", { &config.borArg } },
      "no brownout reset if BOOL is 0;\n"
      "brownout reset is on if BOOL is 1 [default]"
    },
    {
      'l', "lock", &config.lock,
      { ArgOptional, ArgString, "REGION", { &config.lockArg } },
      "lock the flash REGION as a comma-separated list;\n"
      "lock all if not given [default]"
    },
    {
      'u', "unlock", &config.unlock,
      { ArgOptional, ArgString, "REGION", { &config.unlockArg } },
      "unlock the flash REGION as a comma-separated list;\n"
      "unlock all if not given [default]"
    },
    {
      's', "security", &config.security,
      { ArgNone },
      "set the flash security flag"
    },
    {
      'i', "info", &config.info,
      { ArgNone },
      "display device information"
    },
    {
      'd', "debug", &config.debug,
      { ArgNone },
      "print debug messages"
    },
    {
      'U', "usb-port", &config.usbPort,
      { ArgOptional, ArgInt, "BOOL", { &config.usbPortArg } },
      "force serial port detection to USB if BOOL is 1 [default]\n"
      "or to RS-232 if BOOL is 0"
    },
    {
      'R', "reset", &config.reset,
      { ArgNone },
      "reset CPU (if supported)"
    },
    {
      'a', "arduino-erase", &config.arduinoErase,
      { ArgNone },
      "erase and reset via Arduino 1200 baud hack"
    },
    {
      'h', "help", &config.help,
      { ArgNone },
      "display this help text"
    },
    {
      'V', "version", &config.version,
      { ArgNone },
      "display version info"
    },
};

int
help(const char* program)
{
    fprintf(stderr, "Try '%s -h' or '%s --help' for more information\n", program, program);
    return 1;
}

static struct timeval start_time;

void
timer_start()
{
    gettimeofday(&start_time, NULL);
}

float
timer_stop()
{
    struct timeval end;
    gettimeofday(&end, NULL);
    return (float)((end.tv_sec - start_time.tv_sec) + (end.tv_usec - start_time.tv_usec) / 1000000.0);
}

int
main(int argc, char* argv[])
{
    int args;
    char* pos;
    CmdOpts cmd(argc, argv, sizeof(opts) / sizeof(opts[0]), opts);

    if ((pos = strrchr(argv[0], '/')) || (pos = strrchr(argv[0], '\\')))
        argv[0] = pos + 1;

    if (argc <= 1)
    {
        fprintf(stderr, "%s: you must specify at least one option\n", argv[0]);
        return help(argv[0]);
    }

    args = cmd.parse();
    if (args < 0)
        return help(argv[0]);

    if (config.read && (config.write || config.verify))
    {
        fprintf(stderr, "%s: read option is exclusive of write or verify\n", argv[0]);
        return help(argv[0]);
    }

    if (config.read || config.write || config.verify)
    {
        if (args == argc)
        {
            fprintf(stderr, "%s: missing file\n", argv[0]);
            return help(argv[0]);
        }
        argc--;
    }
    if (args != argc)
    {
        fprintf(stderr, "%s: extra arguments found\n", argv[0]);
        return help(argv[0]);
    }

    if (config.help || config.version)
    {
        if (config.help)
            printf("Usage: %s [OPTION...] [FILE]\n", argv[0]);
        printf("Basic Open Source SAM-BA Application (BOSSA) Version " VERSION "\n"
               "Flash programmer for Atmel SAM devices.\n"
               "Copyright (c) 2011-2018 ShumaTech (http://www.shumatech.com)\n"
              );
        if (config.help)
        {
            printf("\n"
                   "Examples:\n"
                   "  bossac -e -w -v -b image.bin   # Erase flash, write flash with image.bin,\n"
                   "                                 # verify the write, and set boot from flash\n"
                   "  bossac -r0x10000 image.bin     # Read 64KB from flash and store in image.bin\n"
                  );
            printf("\nOptions:\n");
            cmd.usage(stdout);
            printf("\nReport bugs to <bugs@shumatech.com>\n");
        }
        return 0;
    }

    try
    {
        Samba samba;
        PortFactory portFactory;

        if (config.debug)
            samba.setDebug(true);

        if (!config.port)
            config.portArg = portFactory.def();

        if (config.arduinoErase)
        {
            SerialPort::Ptr port;
            port = portFactory.create(config.portArg, config.usbPortArg != 0);

            printf("Arduino 1200 baud reset\n");
            if(!port->open(1200))
            {
                fprintf(stderr, "Failed to open port at 1200bps\n");
                return 1;
            }

            port->setRTS(true);
            port->setDTR(false);
            port->close();

            // wait for chip to reboot and USB port to re-appear
            sleep(1);

            if (config.debug)
                printf("Arduino reset done\n");
        }

        if (config.portArg.empty())
        {
            fprintf(stderr, "No serial ports available\n");
            return 1;
        }

        bool res;
        if (config.usbPort)
            res = samba.connect(portFactory.create(config.portArg, config.usbPortArg != 0));
        else
            res = samba.connect(portFactory.create(config.portArg));
        if (!res)
        {
            fprintf(stderr, "No device found on %s\n", config.portArg.c_str());
            return 1;
        }

        Device device(samba);
        device.create();

        Device::FlashPtr& flash = device.getFlash();

        BossaObserver observer;
        Flasher flasher(samba, device, observer);

        if (config.info)
        {
            FlasherInfo info;
            flasher.info(info);
            info.print();
        }

        if (config.unlock)
            flasher.lock(config.unlockArg, false);

        if (config.erase)
        {
            timer_start();
            flasher.erase(config.offsetArg);
            printf("\nDone in %5.3f seconds\n", timer_stop());
        }

        if (config.write)
        {
            timer_start();
            flasher.write(argv[args], config.offsetArg);
            printf("\nDone in %5.3f seconds\n", timer_stop());
        }

        if (config.verify)
        {
            uint32_t pageErrors;
            uint32_t totalErrors;

            timer_start();
            if (!flasher.verify(argv[args], pageErrors, totalErrors, config.offsetArg))
            {
                printf("\nVerify failed\nPage errors: %d\nByte errors: %d\n",
                    pageErrors, totalErrors);
                return 2;
            }

            printf("\nVerify successful\nDone in %5.3f seconds\n", timer_stop());
        }

        if (config.read)
        {
            timer_start();
            flasher.read(argv[args], config.readArg, config.offsetArg);
            printf("\nDone in %5.3f seconds\n", timer_stop());
        }

        if (config.boot)
        {
            printf("Set boot flash %s\n", config.bootArg ? "true" : "false");
            flash->setBootFlash(config.bootArg != 0);
        }

        if (config.bod)
        {
            printf("Set brownout detect %s\n", config.bodArg ? "true" : "false");
            flash->setBod(config.bodArg != 0);
        }

        if (config.bor)
        {
            printf("Set brownout reset %s\n", config.borArg ? "true" : "false");
            flash->setBor(config.borArg != 0);
        }

        if (config.security)
        {
            printf("Set security\n");
            flash->setSecurity();
        }

        if (config.lock)
            flasher.lock(config.lockArg, true);

        flash->writeOptions();

        if (config.reset) {
            // Ensure flash operation are complete prior to reset
            flash->ready();
            device.reset();
        }
    }
    catch (exception& e)
    {
        fprintf(stderr, "\n%s\n", e.what());
        return 1;
    }
    catch(...)
    {
        fprintf(stderr, "\nUnhandled exception\n");
        return 1;
    }

    return 0;
}

