/*
  sfbprog.cpp: 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 sfbprog.cpp Linux host-side support for burning 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 'sfbprog' program
     IS NOT.  It includes code from the GNU GPL-licensed 'avrdude'
     program, and is therefore licensed under the FULL GNU GPL.
*/

/** \cond */
extern void startupEarlyInits(); // In startup.cpp
extern void startupLateInits(); // In startup.cpp
/** \endcond */

#include <ctype.h>              /* For isupper, isprint */
#include <errno.h>              /* For errno */
#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(), strtoul */
#include <string.h>             /* For strlen */
#include <sys/stat.h>           /* For stat() */ 
#include <sys/types.h>
#include <unistd.h>             /* For read */

#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 */
#define VERSION 1.0
#define VERSION_STRING "sfbprog-" XSTR(VERSION)

#ifndef DEFAULT_TIMEOUT
#define DEFAULT_TIMEOUT 25   /* seconds */
#endif
#ifndef DEFAULT_SKETCH
#define DEFAULT_SKETCH "sketch.hex"
#endif
#ifndef DEFAULT_PROGRAM_DIRECTORY
#define DEFAULT_PROGRAM_DIRECTORY "."
#endif

char * progname;
int verbose = 0;                /* To be read by ser_* code */

static u8 * memoryImage;
static int memorySize;
static int memoryUsed;
static SFBProvenance * theSketch;
static uptr theSketchStartAddress;
static uptr theSketchLowAddress;
static uptr theSketchHighAddress;
static time_t theSketchModTime = 0;
static u8 programmingFlagsSFB = 0;
static u8 preferredBaudCode = 4;
static u8 lowBaudCode = 0;
static u8 highBaudCode = 6;
static int timeout = -1;
static const char * sketchHexPath = 0;
static const char * programDirectoryPath = 0;
static bool monitorMode = false; // on '-m', means no valid sketch is okay
static char * devices[FACE_COUNT] = { 0, 0, 0, 0 };
static union filedescriptor deviceFileDescriptor[FACE_COUNT];
static union filedescriptor reportFileDescriptor;
static bool results[FACE_COUNT] = { false, false, false, false };
static u8 downloadCount = 0;
static u32 lastDataSentTime = 0;
static u32 firstErrorOdds = 0;
static u32 nextErrorOdds = 0;
static const char * boardOwnerIdString = 0;
static const char * boardBoardIdString = 0;

static void quit(int code) {
  serial_close(&reportFileDescriptor); // Need this to reblock stdin on posix; made harmless on w32
  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 unless -m)\n"
          " -m           'Monitor mode' - do not (down)load any compiled .hex sketch file\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"
          "    -t 0 is 'server mode': Run indefinitely, monitoring sketch for changes.\n"  
          " -r [baud rate code] Set preferred baud rate code for negotiation.\n" 
          "\n"
          " -n path   Open path for programming to the north (default '/dev/ttyUSB0')\n"
          " -s path   Open path for programming to the south (default none)\n"
          " -e path   Open path for programming to the east  (default none)\n"
          " -w path   Open path for programming to the west  (default none)\n"
          " -N path   Open path for results to the north     (default none)\n"
          " -S path   Open path for results to the south     (default '-', stdin/out)\n"
          " -E path   Open path for results to the east      (default none)\n"
          " -W path   Open path for results to the west      (default none)\n"
          "\n"
          " -hH? print help\n"
          " -p paranoid: Ask SFBs to ignore block checksums and download all data\n"
          " -v verbose: Extra messages (repeat for even more)\n"
          " -q quiet: Minimal messages\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 SFBPRG)\n"
          " -z firstOdds/nextOdds : Specify noise parameters for serial output (for testing)\n"
          " -V report version\n"
          );
}

static void getArgs(int argc, char ** argv) {
  int c;
  opterr = 0;
  while ((c = getopt(argc, argv, "mqvVphn:s:e:w:N:S:E:W:t:f:d:r:z:o:b:")) != -1) {
    switch (c) {
    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 'r': {
      char * endptr;
      int code = getStandardBaudCode(strtoul(optarg,&endptr,10));
      if (code < 0 || *endptr != 0)
        quit(1,"Illegal baud code or unsupported baud rate to -r");
      preferredBaudCode = code;
      if (preferredBaudCode < lowBaudCode)  lowBaudCode = preferredBaudCode;
      if (preferredBaudCode > highBaudCode) highBaudCode = preferredBaudCode;
      break;
    }
    case 'V':
      quit(0,"%s",VERSION_STRING);
      break;
    case 'm':
      if (monitorMode)
        quit(1,"Duplicate -m");
      monitorMode = true;
      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 'd':
      if (programDirectoryPath)
        quit(1,"Repeated -d (first was -d %s) ",programDirectoryPath);
      if (optarg==0)
        quit(1,"Need program directory path after -d");
      programDirectoryPath = 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 'z':
      if (optarg==0)
        quit(1,"Need error odds (e.g,. '0' or '100' or '500/2') after -z");
      {
        char * end;
        errno = 0;
        int firstOdds = strtol(optarg,&end,10);
        int nextOdds = 0;
        if (*end != 0 && *end != '/')
          quit(1,"-z argument must be decimal number",errno,3);
        if (firstOdds < 0)
          quit(1,"-z argument must be non-negative",0,3);
        if (*end == '/')  {
          nextOdds = strtol(end+1,&end,10);
          if (*end)
            quit(1,"-z second argument must be decimal number",errno,3);
          if (nextOdds < 0)
            quit(1,"-z second argument must be non-negative",0,3);
        }
        firstErrorOdds = firstOdds;
        nextErrorOdds = nextOdds;
      }
      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 'n':case 's':case 'e':case 'w': 
    case 'N':case 'S':case 'E':case 'W': {
      int face = FACE_NUMBER_FROM_CODE(c);
      if (devices[face] != 0)
        quit(1,"Duplicate -nsewNSEW switch");
      devices[face] = optarg;
      results[face] = isupper(c);
      break;
    }
    case '?':
      switch (optopt) {
      case 'f': quit(1,"Need hex file path after -f");
      case 'r': quit(1,"Need baud code or supported baud rate after -r");
      case 't': quit(1,"Need timeout (positive decimal seconds or 0 for server mode) after -t");
      case 'n': case 's': case 'e': case 'w': case 'N': case 'S':  case 'E':  case 'W': 
        quit(1,"Need serial device after -n -s -e -w -N -S -E -W");
      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:
      abort ();
    }
  }
}

extern void eeprom_set_directoryImpl(const char * dirPath);

static void preStartupArgProcessing() {

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

static void postStartupArgProcessing() {

  ///
  // Default sketch path

  if (monitorMode && sketchHexPath) {
    logQuiet("[Monitor mode only: Ignoring sketch '%s']\n",sketchHexPath);
    sketchHexPath = 0;
  }

  if (sketchHexPath==0 && !monitorMode) {
    logVerbose("[Defaulting sketchHexPath to %s]\n",DEFAULT_SKETCH);
    sketchHexPath = DEFAULT_SKETCH;
  }

  ///
  // Default program directory

  if (programDirectoryPath==0) {
    logVerbose("[Defaulting programDirectoryPath to %s]\n",DEFAULT_PROGRAM_DIRECTORY);
    programDirectoryPath = DEFAULT_PROGRAM_DIRECTORY;
  }

  ///
  // Access or init 'eeprom'

  eeprom_set_directoryImpl(programDirectoryPath);

  eeprom_startup_initialization();

  ///
  // Default board owner

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

  u32 eownerid = getBootBlockOwnerId();
  if (eownerid == 0) {
    const char * uname = getenv("USER");
    if (!uname) uname = "nobody";
    u32 len = strlen(uname)+1;
    char * buf = (char*) malloc(len+1);
    while (--len > 0) {         // Let's silently rtrim too-long user names...
      strncpy(buf,uname,len);
      buf[len] = 0;
      u32 id;
      if (b36ify(buf,id)) {
        eownerid = id;
        break;
      }
    }
    if (eownerid == 0)
      quit(1,"can't make valid ownerid, rerun with -o OWNERid");
    setBootBlockFlags(getBootBlockFlags()|BOOTBLOCK_UNLOCKED);
    setBootBlockOwnerId(eownerid);
  }

  ///
  // Default board id

  if (boardBoardIdString != 0) {
    u32 boardId;
    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);
  }
  u32 eboardid = getBootBlockBoardId();
  if (eboardid == 0) {
    b36ify("SFBPRG",eboardid);
    setBootBlockFlags(getBootBlockFlags()|BOOTBLOCK_UNLOCKED);
    setBootBlockBoardId(eboardid);
  }

  ///
  // 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);
  }
}

bool bridging = true;
void commandHandler(u8 * packet) {
  if (zPacketPrefix(packet,"quit")) quit(0);
  else if (zPacketPrefix(packet,"bon")) bridging = true;
  else if (zPacketPrefix(packet,"boff")) bridging = false;
  else
    Body.triggerUp();
}

static void broadcastSketchInfo() {
  for (u32 i = NORTH; i <= WEST; ++i) {
    if (devices[i] == 0) continue;
    if (!results[i]) {
      printPhSketchInfo(i,theSketch);
    }
  }
}

static void startup() {

  startupEarlyInits();

  u32 progDevs = 0;
  u32 reportDevs = 0;
  for (u32 i = NORTH; i <= WEST; ++i) {
    if (devices[i] == 0) continue;
    if (results[i]) {
      if (reportDevs > 0)
        quit(3,"Reporting to multiple faces not yet implemented, sorry");

      if (strcmp(devices[i],"-")==0) {

        serial_open(devices[i],9600,&reportFileDescriptor); // Baud rate not actually used for "-"
        Faces[i].getHWSerial().setUFD(&reportFileDescriptor);
        Faces[i].begin(9600,false); // Baud rate is actually phony - just want conscious control
        Faces[i].otherwise(commandHandler);

        setLogFace(i);            // Automatically log to the reporting face

        ++reportDevs;
      } else {
        quit(3,"Reporting via other than stdin/out ('-') not yet implemented, sorry");
      }
    } else {

      serial_open(devices[i],9600,&deviceFileDescriptor[i]); // Start at 9600; will switch

      //int tryFD = open_serial(devices[i]);
      //if (tryFD < 0)
      //quit(3,"Unable to open serial devices %s: %s",devices[i],strerror(errno));
      logVerbose("[Opened %s for %s]\n", devices[i], FACE_NAME(i));

      Faces[i].getHWSerial().setLowBaudCode(lowBaudCode);
      Faces[i].getHWSerial().setHighBaudCode(highBaudCode);
      Faces[i].setPreferredBaudCode(preferredBaudCode);

      Faces[i].getHWSerial().setUFD(&deviceFileDescriptor[i]);
      if (firstErrorOdds) {
        logNormal("[WARNING: SETTING NOISE PARAMETERS -z %d/%d on %s for %s]\n", 
                  firstErrorOdds, nextErrorOdds,
                  devices[i], FACE_NAME(i));
        Faces[i].getHWSerial().setNoiseParameters(firstErrorOdds,nextErrorOdds);
      }
      Faces[i].begin();
      ++progDevs;
    }
  }
  if (progDevs==0) {
    quit(3,"No programming devices found (need at least one of -n -s -e -w)");
  }
}

#define VERBOSE() (getLogLevel()>=LOGLEVEL_VERBOSE)
#define DEBUG() (getLogLevel()>=LOGLEVEL_DEBUG)
#define DROOL() (getLogLevel()>=LOGLEVEL_DEBUG+2)

static void packetBridge(u8 * packet) {
  if (!bridging && packet[0]!='L')
    return;
  if (monitorMode && packet[0]=='P') // Eat programming packets if we're just monitoring
    return;
  u32 from = Body.source();    // Get source of packet
  for (u32 face = NORTH; face <= WEST; ++face) {
    if (face==from) continue;   // Don't send back to where it came from
    if (DROOL())
      logDebug("Bridging %s packet to %s: '%p'\n",FACE_NAME(from),FACE_NAME(face),packet);
    Faces[face].println(packet,packetLength(packet)); // Otherwise forward it on
  }
}

static time_t getModTime(const char * path) {
  static int tries = 0;
  struct stat buf;
  if (stat(path,&buf)) {
    if (errno==ENOENT) {        // Allow a little slack if the file's disappeared..
      if (theSketchModTime==0 || ++tries > 10)
        quit(4,"Can't stat '%s' to get modification time: %s",path,strerror(errno));
      else
        return theSketchModTime;
    }
  }
  tries = 0;
  return buf.st_mtime;
}

static void findSketch() {
  if (monitorMode)
    return;

  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[(uptr) 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));
  }

  theSketchModTime = getModTime(fname);

  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();
}

void checkUpdate() {
  if (!monitorMode && getModTime(sketchHexPath) > theSketchModTime) {
    logVerbose("[Sketch modification time changed; reloading]\n");
    loadHex(sketchHexPath);
  }
}

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

  getArgs(argc, argv);

  preStartupArgProcessing();

  startup();

  postStartupArgProcessing();

  onceOnlyInit();

  if (!monitorMode)
    loadHex(sketchHexPath);

  u32 startMs = millis();
  u32 endMs = startMs + timeout*1000;
  while (timeout==0 || IS_EARLIER(millis(),lastDataSentTime+1000) 
         || (downloadCount==0 && IS_EARLIER(millis(),endMs))) {
    if (timeout==0) 
      checkUpdate();
    else
      broadcastSketchInfo();

    u32 block = millis()+1000;
    while (IS_EARLIER(millis(),block)) {
      for (u8 i = NORTH; i <= WEST; ++i) {
        Faces[i].dispatch(0, 10);
      }
      delay(10);
      serial_poll();
    }
  }
  if (!downloadCount)
    logQuiet("[Timeout with no successful downloads]\n");
  quit(!downloadCount);
}


/** \endcond */
