//----- Library definition -----//
#include <WiFi.h>
#include "PubSubClient.h"
#include <Keypad.h>

//----- Connectivity configuration -----//
const char* ssid = "Wokwi-GUEST"; //WiFi network identification AP 
const char* password = ""; // Password for the WiFi connectivity
const char* mqttServer = "broker.emqx.io"; // MQTT Server URL
const int port = 1883; // MQTT Port for non secure connection. Use 8883 for TLS.
const char* topicController = "sdg/accessControl/controller"; // Topic used for the controller
const char* topicValidator = "sdg/accessControl/validator"; // Topic used for the access keypad
char clientId[50]; // Client ID used for the MQTT client connecition

WiFiClient espClient; // Creates an instance of the WiFiClient to be used for MQTT connection
PubSubClient client(espClient); // Creates an instance of PubSubClient to handle the MQTT connection

//----- GPIO Pinout -----//
const int redLedPin = 2; // Pin used for the Red LED as output
const int greenLedPin = 15; // Pin used for the Green LED as output
const int buzzerPin = 4; // Ping used for the buzzer as output
const int distanceTrigger = 18; // Pin used to generate emitter signal to the ultrasonic sensor as output
const int distanceEcho = 5; // Pin used to receive the emitter signal from the ultrasonic sensor as input

//----- Keypad configuration -----//
const uint8_t ROWS = 4; 
const uint8_t COLS = 4;
char keys[ROWS][COLS] = {
  { '1', '2', '3', 'A' },
  { '4', '5', '6', 'B' },
  { '7', '8', '9', 'C' },
  { '*', '0', '#', 'D' }
};
uint8_t colPins[COLS] = { 26, 25, 33, 32 }; // Pins connected to C1, C2, C3, C4
uint8_t rowPins[ROWS] = { 13, 12, 14, 27 }; // Pins connected to R1, R2, R3, R4
Keypad keypad = Keypad(makeKeymap(keys), rowPins, colPins, ROWS, COLS); // Creates the instance of the keypad

//----- Global variables -----//
float distance = 0.0; // Variable to allocate the distance data of the ultrasonic sensor
String currentCode = ""; // Variable to allocate the code introduced by the keypad
int idCode = 0; // Variable to allocate an ID of the code introduce to match the response

void setup() {
  Serial.begin(115200); // Initialize the serial communication to 115200 bauds
  randomSeed(analogRead(0)); // Initialize the seed for random numbers according to the analog input 0

  delay(10);
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid); // Prints the AP to be connected

  wifiConnect(); // Call the method to connect to the WiFi network

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP()); // Prints the IP given by the router during the WiFi connection
  Serial.println(WiFi.macAddress()); // Prints the MAC address of the board network adapter
  
  client.setServer(mqttServer, port); // Configures the MQTT client to its later connection
  client.setCallback(callback); // Sets the callback of the MQTT client to listen to events

  pinMode(redLedPin, OUTPUT); // Sets the Red LED pin 2 as output
  pinMode(greenLedPin, OUTPUT); // Sets the Greed LED pin 15 as output
  pinMode(distanceTrigger, OUTPUT); // Sets the Ultrasonic emitter pin 18 as output
  pinMode(distanceEcho, INPUT); // Sets the Ultrasonic receiver pin 5 as input
  pinMode(buzzerPin, OUTPUT); // Sets the buzzer pin 4 as output

}

//----- WiFi Connectivity method -----//
void wifiConnect() {
  WiFi.mode(WIFI_STA); // Configures the WiFi in station mode to get to internet through the router
  WiFi.begin(ssid, password); // Connects to the WiFi with the credentials given
  while (WiFi.status() != WL_CONNECTED) { // Waits until the connection is stablished
    delay(500); // Delay of 500ms 
    Serial.print("."); // Prints in the serial port until the WiFi connectivity is stablished
  }
}

//----- MQTT Reconnection method -----//
void mqttReconnect() {
  while (!client.connected()) { // Checks if the client is connected to the MQTT Server
    Serial.print("Attempting MQTT connection...");
    long r = random(1000); // Generate a random number from 1 to 1000
    sprintf(clientId, "clientId-%ld", r); // Concatenate a string with the random number for the MQTT client ID
    if (client.connect(clientId)) { // Checks if the MQTT client is already connected
      Serial.print(clientId);
      Serial.println(" connected");
      client.subscribe(topicController); // Subscribe to the controller topic to receive the responses
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state()); // Prints the status of the MQTT connection
      Serial.println(" try again in 5 seconds");
      delay(5000);
    }
  }
}

//----- Check the MQTT Connection method -----//
bool checkConnection() {
  delay(10);
  if (!client.connected()) { // Checks if the MQTT client is connected
    mqttReconnect(); // Call the MQTT reconnection method in case the client disconnects 
  }
  client.loop(); // Makes sure the connection is stablished and checks if any event has occured
  return client.connected(); // Return the boolean status of the MQTT connection
}

//----- Ultrasonic sensor reading method -----//
float readDistanceCM() {
  digitalWrite(distanceEcho, LOW); // Sets the ultrasonic sensor receiver pin to 0
  delayMicroseconds(2); // Wait 2 miliseconds to make sure the receiver is in low state
  digitalWrite(distanceTrigger, HIGH); // Sets the ultrasonic sensor emitter to 1
  delayMicroseconds(10); // Wait 10 miliseconds to make sure the emitter is in high state
  digitalWrite(distanceTrigger, LOW); // Sets the ultrasonic sensor emitter to 0
  int duration = pulseIn(distanceEcho, HIGH); // Reads the pulse received and calculate its duration 
  int distance = int(duration * 0.034 / 2); // Algorithm to convert pulse duration in distance
  return distance;
}

//----- Manage the user feedback of the controller -----//
void feedbackAccessValidation(bool accessGranted) {
  if(accessGranted == true) { // Checks if the access is granted
    Serial.print("Access granted"); // Prints in the serial comms the granted message
    digitalWrite(greenLedPin, HIGH); // Turns the green LED on
    digitalWrite(redLedPin, LOW); // Turns the red LED off
    tone(buzzerPin, 800, 250); //Create a tone in output buzzerPin of 200Hz and for 250ms.
    delay(2000); // Maintain the GPIO status for 2 seconds
    digitalWrite(greenLedPin, LOW); // Turns the green led off 
  }else{
    Serial.print("Access denied"); // Prints in the serial comms the denied message
    digitalWrite(greenLedPin, LOW); // Turns the green LED off
    digitalWrite(redLedPin, HIGH); // Turns the red LED on
    tone(buzzerPin, 200, 1000); //Create a tone in output buzzerPin of 800Hz and for 1000ms.
    delay(2000); // Maintain the GPIO status for 2 seconds 
    digitalWrite(redLedPin, LOW); // Turns the red LED off
  }
}

//----- Callback for MQTT Listener events ----//
void callback(char* topic, byte* message, unsigned int length) {
  Serial.print("Message arrived on topic: "); // Prints that a new message has arrived
  Serial.print(topic); // Prints the topic of the arrived message
  Serial.print(". Message: ");
  String stMessage; // Define the variable to store the message received in String format
  
  for (int i = 0; i < length; i++) { // Compose the string message byte by byte
    Serial.print((char)message[i]);
    stMessage += (char)message[i];
  }
  Serial.println();

  if (String(topic) == topicController) { // Checks if the message come from the controller topic
    // JSON Manual decodification // 
    int index = stMessage.indexOf(','); // Searches for a , simbol position
    String sub_S1 = stMessage.substring(0,index); // Gets the substring from position 0 to the previous calculated index 
    int index_S1 = sub_S1.indexOf(':'); // Searches for a : simbol position
    String id = sub_S1.substring(index_S1+1,sub_S1.length()); // Gets the id code from substring calculated from indexes
    String sub_S2 = stMessage.substring(index,stMessage.length()); // Gets a substring to obtain the code
    int index_S2 = sub_S2.indexOf(':'); // Searches for a : simbol position
    String resp = sub_S2.substring(index_S2+1,sub_S2.length()-1); // Gets the response of the controller from the calculated indexes
    //End JSON Manual decodification//

    Serial.print("Validation received - idCode: ");
    Serial.print(id); // Prints the ID Code received by the controller
    Serial.print(" - Response: ");
    Serial.println(resp); // Prints the response received by the controller
    if(id == String(idCode)) { // Checks if the ID code received is the latest one sent
      if(resp == "True"){ // If positive match
        feedbackAccessValidation(true); // Call the method to manage the positive GPIO outputs using true as parameter
      }else{
        feedbackAccessValidation(false);  // Call the method to manage the negative GPIO outputs using false as parameter
      }
    }else{
      Serial.println("ID Code not match the last one. Validation rejected");
    }
  }else{
        Serial.print("Topic does not match");
  }

}

//----- Publishing message method -----//
void publishMessage(String code) {
    String packet = ""; // creates the variable used to be published
    // Compose the JSON object by concatenatic strings //
    packet.concat(("{\"reader\":\"001""\"")); 
    packet.concat((",\"idCode\":"));
    idCode = random(1000000); // Generate a random number from 1 to 1000000 as ID Code
    packet.concat(idCode);
    packet.concat((",\"code\":\""));
    packet.concat(currentCode);
    packet.concat("\"}");
    // End of JSON Composer //
    Serial.println(packet); // Prints in the serial port the message composed
    client.publish(topicValidator, packet.c_str()); // Publish the composed message as a pointer to a null-terminated character array
}

//----- Main programe method -----//
void loop() {
  checkConnection(); // Checks if the MQTT connection is stablished
  if(distance != readDistanceCM()) { // Checks if latest distance is the same to avoid excesive debugging
      distance = readDistanceCM(); // Sets the latest read distance to the new reading
      Serial.print("Distance: ");
      Serial.println(distance); // Prints in the serial port the distance read
  }

  if(distance < 100) { // Checks if the distance read is less than 100cm.
    char key = keypad.getKey(); // If the distance is less than 1m it enables the keypad reading
    
    if (key != NO_KEY) { // Checks if a key is pressed in the keypad 
      currentCode += String(key); // Concatenate the keys pressed to componse a code
      Serial.println(currentCode); // Prints the current introduced keys
      if('#' == key){ // Check if the # simbol is pressed, which means the code is going to be sent to the controller
        publishMessage(currentCode); // Publishes the message with the code introduced
        currentCode = ""; // Reset the currentCode introduced
      }
    }
  }
}