/*
  sfbdl.cpp: Minimal host side support for programming via the tertiary bootloader
  Copyright (C) 2009 The Regents of the University of New Mexico.  All rights reserved.

  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 program is distributed in the hope that it will be useful, but
  WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301
  USA
*/

/**
  \file sfbdl.cpp Minimal cross-platform host-side support for downloading through the tertiary bootloader
  \author David H. Ackley.  
  \date (C) 2009 All rights reserved.
  \gpl

  WARNING!  LICENSING NOTE: 
  
     PLEASE BE AWARE THAT although most of the SFB system software is
     licensed under the GNU LGPL -- the 'lesser' general public
     license, allowing more flexible reuse -- this 'sfbdl' program
     IS NOT.  It includes code from the GNU GPL-licensed 'avrdude'
     program, and is therefore licensed under the FULL GNU GPL.
*/

#include <ctype.h>              /* For isupper, isprint */
#include <errno.h>              /* For errno */
#include <unistd.h>             /* For getopt */
#include <fcntl.h>              /* For fnctl */
#include <limits.h>             /* For LONG_MAX */
#include <stdarg.h>             /* For va_list etc */
#include <stdio.h>              /* For printf */
#include <stdlib.h>             /* For exit() */
#include <string.h>             /* For strlen */
#include <sys/types.h>

#include "SFBBootBlock.h"       /* For getBootBlockOwnerId, etc */
#include "SFBCodeFlow.h"        /* For findProvenance */
#include "SFBEeprom.h"          /* For eeprom_startup_initialization */
#include "SFBPrintf.h"          /* For logging stuff */ 
#include "SFBProvenance.h"      /* For SFBProvenance */
#include "SFBReflex.h"
#include "SFBSerial.h"
#include "SFBWiring.h"          /* For delay() */

#include "fileio.h"             /* For fileio_ihex */ 
#include "serial.h"             /* For union filedescriptor */ 

/** \cond Don't document any of this (yet anyway) */
#define VERSION 1.0
#define VERSION_STRING "sfbdl-" XSTR(VERSION)

#ifndef DEFAULT_TIMEOUT
#define DEFAULT_TIMEOUT 30   /* seconds.  Note this is timout-to-BEGIN-download, not to finish it */
#endif
#ifndef DEFAULT_SKETCH
#define DEFAULT_SKETCH "sketch.hex"
#endif
#ifndef DEFAULT_PROGRAM_DIRECTORY
#define DEFAULT_PROGRAM_DIRECTORY "."
#endif

extern void startupEarlyInits(); // In startup.cpp
extern void startupLateInits(); // In startup.cpp

/////
/// Decls provided for avrdude code's use
char * progname;
int verbose = 0;                /* To be read by ser_* code */

static u8 * memoryImage;
static int memorySize;
static int memoryUsed;
static SFBProvenance * theSketch;
static u32 theSketchStartAddress;
static u32 theSketchLowAddress;
static u32 theSketchHighAddress;
static u8 programmingFlagsSFB = 0;
static u8 stdoutFace = 0xff;
static int timeout = -1;
static const char * sketchHexPath = 0;
static char * device = 0;
static union filedescriptor deviceFileDescriptor;
static bool downloadActive = false;
static u8 downloadCount = 0;
static u32 lastAnnouncedTime = 0;
static const char * boardOwnerIdString = 0;
static const char * boardBoardIdString = 0;
static bool stdinBlocking = true;
static void setStdinBlocking(bool set); 


static void quit(int code) {
  if (!stdinBlocking) setStdinBlocking(true); // Clean up behind ourselves
  exit(code);
}

static void quit(int code, const char * format, ...) {
  va_list ap;
  va_start(ap,format);
  vfprintf(stderr,format,ap);
  fprintf(stderr,"\n");
  quit(code);
}

static bool b36ify(const char * buf, u32 & result) {
  char * endptr;
  u32 val = strtol(buf,&endptr,36);

  if (*endptr != 0)
    return false;

  if (val == LONG_MAX && errno != 0) 
    return false;

  result = val;
  return true;
}


static void printHelp() {
  fprintf(stderr,"%s",
          VERSION_STRING "\n"
          "Options are: \n"
          " -f hexfile   Specify path to compiled .hex sketch file (required)\n"
          " -t {seconds}   Set programming timeout to seconds.  Default " XSTR(DEFAULT_TIMEOUT) " seconds\n"
          "                Exit 0 on successful burn, non-zero on timeout or failure\n"
          " -d path   Path to serial device connected to SFB (required)\n"
          "\n"
          " -hH print help\n"
          " -p paranoid: Ask SFBs to ignore block checksums and download all data\n"
          " -v verbose: More messages (repeat for even more)\n"
          " -q quiet: Less messages (repeat for still less)\n"
          " -o ownerId : sfbprog will report 'ownerId' as its 'board owner' (default username)\n"
          " -b boardId : sfbprog will report 'boardId' as its 'board id' (default SFBDL)\n"
          " -V report version\n"
          );
}

static void getArgs(int argc, char ** argv) {
  int c;
  opterr = 0;
  while ((c = getopt(argc, argv, "mqvVphHt:f:d:o:b:")) != -1) {
    switch (c) {
    case 'h':
    case 'H':
      printHelp();
      exit(0);

    case 'q':
      if (getLogLevel() > LOGLEVEL_NORMAL)
        quit(1,"Cannot specify -q and -v together");

      if (getLogLevel()==LOGLEVEL_NORMAL)
        setLogLevel(LOGLEVEL_QUIET);
      else if (getLogLevel()==LOGLEVEL_QUIET && (programmingFlagsSFB&PROGRAMMING_FLAG_QUIET)==0)
        programmingFlagsSFB |= PROGRAMMING_FLAG_QUIET;
      else 
        setLogLevel(LOGLEVEL_NONE);

      break;
    case 'v':
      if (getLogLevel() < LOGLEVEL_NORMAL)
        quit(1,"Cannot specify -v and -q together");

      if (getLogLevel()==LOGLEVEL_NORMAL)
        setLogLevel(LOGLEVEL_VERBOSE);
      else if (getLogLevel()==LOGLEVEL_VERBOSE && (programmingFlagsSFB&PROGRAMMING_FLAG_VERBOSE)==0)
        programmingFlagsSFB |= PROGRAMMING_FLAG_VERBOSE;
      else
        setLogLevel(getLogLevel() + 1);

      break;
    case 'p':
      programmingFlagsSFB |= PROGRAMMING_FLAG_CHECKOFF;
      break;
    case 'V':
      quit(0,"%s",VERSION_STRING);
      break;
    case 'f':
      if (sketchHexPath)
        quit(1,"Repeated -f (first was -f %s) ",sketchHexPath);
      if (optarg==0)
        quit(1,"Need sketch hex file path after -f");
      sketchHexPath = optarg;
      break;

    case 'o':
      if (boardOwnerIdString)
        quit(1,"Repeated -o (first was -o %s) ",boardOwnerIdString);
      if (optarg==0)
        quit(1,"Need owner id after -o");
      boardOwnerIdString = optarg;
      break;

    case 'b':
      if (boardBoardIdString)
        quit(1,"Repeated -b (first was -b %s) ",boardBoardIdString);
      if (optarg==0)
        quit(1,"Need board id after -b");
      boardBoardIdString = optarg;
      break;

    case 't':
      if (timeout>=0) 
        quit(1,"Repeated -t");
      if (optarg==0)
        timeout = DEFAULT_TIMEOUT;
      else {
        char * end;
        errno = 0;
        timeout = strtol(optarg,&end,10);
        if (*end)
          quit(1,"-t optional argument must be decimal number",errno,3);
        if (timeout < 0)
          quit(1,"-t argument must be non-negative",0,3);
      }
      break;
    case 'd':
      if (device != 0)
        quit(1,"Duplicate -d switch");
      device = optarg;
      break;

    case '?':
      switch (optopt) {
      case 'f': quit(1,"Need hex file path after -f");
      case 't': quit(1,"Need timeout (positive decimal seconds) after -t");
      case 'd': 
        quit(1,"Need serial device after -d");
      case 0: break;
      default:
        abort();
      }
      if (isprint (optopt))
        fprintf (stderr, "Option `-%c' ", optopt);
      else
        fprintf (stderr,
                 "Option character `\\x%x'",
                 optopt);
      quit(1,"unrecognized; use -h for help");
      break;

    default:
      printHelp();
      exit(1);
    }
  }
}

static void preStartupArgProcessing() {

  ///
  // Check for sketch path

  if (sketchHexPath==0) 
    quit(2,"'-f hexfile' argument required");

  ///
  // Check for device

  if (device==0) 
    quit(1,"'-d path' argument required");

  ///
  // Move verbosity info to where ser_* code can see it
  verbose = getLogLevel();

}

#ifndef OS
#define OS UNDEF
#endif

static void postStartupArgProcessing() {

  ///
  // Access or init 'eeprom'

  eeprom_startup_initialization();

  ///
  // Default board owner

  u32 ownerId;
  if (boardOwnerIdString == 0) 
    boardOwnerIdString = XSTR(OS);
  if (!b36ify(boardOwnerIdString,ownerId)) 
    quit(1,"invalid -o OWNERID '%s' (must be brief and all 0-9A-Z)",boardOwnerIdString);
  setBootBlockFlags(getBootBlockFlags()|BOOTBLOCK_UNLOCKED);
  setBootBlockOwnerId(ownerId);

  ///
  // Default board id

  u32 boardId;
  if (boardBoardIdString == 0) 
    boardBoardIdString = "sfbdl";

  if (!b36ify(boardBoardIdString,boardId)) 
    quit(1,"invalid -b BOARDID '%s' (must be short and all 0-9A-Z)",boardBoardIdString);

  setBootBlockFlags(getBootBlockFlags()|BOOTBLOCK_UNLOCKED);
  setBootBlockBoardId(boardId);

  ///
  // Default burn timeout

  if (timeout < 0) {
    logVerbose("[Defaulting timeout to %d]\n",DEFAULT_TIMEOUT);
    timeout = DEFAULT_TIMEOUT;
  } else if (timeout == 0) {
    logNormal("[Running in server mode]\n");
  } else {
    logVerbose("[Timeout = %d seconds]\n",timeout);
  }
}

static void setStdinBlocking(bool set) {
  #if 0
  stdinBlocking = set;

  int flags = fcntl(0, F_GETFL);
  if (set)  flags &= ~O_NONBLOCK;
  else      flags |= O_NONBLOCK;

  if (fcntl(0, F_SETFL, flags) != 0) 
    quit(2,"stdinBlock: Unable to set stdin blocking: %s",strerror(errno));
  #endif
}

static void broadcastSketchInfo() {
  printPhSketchInfo(SOUTH,theSketch);
}

static void facePrintStdout(u8, u8 byte) { printf("%c",byte); }
static void facePrintlnStdout(u8) { printf("\n"); }
static void facePrintFaceCodeStdout(u8) { printf("-"); }
static const char * faceGetFaceNameStdout(u8) { return "stdout"; }

static const FacePrinter stdoutPrinter = {
  facePrintStdout,
  facePrintlnStdout,
  facePrintFaceCodeStdout,
  faceGetFaceNameStdout
};

static void programStatusCallback(u8 face, CodeFlowStatus status) {
  if (face!=SOUTH) return;       // WTH?
  switch (status) {
  case CODE_FLOW_SUCCEEDED:     // Yay!
    ++downloadCount;            // Note success
    // FALL THROUGH

  case CODE_FLOW_IDLE:          // Nothing happening
    downloadActive = false;
    break;

  case CODE_FLOW_ACTIVE:        // Note something's going
    downloadActive = true;      // ..so we won't timeout midflow
    break;
  default:                      // Any other states
    break;                      // ..we don't care
  }
}


static void startup() {
  startupEarlyInits();

  //////
  /// Set up a virtual face for logging to stdout
  if (!faceFindFreeFace(stdoutFace)) 
    quit(6,"No free faces?");
  faceSetPrinter(stdoutFace,&stdoutPrinter);
  setLogFace(stdoutFace);

  //////
  /// Connect SOUTH to serial device for programming

  serial_open(device,9600,&deviceFileDescriptor); // Start at 9600; will switch

  logVerbose("[Opened %s]\n", device);
  Faces[SOUTH].getHWSerial().setLowBaudCode(SFBAUD1200);
  Faces[SOUTH].getHWSerial().setHighBaudCode(SFBAUD115200);
  Faces[SOUTH].getHWSerial().setUFD(&deviceFileDescriptor);
  Faces[SOUTH].begin();

  //////
  /// Register our callback for successful programming
  setCodeFlowStatusCallback(programStatusCallback);

}

#define QUIET() (getLogLevel()>=LOGLEVEL_QUIET)
#define NORMAL() (getLogLevel()>=LOGLEVEL_NORMAL)
#define VERBOSE() (getLogLevel()>=LOGLEVEL_VERBOSE)
#define DEBUG() (getLogLevel()>=LOGLEVEL_DEBUG)
#define DROOL() (getLogLevel()>=LOGLEVEL_DEBUG+2)

static void packetBridge(u8 * packet) {
  if (packet[0]!='L' || packetLength(packet) < 2)
    return;

  switch (packet[1]) {  // Bridge the packet if we're logging at its log level 
  case 'n': if (NORMAL()) goto log; else break;
  case 'v': if (VERBOSE()) goto log; else break;
  case 'q': if (QUIET()) goto log; else break;
  case 'd': if (DEBUG()) goto log; else break;
  log:
    facePrintln(stdoutFace,packet,packetLength(packet));
  }
}

static void findSketch() {

  SFBProvenance * p = findProvenance((uptr) memoryImage, ((uptr) memoryImage)+memoryUsed, CHECKSUM_GENERATE);
  if (!p) {
    logQuiet("Error: No provenance found in %s\n",sketchHexPath);
    quit(5,"Unknown provenance");
  }
  if (p->architecture == 0) {
    logQuiet("Error: '%s' architecture code is 0\n",sketchHexPath);
    quit(5,"Unknown/unspecified architecture code",0,1);
  }

  theSketchStartAddress = (uptr)p - (uptr)memoryImage;
  theSketchLowAddress = theSketchStartAddress&~0xff; // Round down to multiple of 256
  theSketchHighAddress = ((theSketchStartAddress+p->programLength)|0xff)+1; // Round up ditto

  logVerbose("[Start=0x%x, End=0x%x, Size=%d]\n", 
         theSketchStartAddress, memoryUsed, memoryUsed-theSketchStartAddress);

  logNormal("[Loaded %t:%t/%x-%x Size=%d CS=%x-%x Copyright=%s]\n",
            p->creatorId,p->programId,p->buildDate,p->buildTime,
            memoryUsed-theSketchStartAddress,
            p->checksum0, p->checksum1,
            (p->copyrightNotice?&memoryImage[p->copyrightNotice]:0));
  theSketch = p;

  programServerSetSketch(p, theSketchStartAddress, (uptr) memoryImage, programmingFlagsSFB);
  
  broadcastSketchInfo();
}


void loadHex(const char * fname) {
  struct fioparms fio;

  int rc;
  FILE * f;

  rc = fileio_setparms(FIO_READ, &fio);
  if (rc < 0)
    quit(6,"fileio_setparms failed on %s",fname);

  f = fopen(fname, fio.mode);
  if (f == NULL) {
    logQuiet("%s: can't open %s file %s: %s\n",
            progname, fio.iodesc, fname, strerror(errno));
    quit(6,"fopen failed on '%s': %s",fname,strerror(errno));
  }

  memset(memoryImage, 0xff, memorySize);

  memoryUsed = fileio_ihex(&fio, fname, f, memoryImage, memorySize);
  if (memoryUsed < 0)
    quit(6,"hex load of %s failed",fname);

  if ((memoryUsed&3) != 0)
    quit(6,"unaligned length %d, hex load of %s failed",memoryUsed,fname);

  logVerbose("[%s loaded]\n",fname);

  findSketch();
}

static void onceOnlyInit() {
  memorySize = 1<<19;             //  512KB.
  memoryImage = (u8*) malloc(memorySize);

  Body.otherwise(packetBridge);

  startupLateInits();
}

int main(int argc, char ** argv) {
  progname = argv[0];

  getArgs(argc, argv);

  preStartupArgProcessing();

  startup();

  postStartupArgProcessing();

  onceOnlyInit();

  loadHex(sketchHexPath);

  u32 startMs = millis();
  u32 endMs = startMs + timeout*1000;

  while (downloadActive || (downloadCount == 0 && IS_EARLIER(millis(),endMs))) {
    if (!downloadActive && IS_EARLIER(millis(),lastAnnouncedTime+1000)) {
      broadcastSketchInfo();
      lastAnnouncedTime += 1000;
    }
    for (u8 i = NORTH; i <= WEST; ++i) {
      Faces[i].dispatch(0, 10);
    }
    delay(10);
  }

  delay(1000);
  if (!downloadCount)
    logQuiet("[Timeout with no successful downloads]\n");
  quit(!downloadCount);
}

/** \endcond */
