/*                                              -*- mode:C++; fill-column:100 -*-
  SFBBootBlock.cpp - Persistent boot-time information support
  Copyright (C) 2009 The Regents of the University of New Mexico.  All rights reserved.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library 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
  Lesser General Public License for more details.

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

  $Id$
*/

#include "SFBBootBlock.h"
#include "SFBEeprom.h"          // For eepromRead etc 
#include "SFBAssert.h"          // For API_ASSERT etc 
#include "SFBRandom.h"          // For randomSeed()
#include "SFBReflex.h"          // For TEST_REFLEX_FLAGS, RF_LESS_EEPROM
#include "SFBAlarm.h"           // For Alarms

// BootBlock version updaters.  EVERY version must ALWAYS have one!
static bool updateBootBlockV5();
static bool updateBootBlockV4();
static bool updateBootBlockV3();
static bool updateBootBlockV2();
static bool updateBootBlockV1();

static SFBBootBlock bootBlockCache IN_BODYRAM_BSS;
static u8 bootBlockIndexLoaded IN_BODYRAM_BSS;

static u32 bootBlockAddr(u32 which) { return sizeof(SFBBootBlock)*which; }
static void loadCache(u32 which);
static void flushCache();
static void checksumBootBlock();
static bool validBootBlock();

static bool updateBootBlock() {

  if (updateBootBlockV5()) {
    flushCache();
    return true;
  }

  return false;
}

static void loadCache(u32 which) {
  eepromReadReserved(bootBlockAddr(which), (u8*) &bootBlockCache, sizeof(SFBBootBlock));
  bootBlockIndexLoaded = which;
}

static void flushCache() {
  bootBlockCache.flags &= ~BOOTBLOCK_INVALID; // We're making the cache valid by writing it
  bootBlockCache.writeTag++;                  // And, I am now newer!
  checksumBootBlock();                        // My checksum is now good!
  bootBlockIndexLoaded = !bootBlockIndexLoaded; // I am getting written to the other block!
  eepromWriteReserved(bootBlockAddr(bootBlockIndexLoaded), (u8*) &bootBlockCache, sizeof(SFBBootBlock));
}

static void initBootBlock() {
  u8 * ptr = (u8*) &bootBlockCache;
  for (u32 i = 0; i < sizeof(bootBlockCache); ++i) // Sigh.
    ptr[i] = 0;                                    // Nuke it all.

  updateBootBlock();
}

static void findLoadInitBootBlock() {

  u32 attempts = 0;

  while (++attempts) {

    // Step 1: Read the two write tags
    u8 writeTags[2];
    for (u32 i = 0; i < 2; ++i) {
      eepromReadReserved(bootBlockAddr(i)+STRUCT_MEMBER_OFFSET(SFBBootBlock,writeTag), 
                         &writeTags[i], 1);
    }

    // Step 2: Guess which is older (without yet knowing validity)
    bool firstIsOlder = ((u8) (writeTags[1]-writeTags[0])) < 0x80;

    // Step 3: Load probably older block and see if valid
    u32 firstIndex = firstIsOlder?0:1;
    loadCache(firstIndex);
    bool firstIsValid = validBootBlock();

    // Step 4: Now load newer and see if it's valid
    u32 secondIndex = !firstIndex;
    loadCache(secondIndex);
    bool secondIsValid = validBootBlock();

    // Step 5: If both are invalid, we must init one :( 
    if (!firstIsValid && !secondIsValid) {

      API_ASSERT(attempts < 5, E_API_EEPROM_RESPONSE); // CPR not working.. Call it..
      
      initBootBlock();          // Nuke whichever we think we're not on..
      continue;                 // ..and try again
    }

    // Step 6: If only first is valid, we guessed wrong: Reload it
    if (!secondIsValid) {
      loadCache(firstIndex);

      if (!validBootBlock()) {  // "It's not paranoia if.."
        API_ASSERT(attempts < 5, E_API_EEPROM_RESPONSE); 
        continue;               // ..try again.
      }
    }

    // Step 7: Made it.

    // Now bootBlockCache has newest valid block, and
    // bootBlockIndexLoaded tells where it was read from.

    return;
  }
}

void invalidateBootBlockCache() {
  bootBlockCache.flags |= BOOTBLOCK_INVALID;
}

#define BOOTBLOCK_CHECKSUMMED_LENGTH (sizeof(SFBBootBlock)-sizeof(SFBChecksum))

static void checksumBootBlock() {
  SFBChecksumInit(bootBlockCache.cs);
  SFBChecksumAddBytes(bootBlockCache.cs,(const char*) &bootBlockCache, BOOTBLOCK_CHECKSUMMED_LENGTH);
}

static bool validBootBlockVersion() {
  return 
    bootBlockCache.version >= BOOTBLOCK_MIN_VERSION &&
    bootBlockCache.version <= BOOTBLOCK_MAX_VERSION;
}
static bool olderBootBlockVersion(u8 comparedTo) {
  return !validBootBlockVersion() || bootBlockCache.version < comparedTo;
}
static bool updateBootBlockV1() {
  if (!olderBootBlockVersion(1)) return false; 

  bootBlockCache.version = 1;
  bootBlockCache.writeTag = 0;
  bootBlockCache.flags = 0;
  bootBlockCache.bootCount = 0;
  bootBlockCache.burnCount = 0;

  return true;
}

static bool updateBootBlockV2() {
  if (!olderBootBlockVersion(2)) return false;

  updateBootBlockV1();

  bootBlockCache.version = 2;
  bootBlockCache.ownerId = 0;
  bootBlockCache.boardId = 0;

  return true;
}

static bool updateBootBlockV3() {
  if (!olderBootBlockVersion(3)) return false;

  updateBootBlockV2();

  bootBlockCache.version = 3;
  bootBlockCache.powerOnHours = 0;

  return true;
}

static bool updateBootBlockV4() {
  if (!olderBootBlockVersion(4)) return false;

  updateBootBlockV3();

  bootBlockCache.version = 4;
  bootBlockCache.initialPower = 0xff; /* Default: All faces in (0xf0) and all faces out (0x0f) */

  return true;
}

static bool updateBootBlockV5() {
  if (!olderBootBlockVersion(5)) return false;

  updateBootBlockV4();

  bootBlockCache.version = 5;
  bootBlockCache.watchdogResets = 0x00; /* Default: Most recent eight boots assumed not dog-related */

  return true;
}

static bool validBootBlock() {

  if (bootBlockCache.flags & BOOTBLOCK_INVALID)
    return false;

  if (!validBootBlockVersion())
    return false;

  SFBChecksum cs;
  SFBChecksumInit(cs);
  SFBChecksumAddBytes(cs,(const char *) &bootBlockCache, BOOTBLOCK_CHECKSUMMED_LENGTH);
  if (!SFBChecksumEqual(cs,bootBlockCache.cs))
    return false;

  return true;
}

static void checkCache() {
  if (validBootBlock()) return;

  findLoadInitBootBlock();      // Go load the newest block
  updateBootBlock();            // Update it if needed
}

u32 getBootBlockVersion() {
  checkCache();
  return bootBlockCache.version;
}
    
u8 getBootBlockWatchdogResets() {
  checkCache();
  return bootBlockCache.watchdogResets;
}
u32 getBootBlockOwnerId() {
  checkCache();
  return bootBlockCache.ownerId;
}
u32 getBootBlockBoardId() {
  checkCache();
  return bootBlockCache.boardId;
}
u32 getBootBlockBootCount() {
  checkCache();
  return bootBlockCache.bootCount;
}
u32 getBootBlockBurnCount() {
  checkCache();
  return bootBlockCache.burnCount;
}
u32 getBootBlockPowerOnHours() {
  checkCache();
  return bootBlockCache.powerOnHours;
}
u32 getBootBlockFlags() {
  checkCache();
  return bootBlockCache.flags;
}
u32 getBootBlockBootMode() {
  checkCache();
  if ((bootBlockCache.flags&BOOTBLOCK_BOOT_MODE)==0)
    return GREEN_BOOT_MODE;
  return (bootBlockCache.flags&BOOTBLOCK_BLUE_NOT_RED)?BLUE_BOOT_MODE:RED_BOOT_MODE;
}

u32 getBootBlockMergedChecksum() {
  checkCache();
  return bootBlockCache.cs[0]^bootBlockCache.cs[1];
}

#define POWER_IN_BIT(face) (1<<(4+(face)))  // N=0x10, S=0x20, E=0x40, W=0x80
#define POWER_OUT_BIT(face) (1<<(0+(face))) // N=0x01, S=0x02, E=0x04, W=0x08

bool getBootBlockPowerIn(u8 face) {
  API_ASSERT_VALID_FACE(face);
  checkCache();
  return bootBlockCache.initialPower&POWER_IN_BIT(face);
}

bool getBootBlockPowerOut(u8 face) {
  API_ASSERT_VALID_FACE(face);
  checkCache();
  return bootBlockCache.initialPower&POWER_OUT_BIT(face);
}

void setBootBlockPowerIn(u8 face, bool accept) {
  API_ASSERT_VALID_FACE(face);
  checkCache();
  u8 bit = POWER_IN_BIT(face);
  if (accept)
    bootBlockCache.initialPower |= bit;
  else
    bootBlockCache.initialPower &= ~bit;
  flushCache();
}

void setBootBlockPowerOut(u8 face, bool provide) {
  API_ASSERT_VALID_FACE(face);
  checkCache();
  u8 bit = POWER_OUT_BIT(face);
  if (provide)
    bootBlockCache.initialPower |= bit;
  else
    bootBlockCache.initialPower &= ~bit;
  flushCache();
}

void setBootBlockWatchdogResetFlag(bool bootedClean) {
  checkCache();
  bootBlockCache.watchdogResets = (bootBlockCache.watchdogResets<<1)|(bootedClean?0:1);
  flushCache();
}
void setBootBlockOwnerId(u32 newOwnerId) {
  checkCache();
  if ((bootBlockCache.flags&BOOTBLOCK_UNLOCKED) && bootBlockCache.ownerId != newOwnerId) {
    bootBlockCache.ownerId = newOwnerId;
    bootBlockCache.flags &= ~BOOTBLOCK_UNLOCKED;
    flushCache();
  }
}
void setBootBlockBoardId(u32 newBoardId) {
  checkCache();
  if ((bootBlockCache.flags&BOOTBLOCK_UNLOCKED) && bootBlockCache.boardId != newBoardId) {
    bootBlockCache.boardId = newBoardId;
    bootBlockCache.flags &= ~BOOTBLOCK_UNLOCKED;
    flushCache();
  }
}

void incrementBootBlockBootCount() {
  checkCache();
  ++bootBlockCache.bootCount;
  flushCache();
}
void incrementBootBlockBurnCount() {
  checkCache();
  ++bootBlockCache.burnCount;
  flushCache();
}
static u32 lastPoH IN_BODYRAM = 0;
void incrementBootBlockPowerOnHours() {
  if (IS_EARLIER(lastPoH+1000*60*45,millis())) { // Balk at too frequent increments
    lastPoH = millis();
    checkCache();
    ++bootBlockCache.powerOnHours;
    flushCache();
  }
}
void setBootBlockFlags(u32 newFlags) {
  checkCache();
  if (bootBlockCache.flags != newFlags) {
    bootBlockCache.flags = newFlags;
    flushCache();
  }
}
const char * bootModeColorName(u32 bootMode) {
  switch (bootMode) {
  default: 
    /* FALL THROUGH */
  case UNKNOWN_BOOT_MODE: return "UNKNOWN";
  case RED_BOOT_MODE: return "RED";
  case GREEN_BOOT_MODE: return "GREEN"; 
  case BLUE_BOOT_MODE: return "BLUE";
  }
}

void setBootBlockBootMode(u32 newBootMode) {
  u32 flags = getBootBlockFlags()&~(BOOTBLOCK_BOOT_MODE|BOOTBLOCK_BLUE_NOT_RED); // Drop both flags
  switch (newBootMode) {
  case GREEN_BOOT_MODE: break;
  case RED_BOOT_MODE:   flags |= BOOTBLOCK_BOOT_MODE; break;
  case BLUE_BOOT_MODE:  flags |= BOOTBLOCK_BOOT_MODE|BOOTBLOCK_BLUE_NOT_RED; break;
  default: API_BUG(E_API_ARGUMENT);
  }
  setBootBlockFlags(flags);
}

void bootblock_bootcount_initialization() {
  incrementBootBlockBootCount();
  randomSeed(bootBlockCache.bootCount);
}

#define MS_PER_HOUR (1000*60*60)
static void hourlyAlarm(u32 when) {
  if (!TEST_REFLEX_FLAGS(RF_LESS_EEPROM))   // Unless suppressed..
    incrementBootBlockPowerOnHours();       // ..mark another hour in service
  Alarms.set(Alarms.currentAlarmNumber(),when+MS_PER_HOUR);
}

void bootblock_startup_initialization() {
  invalidateBootBlockCache();

  u32 alarmNum = Alarms.create(hourlyAlarm);
  API_ASSERT(alarmNum, E_API_NO_MORE_ALARMS);
  Alarms.set(alarmNum, 0+MS_PER_HOUR);
}

/**
 * The data stored in flash configuration memory by the SFB _secondary_ bootloader.  Note that if
 * the SFB _tertiary_ bootloader is present, as usual, the information in this struct describes the
 * tertiary bootloader, not the end-user sketch!
 */
typedef struct {
   SFBChecksum AppChecksum;
   u64    AppBurnCount;
   u32    AppLength;
} SFB_CONFIG_MEM_T;

/* Configuration Memory */

u32 getBootBlockBootLoaderLength() {
  SFB_CONFIG_MEM_T * gConfigMemPtr = (SFB_CONFIG_MEM_T*)(0x2000); // Flash Location of Configuration Memory
  return gConfigMemPtr->AppLength;
}
u32 getBootBlockBootLoaderBurnCount() {
  SFB_CONFIG_MEM_T * gConfigMemPtr = (SFB_CONFIG_MEM_T*)(0x2000); // Flash Location of Configuration Memory
  return gConfigMemPtr->AppBurnCount;
}
void getBootBlockBootLoaderChecksum(SFBChecksum cs) {
  SFB_CONFIG_MEM_T * gConfigMemPtr = (SFB_CONFIG_MEM_T*)(0x2000); // Flash Location of Configuration Memory
  cs[0] = gConfigMemPtr->AppChecksum[0];
  cs[1] = gConfigMemPtr->AppChecksum[1];
}
