/** 
 *  @filename   :   HPTrackerEInk.ino
 *  @brief      :   Code for "Dungeons and Dragons Hit Point Tracker with e-Ink display" on www.instructables.com
 *  @author     :   Neil Martin from www.BigBagOfBits.com
 *  @version    :   v1.0
 *  
 *  https://www.instructables.com/id/Dungeons-and-Dragons-Hit-Point-Tracker-With-E-Ink-/
 */
 
#include <EEPROM.h>
#include <RobotUI.h>
#include <SPI.h>
#include <epd4in2.h>
#include "epdpaint.h"

#define COLORED     0
#define UNCOLORED   1

#define BT_BAUDRATE   9600

Epd epaper;

#define NUM_PLAYERS   (8)
#define NUM_CAMPAIGNS (3)
#define VALIDATION_CODE (((long int)'N'<<24)|((long int)'J'<<16)|((long int)'M'<<8))
#define FLAG_HIDDEN (1<<0)

const char option0[] PROGMEM = "Chad's Campaign";
const char option1[] PROGMEM = "Lando's Campaign";
const char option2[] PROGMEM = "Other Campaign";
const char * const campaignOptions[NUM_CAMPAIGNS] PROGMEM = {option0, option1, option2};
#define NUM_ID_OPTIONS  (20)
char const * const nameOptions[NUM_ID_OPTIONS] = { 
    "AKIRA",
    "ALICE",
    "DARAK",
    "GUY",
    "IDIANA",
    "MORYN",
    "PANGR",
    "RODNBAR",
    "SAMOON",
    "SUR",
    "TITAN",
    "TORGA",
    "VAL",
    "VISTRA",
    "???",
    "NPC1",
    "NPC2",
    "NPC3",
    "NPC4",
    "NPC5"
  };      
      
 
RobotUI rui;
RobotUIButton buttonHealAll;
RobotUISpinner selectID[NUM_PLAYERS];
RobotUIButton buttonAmountDown10;
RobotUIButton buttonAmountDown;
RobotUIButton buttonAmountUp;
RobotUIButton buttonAmountUp10;
RobotUIButton buttonHPUp[NUM_PLAYERS];
RobotUIButton buttonHPDown[NUM_PLAYERS];
RobotUIButton buttonHPMaxUp[NUM_PLAYERS];
RobotUIButton buttonHPMaxDown[NUM_PLAYERS];
RobotUIToggleButton checkBox[NUM_PLAYERS];
RobotUIButton buttonSwap[NUM_PLAYERS-1];
RobotUIText   textHP[NUM_PLAYERS];
RobotUIText   textHPMax[NUM_PLAYERS];
RobotUIText   textAmount;
RobotUISpinner selectCampaign;

class PlayerData
{
  public:
  int hpMax;
  int hp;
  char id;
  unsigned char flags;
};

class SaveData
{
  public:
  PlayerData playerData[NUM_PLAYERS*NUM_CAMPAIGNS];
  long int validation;
  char campaignIndex;
};

SaveData sd;
int healthAmount = 5;
int firstPlayerIndex = sd.campaignIndex*NUM_PLAYERS;
PlayerData *playerData = sd.playerData;
bool prepareToSave = false;
unsigned long saveTime = 0;

void LayoutDef()
{
  rui.StartLayout();
  rui.AddLabel("Damage/Heal Amount", 1, 20, JUSTIFY_BOTTOM);
  rui.StartHorizontal(1);     // start a horizontal layout with the height of 1 buttons
  rui.Add(buttonAmountDown10, 1, 20);
  rui.Add(buttonAmountDown,   1, 20);
  rui.Add(textAmount,         1, 20, JUSTIFY_CENTRE);
  rui.Add(buttonAmountUp,     1, 20);
  rui.Add(buttonAmountUp10,   1, 20);
  rui.EndHorizontal();
  rui.AddLabel("Players", 1, 20, JUSTIFY_BOTTOM);
  rui.StartVerticalScroll(6);     // start a vertical layout with the height of 6 buttons
  for(int i=0;i<NUM_PLAYERS;++i)
  {
    rui.StartHorizontal(1);
    rui.Add(selectID[i],       2, 20);
    if(i>0)
    {
      rui.Add(buttonSwap[i-1],   1, 15);
    }
    else
    {
      rui.AddSpace(1);
    }
    rui.Add(checkBox[i],       1, 15);
    rui.EndHorizontal();
    rui.StartHorizontal(1, checkBox[i], true);
    rui.Add(buttonHPDown[i],   1, 20);
    rui.Add(textHP[i],         1, 20, JUSTIFY_CENTRE);
    rui.Add(buttonHPUp[i],     1, 20);
    rui.AddGreySpace(0.2f);
    rui.Add(buttonHPMaxDown[i], 1, 20);
    rui.Add(textHPMax[i],       1, 20, JUSTIFY_CENTRE);
    rui.Add(buttonHPMaxUp[i],   1, 20);
    rui.EndHorizontal();
    rui.AddGreySpace(0.2f);
  }
  rui.EndVerticalScroll();
  rui.AddLabel("Campaign", 1, 20, JUSTIFY_BOTTOM);
  rui.Add(buttonHealAll, 1, 20);
  rui.Add(selectCampaign, 1);
  rui.EndLayout();
}

void SetHPText( int hp, RobotUIText& uiText )
{
  char hpText[10];
  sprintf(hpText, "%d", hp);
  uiText.SetText(hpText);
}

int ClampedNameIndex( int unclamped )
{
  int nameIndex = min((NUM_ID_OPTIONS)-1, max(0, unclamped));
  return nameIndex;
}

void RecoverStatusForCampaignSwitch()
{
  char charToStr[2];
  charToStr[1]=0;
  for(int i=0;i<NUM_PLAYERS;++i)
  {
    PlayerData *pd = playerData + firstPlayerIndex + i;
    SetHPText( pd->hp, textHP[i] );
    SetHPText( pd->hpMax, textHPMax[i] );
    bool visible = (pd->flags & FLAG_HIDDEN)==0;
    buttonHPDown[i].SetIsEnabled(visible );
    checkBox[i].SetIsPressed( visible );
    selectID[i].SetOption( ClampedNameIndex(pd->id - 'A') );
  }
}

// NJM - This function gets called when the RobotUI app requests a UI status refresh on start up
void RecoverStatus()
{
  SetHPText( healthAmount, textAmount );
  buttonHealAll.SetName("Heal all");
  buttonAmountUp.SetName("+1");
  buttonAmountUp10.SetName("+10");
  buttonAmountDown.SetName("-1");
  buttonAmountDown10.SetName("-10");
  for(int i=0;i<NUM_PLAYERS;++i)
  {
    buttonHPUp[i].SetName("+");
    buttonHPDown[i].SetName("-");
    buttonHPMaxUp[i].SetName("+1");
    buttonHPMaxDown[i].SetName("-1");
    checkBox[i].SetNames("hide", "show");
    checkBox[i].SetIsPressedEnabled( false, true );
    selectID[i].SetOptions(nameOptions, NUM_ID_OPTIONS, 0);
  }
  for(int i=0;i<NUM_PLAYERS-1;++i)
  {
    buttonSwap[i].SetName(F("Move up"));
  }
  selectCampaign.SetOptionsF(campaignOptions, NUM_CAMPAIGNS, sd.campaignIndex);
  RecoverStatusForCampaignSwitch();
}

bool HandleHPUpButton( RobotUIButton& button, int &hp, int hpMax, RobotUIText& uiText, int amount )
{
  if(button.IsPressed())
  {
    button.ConsumeIsPressed();
    if(hp<hpMax)
    {
      hp += amount;
      if(hp>hpMax)
      {
        hp = hpMax;
      }
      SetHPText( hp, uiText );
      return true;
    }
  }
  return false;
}

bool HandleHPDownButton( RobotUIButton& button, int &hp, RobotUIText& uiText, int amount )
{
  if(button.IsPressed())
  {
    button.ConsumeIsPressed();
    if(hp>0)
    {
      hp -= amount;
      if(hp<0)
      {
        hp = 0;
      }
      SetHPText( hp, uiText );
      return true;
    }
  }
  return false;
}

void setup()
{
  Serial.begin(9600);
  
  while (!Serial)
  {
    ; // wait for serial port to connect. Needed for native USB
  }
  Serial.println("v0.1");
  pinMode(LED_BUILTIN, OUTPUT);
  rui.Init(ROBOTUI_PIN_RX, ROBOTUI_PIN_TX, BT_BAUDRATE, DEFAULT_COMMAND_BUFFER_SIZE*2, LayoutDef, RecoverStatus);
  LoadData();

  Serial.println("setup finished");
}

void LoadData()
{
  int eeAddress = 0;
  EEPROM.get( eeAddress, sd );
  if(sd.validation != VALIDATION_CODE)
  {
    ClearData();
  }
  else
  {
    FixData();
  }
  firstPlayerIndex = sd.campaignIndex*NUM_PLAYERS;
}

void SaveData()
{
  if(sd.validation == VALIDATION_CODE)
  {
    int eeAddress = 0;
    EEPROM.put( eeAddress, sd );
  }
}

void ClearData()
{
  int numPlayers = NUM_PLAYERS * NUM_CAMPAIGNS;
  for(int i=0;i<numPlayers;++i)
  {
    int ii = (i%NUM_PLAYERS);
    PlayerData *pd = playerData + i;
    pd->hpMax = 50+ii*5;
    pd->hp = 45+ii*5;
    pd->id = 'A' + i%NUM_ID_OPTIONS;
    pd->flags = 0;
  }
  sd.validation = VALIDATION_CODE;
  sd.campaignIndex = 0;
  firstPlayerIndex = sd.campaignIndex*NUM_PLAYERS;
}

void FixData()
{
  int numPlayers = NUM_PLAYERS * NUM_CAMPAIGNS;
  for(int i=0;i<numPlayers;++i)
  {
    int ii = (i%NUM_PLAYERS);
    PlayerData *pd = playerData + i;
    if((pd->id<'A') || (pd->id>('A'+NUM_ID_OPTIONS-1)))
    {
      pd->id = 'A' + (pd->id-'a')%(NUM_ID_OPTIONS);
    }
  }
  sd.validation = VALIDATION_CODE;
  if((sd.campaignIndex<0) || (sd.campaignIndex>=NUM_CAMPAIGNS))
  {
    sd.campaignIndex = 0;
  }
  firstPlayerIndex = sd.campaignIndex*NUM_PLAYERS;
}

void loop()
{
  bool changed = false;
  // NJM - You need to call the Update() function often, so the RobotUI library can do its work
  unsigned long ms = rui.Update();
  if(buttonHealAll.IsPressed())
  {
    buttonHealAll.ConsumeIsPressed();
    for(int i=0;i<NUM_PLAYERS;++i)
    {
      PlayerData *pd = playerData + firstPlayerIndex + i;
      pd->hp = pd->hpMax; 
      SetHPText( pd->hp, textHP[i] );
    }
    changed = true;
  }
  if(selectCampaign.HasChanged())
  {
    selectCampaign.ConsumeHasChanged();
    if(selectCampaign.GetOption() != sd.campaignIndex)
    {
      // NJM - Check if we are about to save the current campaign, if so, save before we switch
      if(prepareToSave)
      {
        SaveData();
        prepareToSave = false;
      }
      sd.campaignIndex = selectCampaign.GetOption();
      firstPlayerIndex = (int)sd.campaignIndex * NUM_PLAYERS;
      changed = true;
      RecoverStatusForCampaignSwitch();
    }
  }
  char charToStr[2];
  charToStr[1] = 0;
  for(int i=0;i<NUM_PLAYERS;++i)
  {
    PlayerData *pd = playerData + firstPlayerIndex + i;
    changed |= HandleHPUpButton(buttonHPUp[i], pd->hp, pd->hpMax, textHP[i], healthAmount );
    changed |= HandleHPDownButton(buttonHPDown[i], pd->hp, textHP[i], healthAmount );
    changed |= HandleHPUpButton(buttonHPMaxUp[i], pd->hpMax, 500, textHPMax[i], 1 );
    changed |= HandleHPDownButton(buttonHPMaxDown[i], pd->hpMax, textHPMax[i], 1 );
    HandleHPUpButton(buttonAmountUp, healthAmount, 500, textAmount, 1 );
    HandleHPUpButton(buttonAmountUp10, healthAmount, 500, textAmount, 10 );
    HandleHPDownButton(buttonAmountDown, healthAmount, textAmount, 1 );
    HandleHPDownButton(buttonAmountDown10, healthAmount, textAmount, 10 );
    if(checkBox[i].HasChanged())
    {
      checkBox[i].ConsumeHasChanged();
      bool visible = checkBox[i].IsPressed();
      if(visible)
      {
        pd->flags &= ~FLAG_HIDDEN;
      }
      else
      {
        pd->flags |= FLAG_HIDDEN;
      }
      changed = true;
      buttonHPDown[i].SetIsEnabled( visible );
    }
    if(selectID[i].HasChanged())
    {
      selectID[i].ConsumeHasChanged();
      pd->id = 'A' + selectID[i].GetOption();
      changed = true;
    }
  }
  for(int i=0;i<NUM_PLAYERS-1;++i)
  {
    if(buttonSwap[i].IsPressed())
    {
      buttonSwap[i].ConsumeIsPressed();
      PlayerData *pd0 = playerData + firstPlayerIndex + i;
      PlayerData *pd1 = pd0+1;
      PlayerData temp = *pd0;
      *pd0 = *pd1;
      *pd1 = temp;
      changed = true;
      SetHPText( pd0->hp,    textHP[i] );
      SetHPText( pd0->hpMax, textHPMax[i] );
      SetHPText( pd1->hp,    textHP[i+1] );
      SetHPText( pd1->hpMax, textHPMax[i+1] );
      bool visible0 = (pd0->flags & FLAG_HIDDEN)==0;
      bool visible1 = (pd1->flags & FLAG_HIDDEN)==0;
      checkBox[i].SetIsPressed( visible0 );
      checkBox[i+1].SetIsPressed( visible1 );
      buttonHPDown[i].SetIsEnabled( visible0 );
      buttonHPDown[i+1].SetIsEnabled( visible1 );
      selectID[i].SetOption( ClampedNameIndex(pd0->id-'A') );
      selectID[i+1].SetOption( ClampedNameIndex(pd1->id-'A') );
    }
  }
  if(changed)
  {
    prepareToSave = true;
    saveTime = ms + 3000;
  }
  if((prepareToSave) && (ms>=saveTime))
  {
    SaveData();
    saveTime = ms + 10000;
    prepareToSave = false;
    renderDisplay2();
  }
}

bool ePaperInit()
{
  Serial.println("e-Paper init...");
  if (epaper.Init() != 0) 
  {
    Serial.println("e-Paper init failed");
    return false;
  }
  else
  {
    Serial.println("e-Paper init success");
  }
  return true;
}

void DrawThickCircle2(Paint& paint, int x, int y, int outerRadius, int innerRadius, int startDegrees, int endDegrees, int colored)
{
  int x_pos = -outerRadius;
  int y_pos = 0;
  int err = 2 - 2 * outerRadius;
  int e2;
  if(outerRadius<innerRadius)
  {
    int temp = outerRadius;
    outerRadius = innerRadius;
    innerRadius = temp;
  }
  int thickness = (innerRadius*256) / outerRadius;
  do 
  {
    float angle = (atanf(fabsf((float)y_pos/(float)x_pos))*180.0f)/3.1415927f;
    int qAngle = angle;
    int q0Angle = 90+angle;
    int q1Angle = 270-angle;
    int q2Angle = 270+angle;
    int q3Angle = 90-angle;
    short x_pos2 = ((long int)x_pos * (long int)thickness)/256;
    short y_pos2 = ((long int)y_pos * (long int)thickness)/256;

    short xm1 = x-x_pos;
    short xp1 = x+x_pos;
    short ym1 = y-y_pos;
    short yp1 = y+y_pos;
    short xm2 = x-x_pos2;
    short xp2 = x+x_pos2;
    short ym2 = y-y_pos2;
    short yp2 = y+y_pos2;
    if(startDegrees>endDegrees)
    {
      if((q0Angle>=startDegrees) || (q0Angle<=endDegrees))
      {
        paint.DrawLine(xm1, yp1, xm2, yp2, colored);  // 90-180
        paint.DrawLine(xm1, yp1+1, xm2, yp2+1, colored);  // 90-180
      }
      if((q1Angle>=startDegrees) || (q1Angle<=endDegrees))
      {
        paint.DrawLine(xp1, yp1, xp2, yp2, colored); // 270-180
        paint.DrawLine(xp1, yp1+1, xp2, yp2+1, colored); // 270-180
      }
      if((q2Angle>=startDegrees) || (q2Angle<=endDegrees))
      {
        paint.DrawLine(xp1, ym1, xp2, ym2, colored); // 270-360
        paint.DrawLine(xp1, ym1+1, xp2, ym2+1, colored); // 270-360
      }
      if((q3Angle>=startDegrees) || (q3Angle<=endDegrees))
      {
        paint.DrawLine(xm1, ym1, xm2, ym2, colored); // 90 -0
        paint.DrawLine(xm1, ym1+1, xm2, ym2+1, colored); // 90 -0
      }
    }
    else
    {
      if((q0Angle>=startDegrees) && (q0Angle<=endDegrees))
      {
        paint.DrawLine(xm1, yp1, xm2, yp2, colored);  // 90-180
        paint.DrawLine(xm1, yp1+1, xm2, yp2+1, colored);  // 90-180
      }
      if((q1Angle>=startDegrees) && (q1Angle<=endDegrees))
      {
        paint.DrawLine(xp1, yp1, xp2, yp2, colored); // 270-180
        paint.DrawLine(xp1, yp1+1, xp2, yp2+1, colored); // 270-180
      }
      if((q2Angle>=startDegrees) && (q2Angle<=endDegrees))
      {
        paint.DrawLine(xp1, ym1, xp2, ym2, colored); // 270-360
        paint.DrawLine(xp1, ym1+1, xp2, ym2+1, colored); // 270-360
      }
      if((q3Angle>=startDegrees) && (q3Angle<=endDegrees))
      {
        paint.DrawLine(xm1, ym1, xm2, ym2, colored); // 90 -0
        paint.DrawLine(xm1, ym1+1, xm2, ym2+1, colored); // 90 -0
      }
    }
    e2 = err;
    if (e2 <= y_pos) 
    {
      err += ++y_pos * 2 + 1;
      if(-x_pos == y_pos && e2 <= x_pos) 
      {
        e2 = 0;
      }
    }
    if (e2 > x_pos) 
    {
      err += ++x_pos * 2 + 1;
    }
  } while (x_pos <= 0);
}

void clearLowQuadrant(Paint& paint)
{
  short int paintX = paint.GetWidth();
  short int paintY = paint.GetHeight();
  short int paintMin = paintX<paintY?paintX:paintY;
  short int paintMinHalf = paintMin/2;
  short centreX = paintX/2;
  short centreY = paintY/2;
  for(int i=paintMinHalf-28;i<paintMinHalf;++i)
  {
    paint.DrawHorizontalLine(centreX-i, centreY+i, i*2, UNCOLORED);
  }
}

void drawFullBar(Paint& paint)
{
  short int paintX = paint.GetWidth();
  short int paintY = paint.GetHeight();
  short int paintMin = paintX<paintY?paintX:paintY;
  short int paintMinHalf = paintMin/2;
  short centreX = paintX/2;
  short centreY = paintY/2;
  paint.Clear(UNCOLORED);

  paint.DrawFilledCircle(centreX, centreY, paintMinHalf-1, COLORED);
  paint.DrawFilledCircle(centreX, centreY, paintMinHalf-20, UNCOLORED);
}

void renderDisplay2()
{
  if(!ePaperInit())
  {
    return;
  }
  
  /* This clears the SRAM of the e-paper display */
  epaper.ClearFrame();

  short displayWidth = 400;
  short displayHeight = 300;
  short subDisplayWidth = (displayWidth/4)&0xfff8;
  short subDisplayHeight = displayHeight/2;
  
  unsigned char image[(subDisplayWidth*subDisplayHeight)/8];
  Paint paint(image, subDisplayWidth, subDisplayHeight);    //width should be the multiple of 8 

  PlayerData *pd = playerData + firstPlayerIndex;
  short xHighestMax = pd->hpMax;
  bool highestFound = ((pd->flags & FLAG_HIDDEN)==0);
  for(int i=1;i<NUM_PLAYERS;++i)
  {
    pd = playerData + firstPlayerIndex + i;
    if(((pd->flags & FLAG_HIDDEN)==0) && ((!highestFound) || (pd->hpMax > xHighestMax)))
    {
      xHighestMax = pd->hpMax;
      highestFound = true;
    }
  }
  short yBarStart = 32;
  short yStep = 32;
  short barWidth = 240;
  short barStart = displayWidth-barWidth;
  char str[32];
  short int paintX = paint.GetWidth();
  short int paintY = paint.GetHeight();
  short int paintMin = paintX<paintY?paintX:paintY;
  short h8 = paintY-8;
  short h10 = paintY-10;
  short centreX = paintX/2;
  short centreY = paintY/2;

  short charWidth12 = 8;
  short charWidth16 = 11;
  short charWidth24 = 16;
  short charHeight24 = 20;
  short endBarThick = 4;
  for(short int i=0;i<NUM_PLAYERS;++i)
  {
    pd = playerData + firstPlayerIndex + i;
    if((pd->flags & FLAG_HIDDEN)==0)
    {
      drawFullBar(paint);
      clearLowQuadrant(paint);
      sprintf(str, "%d", pd->hp);
      int len = strlen(str);
      paint.DrawStringAt(centreX-(len*charWidth24/2), centreY-(charHeight24/2), str, &Font24, COLORED);

      sprintf(str, "%d", pd->hpMax);
      len = strlen(str);
      paint.DrawStringAt(centreX-(len*charWidth12/2), centreY+25, str, &Font12, COLORED);

      int nameIndex = pd->id-'A';
      if((nameIndex>=0)&&(nameIndex<NUM_ID_OPTIONS))
      {
        sprintf(str, "%s", nameOptions[nameIndex]);
      }
      else
      {
        sprintf(str, "%s", "?ERR?");
      }
      
      len = strlen(str);
      if(len>5)
      {
        str[5] = 0;
        len = 5;
      }
      paint.DrawStringAt(centreX-(len*charWidth24/2), centreY+40, str, &Font24, COLORED);
      
      short maxAsAngle = (short)(225 + ((270*(long)(pd->hpMax))/xHighestMax));
      short currAsAngle = (short)(225 + ((270*(long)(pd->hp))/xHighestMax));
      short nearlyMaxAsAngle = (short)(225 - endBarThick + ((270*(long)(pd->hpMax))/xHighestMax));
      bool nearFull = currAsAngle+endBarThick>=maxAsAngle;
      bool biggestBar = pd->hpMax == xHighestMax;
      if(maxAsAngle>=360) maxAsAngle -= 360;
      if(currAsAngle>=360) currAsAngle -= 360;
      if(nearlyMaxAsAngle>=360) nearlyMaxAsAngle -= 360;
      if(!biggestBar)
      {
        DrawThickCircle2(paint, centreX, centreY, (paintMin/2), (paintMin/2)-20, maxAsAngle, 135+1, UNCOLORED);
      }
      if(!nearFull)
      {
        DrawThickCircle2(paint, centreX, centreY, (paintMin/2)-3, (paintMin/2)-16, currAsAngle, nearlyMaxAsAngle, UNCOLORED);
      }
      epaper.SetPartialWindow(paint.GetImage(), (i%4)*paintX, ((i>3)?1:0)*paintY, paintX, paintY);
    }
  }
  /* This displays the data from the SRAM in e-Paper module */
  epaper.DisplayFrame();

  /* Deep sleep */
  epaper.Sleep();
}
