#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Fonts/FreeSansBold9pt7b.h>

#define EB_NO_FOR           // отключить поддержку pressFor/holdFor/stepFor и счётчик степов (экономит 2 байта оперативки)
#define EB_NO_CALLBACK      // отключить обработчик событий attach (экономит 2 байта оперативки)
#define EB_NO_COUNTER       // отключить счётчик энкодера (экономит 4 байта оперативки)
// #define EB_NO_BUFFER        // отключить буферизацию энкодера (экономит 1 байт оперативки)
#include <EncButton.h>

#include <EEPROM.h>

//#define DEBUG 115200

#define ENCODER_S1  2
#define ENCODER_S2  3
#define ENCODER_KEY 4
#define ENCODER_REVERSE 1 // 0

#define RELAY1 5
#define RELAY2 6

#define SENSOR1 A1
#define SENSOR2 A2
#define MEASURE_DEBOUNCE 1500
#define SENSOR_MIN 0
#define SENSOR_MAX 1023

#define MENU_LENGTH 9
#define VALUE_FAST_STEP 5

#include "images.h"

Adafruit_SSD1306 display(128, 32, &Wire, -1);
EncButton eb(ENCODER_S1, ENCODER_S2, ENCODER_KEY);

typedef enum {
  MODE_STATUS,
  MODE_MENU,  
  MODE_VALUE,
  MODE_ACTION
} Modes;

typedef enum {
  MENU_FLUSH1,
  MENU_FLUSH2,
  MENU_TIME1,
  MENU_CHECK1,
  MENU_MIN1,
  MENU_TIME2,
  MENU_CHECK2,
  MENU_MIN2,
  MENU_EXIT
} MenuIds;

String menuItems[MENU_LENGTH] = {
  "Flush #1",
  "Flush #2",  
  "Time #1",  // sec
  "Check #1", // hour
  "Level #1", // percent
  "Time #2",  // sec
  "Check #2", // hour
  "Level #2", // percent
  "Exit"
};
String menuSubtext[MENU_LENGTH] = {
  "",
  "",
  "Seconds",
  "Hours",
  "Percent",
  "Seconds",
  "Hours",
  "Percent",
  ""
};
byte menuValues[MENU_LENGTH] = {
  0, 
  0, 
  3,  // sec
  2,  // minute 
  30, // percent
  5,  // sec
  10, // hour
  30, // percent
  0
};
byte menuValuesMax[MENU_LENGTH] = {
  0, 
  0, 
  254, 
  254, 
  99, 
  254, 
  254, 
  99, 
  0
};

byte mode = MODE_STATUS;
byte menuIndex = 0;

unsigned long lastCheck1 = 0;
unsigned long lastCheck2 = 0;
unsigned long actionStart = 0;

void setup() {
  #ifdef DEBUG
  Serial.begin(DEBUG);
  #endif

  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
  pinMode(RELAY1, OUTPUT);
  pinMode(RELAY2, OUTPUT);
  pinMode(SENSOR1, INPUT);
  pinMode(SENSOR2, INPUT);

  digitalWrite(RELAY1, HIGH);
  digitalWrite(RELAY2, HIGH);
  
  #ifdef DEBUG
  Serial.print("EEPROM:");
  #endif
  for(byte i = 0; i < MENU_LENGTH; i++) {
    byte val = EEPROM.read(i);
    #ifdef DEBUG
    Serial.print(" ("); Serial.print(i); 
    Serial.print( ")="); Serial.print(val);
    #endif
    if (menuValues[i] && val && !isnan(val) && !isinf(val) && val <= menuValuesMax[i]) {
      menuValues[i] = val;
      #ifdef DEBUG
      Serial.print("*");
      #endif
    }
  }
  #ifdef DEBUG
  Serial.println("");
  #endif

  eb.setEncReverse(ENCODER_REVERSE);
}

void loop() {
  eb.tick();

  if (eb.turn()) {
    int dir = eb.dir();

    switch(mode) {
      case MODE_MENU:
        menuIndex += dir;
        if (menuIndex > MENU_LENGTH) menuIndex = MENU_LENGTH - 1;    
        else if (menuIndex == MENU_LENGTH) menuIndex = 0;
        drawMenu();
        break;
      case MODE_VALUE:
        if (eb.fast() && 
            menuValues[menuIndex] > VALUE_FAST_STEP && 
            menuValues[menuIndex] < menuValuesMax[menuIndex] - VALUE_FAST_STEP
        ) {
          menuValues[menuIndex] += dir * VALUE_FAST_STEP;
        }
        else {
          menuValues[menuIndex] += dir;
        }
        if (menuValues[menuIndex] > menuValuesMax[menuIndex]) menuValues[menuIndex] = 1;
        else if (menuValues[menuIndex] < 1) menuValues[menuIndex] = menuValuesMax[menuIndex];
        drawMenu();
        break;
    }

    return;
  }
  
  if (eb.click()) {
    switch (mode) {
      case MODE_STATUS:
        menuIndex = 0;
        mode = MODE_MENU;
        drawMenu();
        break;
      case MODE_MENU:
        switch(menuIndex) {
          case MENU_FLUSH1:
          case MENU_FLUSH2:
            mode = MODE_ACTION;
            actionStart = millis();
            if (menuIndex == MENU_FLUSH1) {
              digitalWrite(RELAY1, LOW);
              lastCheck1 = millis();
            }
            else {
              digitalWrite(RELAY2, LOW);
              lastCheck2 = millis();
            }
            drawAction();
            break;        
          case MENU_MIN1:
          case MENU_MIN2:
          case MENU_CHECK1:
          case MENU_TIME1:          
          case MENU_CHECK2:
          case MENU_TIME2:
            mode = MODE_VALUE;
            drawMenu();
            break;
          case MENU_EXIT:
            mode = MODE_STATUS;
            drawStatus();
            break;
        }
        break;
      case MODE_VALUE:        
        EEPROM.update(menuIndex, menuValues[menuIndex]);
        #ifdef DEBUG
        Serial.print("EEPROM("); Serial.print(menuIndex); 
        Serial.print( ") = "); Serial.println(menuValues[menuIndex]);
        #endif

        mode = MODE_MENU;
        drawMenu();
        break;
      case MODE_ACTION:
        digitalWrite(RELAY1, HIGH);
        digitalWrite(RELAY2, HIGH);
        mode = MODE_STATUS;
        drawStatus();
        break;
    }

    return;
  }
  
  byte sec = 0;
  byte val = 0;
  unsigned int value = 0;
  switch(mode) {
    case MODE_ACTION:
      switch(menuIndex) {
        case MENU_FLUSH1:
          sec = menuValues[MENU_TIME1];
          break;
        case MENU_FLUSH2:
          sec = menuValues[MENU_TIME2];
          break;
      }

      if (millis() - actionStart > 1000 * sec) {
        digitalWrite(RELAY1, HIGH);
        digitalWrite(RELAY2, HIGH);
        mode = MODE_STATUS;
        drawStatus();
      }
      else {
        drawAction();      
      }
      break;
    case MODE_STATUS:
      if (millis() - lastCheck1 > (unsigned long)menuValues[MENU_CHECK1] * 3600000) {
        lastCheck1 = millis();      
        val = measure1();
        
        #ifdef DEBUG
        Serial.print("Check #1 ("); Serial.print(menuValues[MENU_CHECK1]); Serial.print( " h): "); 
        Serial.print(val); Serial.print(" of "); Serial.println(menuValues[MENU_MIN1]);
        #endif

        if (val <= menuValues[MENU_MIN1]) {
          menuIndex = MENU_FLUSH1;
          mode = MODE_ACTION;
          actionStart = millis();
          digitalWrite(RELAY1, LOW);
          drawAction();
          return ;        
        }
      }

      if (millis() - lastCheck2 > (unsigned long)menuValues[MENU_CHECK2] * 3600000) {
        lastCheck2 = millis();
        val = measure2();
        
        #ifdef DEBUG
        Serial.print("Check #2 ("); Serial.print(menuValues[MENU_CHECK2]); Serial.print( " h): "); 
        Serial.print(val); Serial.print(" of "); Serial.println(menuValues[MENU_MIN2]);
        #endif

        if (val <= menuValues[MENU_MIN2]) {
          menuIndex = MENU_FLUSH2;
          mode = MODE_ACTION;
          actionStart = millis();
          digitalWrite(RELAY2, LOW);
          drawAction();
          return ;        
        }      
      }
      
      drawStatus();      
      break;  
  }
}

void drawMenu() {
  display.clearDisplay();
  //display.drawLine(0, 31, 127, 31, WHITE); //DELME

  display.setTextColor(1);
  display.setFont();

  if (menuSubtext[menuIndex] != "") {  
    display.setCursor(0, 24);
    display.print(menuSubtext[menuIndex]);
    display.setCursor(0, 9);
  }
  else display.setCursor(0, 17);

  display.setFont(&FreeSansBold9pt7b);
  display.print(menuItems[menuIndex]);

  if (menuValues[menuIndex]) {
    display.drawRoundRect(88, 0, 40, 31, 1, WHITE);
    
    if (mode == MODE_VALUE) {
      display.fillRect(89, 1, 38, 29, WHITE);
      display.setTextColor(0);
    }
    if (menuValues[menuIndex] > 99) display.setCursor(93, 20);
    else if (menuValues[menuIndex] > 9) display.setCursor(98, 20);
    else display.setCursor(103, 20);
    display.print(menuValues[menuIndex]);
  }

  display.display();
}

void drawAction() {
  display.clearDisplay();
  display.setFont(&FreeSansBold9pt7b);

  byte sec = 0;
  switch(menuIndex) {
    case MENU_FLUSH1:
      sec = menuValues[MENU_TIME1];
      break;
    case MENU_FLUSH2:
      sec = menuValues[MENU_TIME2];
      break;
  }
  int w = map(millis(), actionStart, actionStart + 1000 * sec, 1, 127);
  display.fillRect(0, 0, w, 31, WHITE);

  if (w > 64) {
    display.setTextColor(0);
    display.setCursor(4, 20);
  }
  else {
    display.setTextColor(1);
    display.setCursor(104, 20);    
  }
  switch(menuIndex) {
    case MENU_FLUSH1:
      display.print("#1");
      break;
    case MENU_FLUSH2:
      display.print("#2");
      break;
  }  

  display.display();
}

void drawStatus() {
  static unsigned long prevMillis = 0;
  static int dy = 0;
  static int dir = 1;
  
  display.clearDisplay();
  //display.drawLine(0, 31, 127, 31, WHITE); //DELME

  display.setFont();
  display.setTextColor(1);
  display.drawBitmap(98, 0 - dy, epd_bitmap_1, 30, 64, 1);

  drawStatusLine(1, dy, 
    measure1(), 
    menuValues[MENU_MIN1], 
    lastCheck1 / 1000 + (unsigned long)menuValues[MENU_CHECK1] * 3600 - millis() / 1000
  );
  
  drawStatusLine(2, dy, 
    measure2(), 
    menuValues[MENU_MIN2], 
    lastCheck2 / 1000 + (unsigned long)menuValues[MENU_CHECK2] * 3600 - millis() / 1000
  );

  display.display();

  unsigned int pause = 50;
  if (dy >= 32 || dy < 0) pause = 3000;
  
  if (millis() - prevMillis > pause) {    
    prevMillis = millis();
    dy += 1 * dir;
    if (dy >= 32) dir = -1;  
    else if (dy < 0) dir = 1;
  }
}

void drawStatusLine(byte line, int dy, byte p, byte minP, unsigned long sec) {
  dy += (line - 1) * -32;
  display.setTextSize(3);
  display.setCursor(0, 3 - dy);
  if (p < 10) display.print(" ");
  display.print(p);
  display.setTextSize(1);
  display.setCursor(36, 3 - dy);
  display.print("% ");
  byte k = min(max(((int)p - (int)minP) / 10, 1), 3);
  for(byte i=0; i<k; i++) display.write(0xAF);
  display.print(" ");
  display.print(minP);
  display.print("%");
  display.setCursor(38, 18 - dy);
  display.print(format_seconds(sec));
}

String format_seconds(unsigned long sec) {
      byte days = floor(sec / 86400);
      byte hours = floor((sec % 86400) / 3600);
      byte minutes = floor((sec % 3600) / 60);
      byte seconds = (sec % 3600) % 60; 
      String times = "";
      if (days > 0) {
        times.concat(days);
        times.concat("d ");
      }
      if (hours < 10) times.concat("0");
      times.concat(hours);
      times.concat(":");
      if (minutes < 10) times.concat("0");
      times.concat(minutes);
      if (days < 1) {
        times.concat(":");
        if (seconds < 10) times.concat("0");
        times.concat(seconds);   
      }
      return times;
}

byte measure1() {
  static unsigned long oldTime = 0;
  static byte oldVal = 0;
  if (!oldVal || millis() - oldTime > MEASURE_DEBOUNCE) {
    int value = analogRead(SENSOR1);
    #ifdef DEBUG
    Serial.print("Measure #1: "); 
    Serial.println(value);
    #endif

    oldVal = 100 - map(value, SENSOR_MIN, SENSOR_MAX, 1, 99);
    if (oldVal > 99) oldVal = 99;
    oldTime = millis();
  }
  return oldVal;
}

byte measure2() {
  static unsigned long oldTime = 0;
  static byte oldVal = 0;
  if (!oldVal || millis() - oldTime > MEASURE_DEBOUNCE) {
    int value = analogRead(SENSOR2);
    #ifdef DEBUG
    Serial.print("Measure #2: "); 
    Serial.println(value);
    #endif

    oldVal = 100 - map(value, SENSOR_MIN, SENSOR_MAX, 1, 99);
    if (oldVal > 99) oldVal = 99;
    oldTime = millis();
  }
  return oldVal;
}
NOCOMNCVCCGNDINLED1PWRRelay Module
NOCOMNCVCCGNDINLED1PWRRelay Module