#include <Wire.h>
#include <hd44780.h> //  https://github.com/duinoWitchery/hd44780
#include <hd44780ioClass/hd44780_I2Cexp.h> // i2c expander i/o class header

#include <Toggle.h>   // https://github.com/Dlloydev/Toggle
#include <Encoder.h>  // https://www.pjrc.com/teensy/td_libs_Encoder.html

// LE LCD
hd44780_I2Cexp lcd;
const int nbCols = 20;
const int nbLignes = 4;

byte fleche[] = {
  0b00000,
  0b00000,
  0b00100,
  0b00010,
  0b11111,
  0b00010,
  0b00100,
  0b00000
};

// L'ENCODEUR
const byte encoderCLKPin = 2;
const byte encoderDTPin  = 3;
Encoder encoder(encoderDTPin, encoderCLKPin);
long encoderPosition;


// LE BOUTON DE L'ENCODEUR
const byte encoderSWPin = 4;
Toggle encoderSwitch;


// UN TYPE POUR SIMPLIFIER LA DECLARATION D'UN CALLBACL
typedef void (*FunctionPointer)(byte);


struct ElementMenu {
  const char * texte;
  FunctionPointer callback;
};

struct Menu {
  const char * titre;
  const byte nombreElements;
  ElementMenu * elements;
};

Menu * menuEnCours = nullptr;
byte posDebut = 0; // l'index du premier élément de menu visible à l'écran
byte posChoix = 0; // l'index du menu pointé par la flèche

//------------------

void callback1(byte menupos);
void callback2(byte menupos);
void choixMenu1(byte menupos);
void choixMenu2(byte menupos);

ElementMenu elems1[] = {
  {"fonction1", callback1},
  {"fonction2", callback1},
  {"choixMenu2", choixMenu2},
  {"fonction3", callback1},
  {"fonction4", callback1},
  {"fonction5", callback1},
};

ElementMenu elems2[] = {
  {"m2, fonction1", callback2},
  {"m2, choixMenu1", choixMenu1},
};



Menu menuPrincipal = {"MENU1", sizeof elems1 / sizeof * elems1, elems1};
Menu menuSecondaire = {"MENU2", sizeof elems2 / sizeof * elems2, elems2};


void callback1(byte menupos) {
  Serial.print("callback MENU1 - click sur ");
  Serial.println(menuEnCours->elements[menupos].texte);
}

void callback2(byte menupos) {
  Serial.print("callback MENU2 - click sur ");
  Serial.println(menuEnCours->elements[menupos].texte);
}

void choixMenu1(byte menupos) {
  Serial.println("aller au menu 1");
  menuEnCours = &menuPrincipal;
}

void choixMenu2(byte menupos) {
  Serial.println("aller au menu 2");
  menuEnCours = &menuSecondaire;
}


bool encoderChanged() {
  bool changed = false;
  long newPosition = encoder.read() >> 2;

  if (newPosition != encoderPosition) {
    if (menuEnCours != nullptr) {
      if (newPosition < 0) {
        encoder.write(0);
        changed = true;
      } else if (newPosition >= menuEnCours->nombreElements) {
        encoder.write((menuEnCours->nombreElements - 1) << 2);
      } else {
        encoderPosition = newPosition;
        changed = true;
      }
    }
  }
  return changed;
}

void effacerLigne(int ligne) {
  lcd.setCursor(0, ligne);
  for (int i = 0; i < nbCols; i++) lcd.write(' ');
}

void afficherCentre(const char * texte, int ligne) {
  int col = 0;
  int longueur =  strlen(texte);
  if (longueur < nbCols) col = (nbCols - longueur) / 2;
  lcd.setCursor(col, ligne);
  lcd.print(texte);
}

void afficherElement(const char * texte, int ligne) {
  lcd.setCursor(1, ligne);
  lcd.print(texte);
}

void afficherMenu(byte debut, byte menuPos) {
  static Menu * ancienMenu = nullptr;
  static byte ancienMenuPos = 255;
  static byte ancienDebut = 255;

  if (menuEnCours != ancienMenu) {
    lcd.clear();
    if (menuEnCours != nullptr) {
      afficherCentre(menuEnCours->titre, 0);
      for (byte y = 0; y < nbLignes - 1; y++) {
        if (y < menuEnCours->nombreElements) afficherElement(menuEnCours->elements[y].texte, y + 1);
      }
      lcd.setCursor(0, 1); lcd.write(0); // affiche la flèche sur la première entrée
    }
    encoder.write(0);
    posDebut = 0;
    posChoix = 0;
    ancienMenuPos = 0;
    ancienDebut = 0;
    ancienMenu = menuEnCours;
  } else if (debut != ancienDebut) {
    byte l = 1;
    for (byte y = debut; l < nbLignes; y++, l++) {
      effacerLigne(l);
      if (menuEnCours != nullptr && y < menuEnCours->nombreElements) afficherElement(menuEnCours->elements[y].texte, l);
    }
    ancienDebut = debut;
  }

  if (menuPos != ancienMenuPos) {
    for (byte i = 1; i < nbLignes; i++) {
      lcd.setCursor(0, i); lcd.write(' ');
    }
    lcd.setCursor(0, 1 + menuPos - debut);
    lcd.write(0); // affiche la flèche
    ancienMenuPos = menuPos;
  }

}

void gestionMenu() {
  if (encoderChanged()) {
    posChoix = encoderPosition;
    if (posChoix < posDebut) {
      posDebut = posChoix;
    }
    else if (posChoix >= posDebut + nbLignes - 1) {
      posDebut = posChoix - nbLignes + 2;
    }
  }
  afficherMenu(posDebut, posChoix);

  encoderSwitch.poll();
  if (encoderSwitch.onPress()) {
    if (menuEnCours != nullptr)  menuEnCours->elements[posChoix].callback(posChoix);
  }
}



void setup() {
  encoderSwitch.begin(encoderSWPin);
  Serial.begin(115200);

  int result = lcd.begin(nbCols, nbLignes);
  if (result) {
    Serial.print("LCD initialization failed: ");
    Serial.println(result);
    hd44780::fatalError(result);
  }
  lcd.createChar(0, fleche);

  menuEnCours = &menuPrincipal;

}

void loop() {
  gestionMenu();
}