/*  ----------------------------------------------------------------------------------------
 *  Arduino Thermostat for Millivolt Gas and Low-Voltage Electric Baseboard Heat
 *  Copyright (c) Graham McMicken <graham@mcmicken.ca>
 *  ----------------------------------------------------------------------------------------
 */
#include <glcd.h>
#include <IRremote.h>
#include <EEPROM.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Wire.h>
#include <RealTimeClockDS1307.h>

#include "fonts/allFonts.h"
#include "bitmaps/allBitmaps.h"

#define TIMER_TEMP       30000  // 30 Seconds
#define TIMER_SLOPE      300000 // 5 Minutes
#define TIMER_NOINPUT    30000  // 30 Seconds
#define TIMER_SETTINGS   10000  // 10 Seconds
#define TOTAL_SCHEDULES  8      // 7 for days of the week + one ad-hoc schedule, this is necessary to blank the EEPROM, uncomment in setup().
#define SCHEDULE_ZONES   2
#define MIN_TEMP         12
#define MAX_TEMP         30
#define TEMP_ADJUST      6.0
#define BUFFER_UBOUND    0.35
#define BUFFER_LBOUND    0.40

#define ROWS             0
#define COLS             1
#define WIDTH            2
#define HEIGHT           3
#define LENGTH           4
#define PADDING          5

#define GRAPH_X          6
#define GRAPH_Y          60
#define GRAPH_LENGTH     55
#define GRAPH_HEIGHT     27
#define HISTORY_LENGTH   ( ( 24 * 60 ) / (TIMER_SLOPE / 1000 / 60) ) // 24 Hours

#define IR_LEFT          0x2FD1AE5
#define IR_RIGHT         0x2FD38C7
#define IR_UP            0x2FDF00F
#define IR_DOWN          0x2FD9867
#define IR_EXIT          0x2FD7887
#define IR_MENU          0x2FD58A7
#define IR_MODE          0x2FDE817
#define IR_SAVE          0x2FDB847
#define IR_VIEWTMP       0x2FDB847
#define IR_ELEC          0x2FD30CF
#define IR_EN_SAVE       0x2FDEA15
#define IR_GAS           0x2FDC639
#define IR_GASLCK        0x2FD2AD5
#define IR_NUM0          0x2FD00FF
#define IR_NUM1          0x2FD807F
#define IR_NUM2          0x2FD40BF
#define IR_NUM3          0x2FDC03F
#define IR_NUM4          0x2FD20DF
#define IR_NUM5          0x2FDA05F
#define IR_NUM6          0x2FD609F
#define IR_NUM7          0x2FDE01F
#define IR_NUM8          0x2FD10EF
#define IR_NUM9          0x2FD906F

//Pin Assignments
int pinLDR               = A1; // Light Dependent Resistor
int pinIR                = 2;  // IR Receiver
int pinRelayGas          = 3;  // Gas Switch
int pinRelayElec         = 4;  // Electric Switch
int pinTemp              = 12; // Temp Sensor
int pinBacklightPWM      = 13; // LCD Backlight Output

//Display Initialization
int iCursorPos           = 0;
boolean bCursorMovement  = true;
int iDefaultX            = 0;
int iDefaultY            = 0;
char formatted[]         = "00-00-00 00:00:00x";
char* rDaysOfTheWeek[]   = {"Ad-hoc Schedule", 
                            "Monday", 
                            "Tuesday", 
                            "Wednesday", 
                            "Thursday", 
                            "Friday", 
                            "Saturday", 
                            "Sunday"};
//Operating Variables
boolean bHeatCycle       = false;
boolean bElec            = true;
boolean bElecSave        = true;
boolean bGas             = true;
boolean bGasLock         = false;
boolean bAdjustedOff     = false;
boolean bTempReqRec      = false;
boolean bSettingChanged  = false;

int     iMode            = 0;
int     iTempRise        = 0;
float   fTempSlope       = 0.0;
float   fCurrentTemp     = 0.0;
float   fPrevTemp        = 0.0;
int     iSetTemp         = 0;
long    iGraphOffset     = 0;
int     iPwmVal          = 0;
byte    rTempHistory[HISTORY_LENGTH];

//IR & Temp Setup
IRrecv irrecv(pinIR);
decode_results results;
OneWire ds(pinTemp);
DallasTemperature dsTemp(&ds);

//Timer Setup
static unsigned long lWaitSlope    = 0;
static unsigned long lWaitTemp     = 0;
static unsigned long lOffTimer     = 0;
static unsigned long lWaitSetting  = 0;

void setup()
{
  //Un-comment this once to blank EEPROM values (default 255) for the programmable schedule.
  //for (int i = 0; i < (TOTAL_SCHEDULES*SCHEDULE_ZONES*5); i++) EEPROM.write(i, 0);
  
  GLCD.Init();
  
  //Serial.begin(9600);
  
  pinMode(pinRelayElec, OUTPUT);
  digitalWrite(pinRelayElec, HIGH);
  pinMode(pinRelayGas, OUTPUT);
  digitalWrite(pinRelayGas, HIGH);
  
  irrecv.enableIRIn(); //Start the IR receiver buffer
  dsTemp.begin();;     //Start the temp sensor library
  Wire.begin();        //Start the I2C RTC

 /* Fill the history array with the current temperature 
  * so that slope calculations begin immediately after startup. */
  dsTemp.requestTemperatures();
  delay(1000);
  pollTemp();
  fPrevTemp = fCurrentTemp;
  for (int i = 0; i < HISTORY_LENGTH; i++) rTempHistory[i] = (int)floor(fCurrentTemp + 0.5);
  
  uiMain(); //Here we go!

}

/*  ----------------------------------------------------------------------------------------
 *  Main Loop
 *
 *  Optimized to run quickly to feel responsive, calling temperature polling 
 *  and display updates at intervals defined at the top of the script.
 *  ----------------------------------------------------------------------------------------
 */
 
void loop()
{

  //Set the backlight brightness dependent on the LDR value
  iPwmVal = analogRead(pinLDR);
  analogWrite(pinBacklightPWM, map(constrain(iPwmVal, 150, 600), 600, 150, 100, 255));
  //Serial.println(iPwmVal);
  
  //Poll temperature and adjust backlight as defined by TIMER_TEMP (plus one second for poll time)
  if( (long)( millis() - lWaitTemp ) >= 0 ){ 
    if (bTempReqRec) {
      pollTemp(); 
      lWaitTemp = millis() + TIMER_TEMP;
    }else{
      dsTemp.requestTemperatures();
      lWaitTemp = millis() + 1000; //Read time on DS18B20 is ~700ms
    }
    bTempReqRec = !bTempReqRec;
  }
  
  //Graph the temp every 5 minutes, determine rising or falling indicator, and calculate temp slope.
  if( (long)( millis() - lWaitSlope ) >= 0 ){ 
    calculateSlope();
    determineHeatState();
    lWaitSlope = millis() + TIMER_SLOPE;
  }
  
  //Apply settings changes after a brief timeout to prevent relay abuse.
  if( (long)( millis() - lWaitSetting ) >= 0 && bSettingChanged == true ){ 
    determineHeatState();
    bSettingChanged = false;
  }
  
  if( (long)( millis() - lOffTimer ) >= 0 && bAdjustedOff == true ){ 
    bAdjustedOff = false;
    controlHeat(false);
    lWaitSlope = millis() + TIMER_SLOPE;
  }

  //Send IR commands to the inputDirector() function.
  if (irrecv.decode(&results)) {
    inputDirector(results.value); 
    irrecv.resume();
  }
  
}
/*  ----------------------------------------------------------------------------------------
 *  IR Input Handling:
 *
 *  inputDirector()  - Handles IR input from the main screen to call various 
 *  toggle functions and launch auxilary screens.
 *  ----------------------------------------------------------------------------------------
 */
 
void inputDirector(unsigned long irKey){
  
  //Serial.println(irKey, HEX);
  
  switch (irKey) {
  
    case IR_ELEC:
      bElec = !bElec;
      uiMainDrawElec();
    break;
    
    case IR_EN_SAVE: if (bElec) {
      bElecSave = !bElecSave;
      uiMainDrawElec();
    } break;
    
    case IR_GAS: if (!bGasLock) {
      bGas = !bGas;
      uiMainDrawGas();
    } break;
    
    case IR_GASLCK: 
      bGasLock = !bGasLock;
      uiMainDrawGas();
      controlHeat(bHeatCycle);
      return;
    break;
    
    case IR_MODE:
      iMode++;
      uiMainDrawMode();
      if (iMode == 3) iSetTemp = 18;
      checkScheduleTime();
    break;
    
    case IR_UP:
    case IR_DOWN: if (iMode == 3) {
      uiMainSetTemp();
      uiMain();
    } break;
    
    case IR_LEFT:
      iGraphOffset = iGraphOffset + 10;
      if (iGraphOffset > HISTORY_LENGTH - GRAPH_LENGTH) iGraphOffset = HISTORY_LENGTH - GRAPH_LENGTH;
      uiMainDrawGraph();
      return;
    break;

    case IR_RIGHT:
      iGraphOffset = iGraphOffset - 10;
      if (iGraphOffset < 0) iGraphOffset = 0;
      uiMainDrawGraph();
      return;
    break;    

    case IR_VIEWTMP:
      uiMainViewTemp();
      uiMain();
    break;
    
    case IR_MENU:
      while(1) {
        if (!uiSetCal(0)) break; //Ad-hoc schedule first for ease of access.
        if (!uiSetCal(1)) break;
        if (!uiSetCal(2)) break;
        if (!uiSetCal(3)) break;
        if (!uiSetCal(4)) break;
        if (!uiSetCal(5)) break;
        if (!uiSetCal(6)) break;
        if (!uiSetCal(7)) break;
        if (!uiSetTime()) break;
      }
      uiMain();
    break;
    
  } //end switch

  lWaitSetting = millis() + TIMER_SETTINGS;
  bSettingChanged = true;
  
}

/*  ----------------------------------------------------------------------------------------
 *  Heater Control
 *  
 *  determineHeatState()
 *  controlHeat()
 *  ----------------------------------------------------------------------------------------
 */
 
void determineHeatState(){
  if (!checkScheduleTime()){
    controlHeat(false);
    return;
  }
  float fLowTempDiff  = iSetTemp - BUFFER_LBOUND - fCurrentTemp;
  float fHighTempDiff = iSetTemp + BUFFER_UBOUND - fCurrentTemp;
  if (fLowTempDiff >= 0 && bHeatCycle == false) {
    //Turn the heat on
    controlHeat(true);
  }else if (fHighTempDiff <= 0 && bHeatCycle == true) {
    //Turn the heat off
    controlHeat(false);
  }else if (fHighTempDiff > 0 && fTempSlope > 0.05 && bHeatCycle == true) {
    //Adjust the off time to prevent over-shoot
    adjustOffTime(fHighTempDiff);
  }else{
    //Re-apply our current heat state. (In the case of a heat source change etc)
    controlHeat(bHeatCycle);
  }
}

void controlHeat(boolean bOnOff){
  if (bOnOff == true){
    bHeatCycle = true;
    if ((iSetTemp - fCurrentTemp) < 3 && bElecSave == true && bGas == true) {
      digitalWrite(pinRelayGas, LOW);
      digitalWrite(pinRelayElec, HIGH);
    }else{
      if (bElec) digitalWrite(pinRelayElec, LOW); else digitalWrite(pinRelayElec, HIGH);
      if (bGas) digitalWrite(pinRelayGas, LOW); else if (!bGasLock) digitalWrite(pinRelayGas, HIGH);
    }
  }else if (bOnOff == false) {
    bHeatCycle = false;
    bAdjustedOff = false;
    digitalWrite(pinRelayElec, HIGH);
    if (!bGasLock) digitalWrite(pinRelayGas, HIGH); else digitalWrite(pinRelayGas, LOW);
  }
}

/*  ----------------------------------------------------------------------------------------
 *  Temperature Polling and Calculations:
 *
 *  checkScheduleTime() - Check to see if we're inside a scheduled block and set iSetTemp appropriately.
 *  pollTemp()  - To poll and store the current temperature, and to call the display function if necessary.
 *  calculateSlope() - To calculate the rate of change and call the graphing function.
 *  adjustOffTime() - A timer set to expire as we reach our set temperature, to combat overshooting.
 *  ----------------------------------------------------------------------------------------
 */
 
boolean checkScheduleTime(){

  if (iMode == 3) {
    uiMainDrawSetTemp();
    return true;
  }
  
  if (iMode == 0) {
    iSetTemp = 0;
    uiMainDrawSetTemp();
    return false;
  }
  
  RTC.readClock();
  /*
  RTC.getFormatted(formatted);
  //Serial.println(formatted);
  if (RTC.isStopped()) //Serial.println("Clock Stopped"); else //Serial.println("Clock Running");
  */
  
  iSetTemp = 0;
  int iScheduleStart, iScheduleEnd, iScheduleTemp, iOffset;
  int iCurrDay = RTC.getDayOfWeek();
  int iHours = RTC.getHours();
  
  //Serial.print("Current Day: ");
  //Serial.print(iCurrDay);
  //Serial.print(", Current Hour: ");
  //Serial.println(iHours);
  
  if (iMode == 1) {
    
    int iPrevDay;
    if (iCurrDay == 1) iPrevDay = 7; else iPrevDay = iCurrDay - 1;
    for (int i = 0; i < SCHEDULE_ZONES; i++) {
      iOffset = (iPrevDay * SCHEDULE_ZONES * 5) + (i * 5);
      iScheduleStart = convert12to24(EEPROM.read(iOffset + 0), EEPROM.read(iOffset + 1));
      iScheduleEnd = convert12to24(EEPROM.read(iOffset + 2), EEPROM.read(iOffset + 3));
      iScheduleTemp = EEPROM.read(iOffset + 4);
      //Serial.print("Start: ");
      //Serial.print(iScheduleStart);
      //Serial.print(", End: ");
      //Serial.print(iScheduleEnd);
      //Serial.print(", Current: ");
      //Serial.println(iHours);
      if (iScheduleEnd < iScheduleStart && iHours < iScheduleEnd && iScheduleTemp >= MIN_TEMP) {
        iSetTemp = EEPROM.read(iOffset + 4);
        break;  //There should not be more than one occurence of an end time into the next day so stop when we find one.
      }
    }

    for (int i = 0; i < SCHEDULE_ZONES; i++) {
      iOffset = (iCurrDay * SCHEDULE_ZONES * 5) + (i * 5);
      iScheduleStart = convert12to24(EEPROM.read(iOffset + 0), EEPROM.read(iOffset + 1));
      iScheduleEnd = convert12to24(EEPROM.read(iOffset + 2), EEPROM.read(iOffset + 3));
      iScheduleTemp = EEPROM.read(iOffset + 4);
      //Serial.print("Start: ");
      //Serial.print(iScheduleStart);
      //Serial.print(", End: ");
      //Serial.print(iScheduleEnd);
      //Serial.print(", Current: ");
      //Serial.println(iHours);
      if (iScheduleEnd < iScheduleStart) iScheduleEnd = 24;
      if (iHours >= iScheduleStart && iHours < iScheduleEnd && iScheduleTemp >= MIN_TEMP) {
        iSetTemp = EEPROM.read(iOffset + 4); //Overwrite value to give lower zones priority.
      }
    }
    
  }
  
  if (iMode == 2) {
  
    for (int i = 0; i < SCHEDULE_ZONES; i++) {
      iOffset = i * 5;
      iScheduleStart = convert12to24(EEPROM.read(iOffset + 0), EEPROM.read(iOffset + 1));
      iScheduleEnd = convert12to24(EEPROM.read(iOffset + 2), EEPROM.read(iOffset + 3));
      iScheduleTemp = EEPROM.read(iOffset + 4);
      if (((iHours >= iScheduleStart && iHours < iScheduleEnd) || (iScheduleEnd < iScheduleStart && (iHours < iScheduleEnd || iHours >= iScheduleStart))) && iScheduleTemp >= MIN_TEMP) {
        iSetTemp = EEPROM.read(iOffset + 4); //Overwrite value to give lower zones priority.
      }
    }
	
  }
  
  uiMainDrawSetTemp();
  if (iSetTemp > 0) return true; else return false;
  
}

void pollTemp(){

  float fOldTemp = 0.0;
  float fNewTemp = 0.0;
  fOldTemp = fCurrentTemp;
  fNewTemp = dsTemp.getTempCByIndex(0) - TEMP_ADJUST;
  fCurrentTemp = fNewTemp;
  if (floor(fNewTemp + 0.5) != floor(fOldTemp + 0.5)) uiMainDrawTemp();
  
}

void calculateSlope(){

  //Get our temp rise over 5 min
  fTempSlope = (float)(fCurrentTemp - fPrevTemp);
  fPrevTemp = fCurrentTemp;
  
  //Increment and add on to our graph data.
  for (int i = 0; i < HISTORY_LENGTH - 1; i++) {
     rTempHistory[i] = rTempHistory[i + 1];
  }
  rTempHistory[HISTORY_LENGTH - 1] = (int)floor(fCurrentTemp + 0.5);
  
  //Determine if the temp is rising or falling over 15 minutes
  iTempRise = rTempHistory[HISTORY_LENGTH - 1] - rTempHistory[HISTORY_LENGTH - 4];

  //Redraw the main screen
  uiMainDrawTrend();
  uiMainDrawGraph();
  
}

void adjustOffTime(float fTempDiff){

  bAdjustedOff = true;
  lOffTimer = millis() + ((fTempDiff/fTempSlope) * TIMER_SLOPE);
  lOffTimer = lOffTimer - (fTempSlope/0.5*90*1000); //Allow for room equilization, 90 seconds for .5 degree/5 min rise.
  if (bElec) lOffTimer = lOffTimer - 45000;  //Baseboard heat has an internal relay delay of 30-45 seconds.

}

/*  ----------------------------------------------------------------------------------------
 *  Main UI: 
 *
 *  uiMain() -->
 *    uiMainSetTemp();
 *    uiMainDrawTemp();
 *    uiMainDrawGraph();
 *    uiMainDrawTemp();
 *    uiMainDrawTrend();
 *    uiMainDrawSetTemp();
 *    uiMainDrawGraph();
 *    uiMainDrawMode();
 *    uiMainDrawGas();
 *    uiMainDrawElec();
 *  ----------------------------------------------------------------------------------------
 */
 
boolean uiMain(){
  
  GLCD.ClearScreen();
  GLCD.DrawLine(6, 30, 61, 30); //Seperator
  //GLCD.DrawLine(GRAPH_X, GRAPH_Y - GRAPH_HEIGHT, GRAPH_X, GRAPH_Y); //Graph Y
  //GLCD.DrawLine(GRAPH_X, GRAPH_Y, GRAPH_X + GRAPH_LENGTH, GRAPH_Y); //Graph X
  
  uiMainDrawTemp();
  uiMainDrawTrend();
  uiMainDrawSetTemp();
  uiMainDrawGraph();
  uiMainDrawMode();
  uiMainDrawGas();
  uiMainDrawElec();
  
}

void uiMainSetTemp() {

  GLCD.ClearScreen();
  GLCD.SelectFont(lucida_Fixed21x42);

  unsigned long lTimeout = millis() + 2000;
  unsigned long irKey;
  int iNewTemp;
  
  while ((long)( millis() - lTimeout ) < 0){
    iNewTemp = iSetTemp;
    if (irrecv.decode(&results)) {   
      lTimeout =  millis() + 2000;
      irKey = results.value;
      delay(100);
      irrecv.resume();
      switch (irKey) {
        case IR_UP: if (iNewTemp < MAX_TEMP) iNewTemp++; break;
        case IR_DOWN: if (iNewTemp > MIN_TEMP) iNewTemp--; break;
      }
    }
    if (iNewTemp != iSetTemp) {
      GLCD.FillRect(42, 12, 42, 42, WHITE);
      GLCD.CursorToXY(42, 12);
      GLCD.print(iNewTemp);
      iSetTemp = iNewTemp;
    }
  } //end while

}

void uiMainViewTemp() {

  GLCD.ClearScreen();
  GLCD.SelectFont(lucida_Fixed21x42);

  GLCD.FillRect(15, 12, 94, 42, WHITE);
  GLCD.CursorToXY(15, 12);
  GLCD.print(fCurrentTemp);

  unsigned long lTimeout = millis() + 2000;  
  while ((long)( millis() - lTimeout ) < 0){
    delay(100);
  }

}

void uiMainDrawTemp(){
  
  GLCD.FillRect(6, 4, 38, 24, WHITE);
  GLCD.SelectFont(Verdana24);
  GLCD.CursorToXY(6, 4);
  GLCD.print((int)floor(fCurrentTemp + 0.5));
  
}

void uiMainDrawTrend(){
  
  if (iTempRise >= 1) GLCD.DrawBitmap(arrow_up, 46, 4);
  else if (iTempRise <= -1) GLCD.DrawBitmap(arrow_down, 46, 4);
  else GLCD.FillRect(46, 4, 16, 8, WHITE);
  
}

void uiMainDrawSetTemp(){

  GLCD.FillRect(46, 14, 16, 15, WHITE);
  GLCD.SelectFont(fixednums7x15);
  GLCD.CursorToXY(46, 14);  
  if (iSetTemp) GLCD.print(iSetTemp);
  
}

void uiMainDrawGraph(){
  
  GLCD.FillRect(GRAPH_X, GRAPH_Y - GRAPH_HEIGHT, GRAPH_LENGTH, GRAPH_HEIGHT, WHITE);
  int iCurrPosX = 0;
  int iCurrPosY = 0;
  int iLastPosX = GRAPH_X;
  int iLastPosY = 0;
  for (int i = (HISTORY_LENGTH - GRAPH_LENGTH - iGraphOffset); i < (HISTORY_LENGTH - iGraphOffset); i++){
    iCurrPosX = GRAPH_X + (i - (HISTORY_LENGTH - GRAPH_LENGTH - iGraphOffset));
    iCurrPosY = GRAPH_Y - (int)floor(rTempHistory[i] + 0.5) + 5;
    if (iCurrPosY >= GRAPH_Y) iCurrPosY = GRAPH_Y - 1; else if (iCurrPosY < GRAPH_Y - GRAPH_HEIGHT) iCurrPosY = GRAPH_Y - GRAPH_HEIGHT;
    if (iLastPosY == 0) iLastPosY = iCurrPosY;
    GLCD.DrawLine(iLastPosX, iLastPosY, iCurrPosX, iCurrPosY);
    GLCD.DrawLine(iLastPosX, iLastPosY + 1, iCurrPosX, iCurrPosY + 1);
    iLastPosX = iCurrPosX;
    iLastPosY = iCurrPosY;
  }
  
}

void uiMainDrawMode(){
  
  GLCD.SelectFont(Arial_bold_14);
  GLCD.FillRect(72, 10, 48, 14, WHITE);
  
  if (iMode == 4 || iMode == 0) {
    iMode = 0;
    GLCD.CursorToXY(82, 10);
    GLCD.print("Off");
  }else if (iMode == 1) {
    iMode = 1;
    GLCD.CursorToXY(78, 10);
    GLCD.print("Auto");
  }else if (iMode == 2) {
    GLCD.CursorToXY(72, 10);
    GLCD.print("Ad-hoc");
  }else if (iMode == 3) {
    GLCD.CursorToXY(72, 10);
    GLCD.print("Manual");
  }
  
}

void uiMainDrawGas(){
  if (bGasLock == true) GLCD.DrawBitmap(fire_locked,62,31); 
  else if (bGas == true) GLCD.DrawBitmap(fire,62,31); 
  else GLCD.FillRect(62, 31, 32, 32, WHITE);
}

void uiMainDrawElec(){
  if (bElecSave == true && bElec == true) GLCD.DrawBitmap(electricity_save,94,31); 
  else if (bElec == true) GLCD.DrawBitmap(electricity,94,31); 
  else GLCD.FillRect(94, 31, 32, 32, WHITE);
}

/*  ----------------------------------------------------------------------------------------
 *  Settings Screens:
 *
 *  uiSetTime()
 *  uiSetCal(int iDay) -> Where iDay is the day of the week, 1 = Monday and so forth.
 *  ----------------------------------------------------------------------------------------
 */
 
boolean uiSetTime(){
  
  //Input Grid
  //              rows  cols  width  height  length  padding
  int rGrid[] = {   2,    5,    16,      14,     10,        2  };
  iDefaultX = 4;
  iDefaultY = 18;
  iCursorPos = 0;

  //Setup
  RTC.readClock();
  unsigned long irKey;
  bCursorMovement = true;
  String s2Digit = "";
  String sExclude = "1368";
  int rTime[10] = {RTC.getHours(), -1, RTC.getMinutes(), -1, RTC.getSeconds(), RTC.getMonth(), -1, RTC.getDate(), -1, RTC.getYear()};
  GLCD.ClearScreen();
  drawTitle("Date/Time Setup");
  for (int i = 0; i < rGrid[LENGTH]; i++) if (rTime[i] == -1 && i < 5) drawString(":", fixednums8x16, i, rGrid, BLACK); else if (rTime[i] == -1 && i > 5) drawString("/", fixednums8x16, i, rGrid, BLACK);
  for (int i = 0; i < rGrid[LENGTH]; i++) if (rTime[i] > -1) drawString(rTime[i], fixednums8x16, i, rGrid, BLACK);  
  
  while(1) {

    //Await user interaction
    irKey = awaitUserInput(rGrid, sExclude);
    
    //Exit to homescreen or to next option screen.
    if(irKey == IR_EXIT){
      return false;
    }else if(irKey == IR_MENU){
      return true;
    }
    
    //Digit input
    if (irKey >= 0 && irKey <= 9 && sExclude.indexOf(String(iCursorPos)) == -1) {
      
      if (bCursorMovement) {
        s2Digit = String(irKey);
        bCursorMovement = false;
      }else s2Digit = String(s2Digit + irKey);
      drawString(s2Digit, fixednums8x16, iCursorPos, rGrid, BLACK);
      rTime[iCursorPos] = stringToInt(s2Digit);
      
      if (s2Digit.length() == 2) {
        bCursorMovement = true;
      	if (   (stringToInt(s2Digit) > 59 && (iCursorPos == 2 || iCursorPos == 4) )
      	    || (stringToInt(s2Digit) > 23 && (iCursorPos == 0) )
      	    || (stringToInt(s2Digit) > 12 && (iCursorPos == 5) )
      	    || (stringToInt(s2Digit) > 31 && (iCursorPos == 7) )   ) {
          delay(500);
          drawString("", fixednums8x16, iCursorPos, rGrid, BLACK);
          rTime[iCursorPos] = 0;
      	}else moveCursor(1, 0, rGrid, sExclude);
        s2Digit = "";
      }
      
    }
    
    if (irKey == IR_SAVE) {

        RTC.switchTo24h();
        RTC.start();
        
        //Serial.print("Hours: ");
        //Serial.print(rTime[0]);
        RTC.setHours(rTime[0]);
        
        //Serial.print(", Minutes: ");
        //Serial.print(rTime[2]);
        RTC.setMinutes(rTime[2]);
        
        //Serial.print(", Seconds: ");
        //Serial.print(rTime[4]);
        RTC.setSeconds(rTime[4]);
        
        //Serial.print(", Month: ");
        //Serial.print(rTime[5]);
        RTC.setMonth(rTime[5]);
        
        //Serial.print(", Day: ");
        //Serial.print(rTime[7]);
        RTC.setDate(rTime[7]);
        
        //Serial.print(", Year: ");
        //Serial.print(rTime[9]);
        RTC.setYear(rTime[9]);
        
        //Serial.print(", DoW: ");
        //Serial.print(getDayOfWeek(rTime[7], rTime[5], rTime[9]));
        RTC.setDayOfWeek(getDayOfWeek(rTime[7], rTime[5], rTime[9]));
        
        RTC.setClock();        
        //Serial.println(".");
        
        drawSaved();
        return false;
        
    }
    
  }
}

boolean uiSetCal(int iDay) {
  //Input Grid
  //              rows  cols  width  height  length  padding
  int rGrid[] = {   2,    5,    16,      14,     10,       2  };
  iDefaultX = 4;
  iDefaultY = 18;
  iCursorPos = 0;
  
  //Setup
  GLCD.ClearScreen();
  drawTitle(rDaysOfTheWeek[iDay]);
  unsigned long irKey;
  bCursorMovement = true;
  String s2Digit = "";
  String sExclude = "";
  int rSchedule[10];
  int iOffset = 0;
  for (int i = iDay * SCHEDULE_ZONES * 5; i < (iDay + 1) * SCHEDULE_ZONES * 5; i++) {
  	iOffset = i - (iDay * SCHEDULE_ZONES * 5);
    rSchedule[iOffset] = EEPROM.read(i);
    if (iOffset == 1 || iOffset == 3 || iOffset == 6 || iOffset == 8 ){
    	drawString(getAMorPM(rSchedule[iOffset]), SystemFont5x7, iOffset, rGrid, BLACK);
    }else{
    	drawString(rSchedule[iOffset], fixednums8x16, iOffset, rGrid, BLACK);
    }
  }
  
  while(1) {

    //Await user interaction
    irKey = awaitUserInput(rGrid, sExclude);
    
    //Exit to homescreen or to next option screen.
    if(irKey == IR_EXIT){
      return false;
    }else if(irKey == IR_MENU){
      return true;
    }
    
    if (iCursorPos == 1 || iCursorPos == 3 || iCursorPos == 6 || iCursorPos == 8){
    
      if (irKey == 1){
        drawString("am", SystemFont5x7, iCursorPos, rGrid, BLACK);
        rSchedule[iCursorPos] = 0;
        moveCursor(1, 0, rGrid, sExclude);
      }
	  
      if (irKey == 2){
        drawString("pm", SystemFont5x7, iCursorPos, rGrid, BLACK);
        rSchedule[iCursorPos] = 12;
        moveCursor(1, 0, rGrid, sExclude);
      }
      bCursorMovement = true;
      
    }else if (irKey >= 0 && irKey <= 9) {
      
      //if (bCursorMovement) //Serial.println("Cursor Movement"); else //Serial.println("No Cursor Movement");
      if (bCursorMovement) {
        s2Digit = String(irKey);
        bCursorMovement = false;
      }else s2Digit = String(s2Digit + irKey);
      drawString(s2Digit, fixednums8x16, iCursorPos, rGrid, BLACK);
      rSchedule[iCursorPos] = stringToInt(s2Digit);
          
      if (s2Digit.length() == 2) {
        bCursorMovement = true;
      	if (   ((stringToInt(s2Digit) > 12 || stringToInt(s2Digit) < 1) && (iCursorPos == 0 || iCursorPos == 2 || iCursorPos == 5 || iCursorPos == 7) )
      	    || (((stringToInt(s2Digit) > MAX_TEMP || stringToInt(s2Digit) < MIN_TEMP) && stringToInt(s2Digit) != 0) && (iCursorPos == 4 || iCursorPos == 9) )    ) {
          delay(500);
          drawString("", fixednums8x16, iCursorPos, rGrid, BLACK);
          rSchedule[iCursorPos] = 0;
      	}else moveCursor(1, 0, rGrid, sExclude);
        s2Digit = "";
      }
      
    }
    
    if (irKey == IR_SAVE) {
      
        //Serial.print("Schedule for: ");
        //Serial.print(iDay);
        //Serial.print(", Start 1: ");
        //Serial.print(rSchedule[0]);
        //Serial.print(getAMorPM(rSchedule[1]));
        //Serial.print(", Stop 1:: ");
        //Serial.print(rSchedule[2]);
        //Serial.print(getAMorPM(rSchedule[3]));
        //Serial.print(", Temp 1: ");
        //Serial.print(rSchedule[4]);
        //Serial.print(", Start 2: ");
        //Serial.print(rSchedule[5]);
        //Serial.print(getAMorPM(rSchedule[6])); 
        //Serial.print(", Stop 2: ");
        //Serial.print(rSchedule[7]);
        //Serial.print(getAMorPM(rSchedule[8]));
        //Serial.print(", Temp 2: ");
        //Serial.print(rSchedule[9]);
        //Serial.println(".");
        for (int i = iDay * SCHEDULE_ZONES * 5; i < (iDay + 1) * SCHEDULE_ZONES * 5; i++) EEPROM.write(i, rSchedule[i - (iDay * SCHEDULE_ZONES * 5)]);
        
        drawSaved();
        return false;
        
    }
    
  }
}

/*  ----------------------------------------------------------------------------------------
 *  awaitUserInput() for Settings Screens
 *
 *  Responsible for updating the cursor position. When a non directional key is pressed, 
 *  the function returns the action. On timeout, the fuction returns the exit key.
 *  ----------------------------------------------------------------------------------------
 */
 
unsigned long awaitUserInput(int rGrid[], String sExclude){

  unsigned long irKey;
  unsigned long lTimeout = millis() + TIMER_NOINPUT;
  irrecv.resume();
  
  while ((long)( millis() - lTimeout ) < 0){
    
    if (irrecv.decode(&results)) {   
      lTimeout += TIMER_NOINPUT;
      irKey = results.value; 
      irrecv.resume();
      //Serial.println(irKey, HEX);
      switch (irKey) {
        case IR_LEFT:   moveCursor(-1, 0, rGrid, sExclude);  break;
        case IR_RIGHT:  moveCursor(1, 0, rGrid, sExclude);   break;
        case IR_UP:     moveCursor(0, -1, rGrid, sExclude);  break;
        case IR_DOWN:   moveCursor(0, 1, rGrid, sExclude);   break;
        case IR_NUM0:   return 0;
        case IR_NUM1:   return 1;
        case IR_NUM2:   return 2;
        case IR_NUM3:   return 3;
        case IR_NUM4:   return 4;
        case IR_NUM5:   return 5;
        case IR_NUM6:   return 6;
        case IR_NUM7:   return 7;
        case IR_NUM8:   return 8;
        case IR_NUM9:   return 9;
        default:        return irKey; //Any other key is returned for handling.
      }
    }
  }
  return IR_EXIT; //Timeout the same as Exit Key 
}

/*  ----------------------------------------------------------------------------------------
 *  Cursor and text drawing functions for the settings screens:
 *
 *  drawCursor() 
 *  drawString()
 *  moveCursor()
 *  drawSaved()
 *  ----------------------------------------------------------------------------------------
 */
 
void drawCursor(int rGrid[], uint8_t bColour){

  int iFloor = floor(iCursorPos / rGrid[COLS]);
  int iCursorPosX = iCursorPos - (iFloor * rGrid[COLS]);
  int iCursorPosY = iFloor;
  int iTotalWidth = rGrid[WIDTH] + (rGrid[PADDING] * 2) + 4;
  int iTotalHeight = rGrid[HEIGHT] + (rGrid[PADDING] * 2) + 4;
  // (x, y, width, height, radius, color)
  GLCD.DrawRoundRect((iCursorPosX * iTotalWidth) + rGrid[PADDING] + iDefaultX, (iCursorPosY * iTotalHeight) + rGrid[PADDING] + iDefaultY, iTotalWidth - (rGrid[PADDING] * 2), iTotalHeight - (rGrid[PADDING] * 2) - 1, 2, bColour);
}

void drawString(String sString, uint8_t* iFont, int iPos, int rGrid[], uint8_t bColour){

  int iFloor = floor(iPos / rGrid[COLS]);
  int iCursorPosX = iPos - (iFloor * rGrid[COLS]);
  int iCursorPosY = iFloor;
  int iTotalWidth = rGrid[WIDTH] + (rGrid[PADDING] * 2) + 4;
  int iTotalHeight = rGrid[HEIGHT] + (rGrid[PADDING] * 2) + 4;
  
  GLCD.FillRect((iCursorPosX * iTotalWidth) + rGrid[PADDING] + 2 + iDefaultX, (iCursorPosY * iTotalHeight) + rGrid[PADDING] + 2 + iDefaultY, rGrid[WIDTH], rGrid[HEIGHT], WHITE);
  GLCD.CursorToXY((iCursorPosX * iTotalWidth) + rGrid[PADDING] + 2 + iDefaultX, (iCursorPosY * iTotalHeight) + rGrid[PADDING] + 2 + iDefaultY);
  GLCD.SelectFont(iFont);
  GLCD.print(sString);
  drawCursor(rGrid, BLACK);
  
}

void moveCursor(int x, int y, int rGrid[], String sExclude){
  
  if (!bCursorMovement) return;
  
  //Erase where we were - Note for this to work the cursor cannot overlap any pixels already drawn on the screen.
  drawCursor(rGrid, WHITE);
    
  iCursorPos += x + (y * rGrid[COLS]);

  if (iCursorPos < 0) {
    if (x < 0){
      iCursorPos = rGrid[LENGTH];
    }else{
      iCursorPos = (rGrid[ROWS] * rGrid[COLS]) + iCursorPos;
    }
  }

  if (iCursorPos >= rGrid[LENGTH]) {
    if (x > 0){
      iCursorPos = 0;
    }else if (y < 0){
      iCursorPos = iCursorPos - rGrid[COLS];
    }else{
      iCursorPos = iCursorPos - (floor(iCursorPos / rGrid[COLS]) * rGrid[COLS]);
    }
  }

  //Skip excluded locations by calling moveCursor recursively and adding 1 each time
  if (sExclude.indexOf(String(iCursorPos)) == -1){
    //Draw where we are.
    drawCursor(rGrid, BLACK);
  }else{
    moveCursor(x, y, rGrid, sExclude);
  }
  
}

void drawTitle(char sTitle[]){
  
  GLCD.SelectFont(Arial_bold_14);
  GLCD.CursorToXY(6, 3);
  GLCD.print(sTitle);
  GLCD.DrawLine(6, 16, 121, 16);
  GLCD.DrawLine(6, 17, 121, 17);
  
}

void drawSaved(){

  GLCD.FillRect(43, 24, 44, 15, WHITE);
  GLCD.SelectFont(Arial_bold_14);
  GLCD.CursorToXY(46, 26);
  GLCD.print("Saved");
  GLCD.DrawRoundRect(43, 24, 44, 15, 5);
  delay(2000);
  
}

/*  ----------------------------------------------------------------------------------------
 *  Utility Functions
 *  ----------------------------------------------------------------------------------------
 */
 
int stringToInt(String input){
  
  char convert[input.length() + 1];
  input.toCharArray(convert, sizeof(convert));
  int output = atoi(convert);
  return output;
  
}

int convert12to24(int i12t, int iAmPm){
  if (i12t == 12) return iAmPm;
  if (iAmPm == 12) return i12t+12; else return i12t;
}

String getAMorPM(int iAmPm){
  if (iAmPm == 0) return "am"; else return "pm";
}

int getDayOfWeek(int day, int mth, int yr) {

  int val; 
  const int table[12] = {6,2,2,5,0,3,5,1,4,6,2,4}; 
  
  val = yr + yr / 4; //leap year adj good 2007 to 2099 
  val = val + table[mth - 1]; //table contains modulo 7 adjustments for mths 
  val = val + day;
  if ((yr % 4 == 0) && (mth < 3)) val = val - 1; // adjust jan and feb down one for leap year
  val = val % 7;
  if (val == 0) val = 7;

  // val is now the day of week 1=Mon... 7=Sun
  return(val);
  
}

void irDump(decode_results *results) {

  int count = results->rawlen;
  if (results->decode_type == UNKNOWN) {
    Serial.print("Unknown encoding: ");
  } 
  else if (results->decode_type == NEC) {
    Serial.print("Decoded NEC: ");
  } 
  else if (results->decode_type == SONY) {
    Serial.print("Decoded SONY: ");
  } 
  else if (results->decode_type == RC5) {
    Serial.print("Decoded RC5: ");
  } 
  else if (results->decode_type == RC6) {
    Serial.print("Decoded RC6: ");
  }
  Serial.print(results->value, HEX);
  Serial.print(" (");
  Serial.print(results->bits, DEC);
  Serial.println(" bits)");
  Serial.print("Raw (");
  Serial.print(count, DEC);
  Serial.print("): ");

  for (int i = 0; i < count; i++) {
    if ((i % 2) == 1) {
      Serial.print(results->rawbuf[i]*USECPERTICK, DEC);
    } 
    else {
      Serial.print(-(int)results->rawbuf[i]*USECPERTICK, DEC);
    }
    Serial.print(" ");
  }
  Serial.println("");
}