//Copyright (C) 22.09.2023, Kirill ZHivotkov
#include <IRremote.h>
#include <LiquidCrystal_I2C.h>
#include <ezBuzzer.h>
#include <RTClib.h>
#include <Wire.h>

ezBuzzer buzzer(12);
LiquidCrystal_I2C lcd(39, 16, 2); //Address = 39
RTC_DS1307 rtc;

//Implement class "Remote Control Button"
class Button {
	
    private:
  
        String buttonName = "";
        int brightness = 10;

    public:

        //Set button name
        void setButtonName(String buttonName) {
          
            this->buttonName = buttonName;
          
        }
        //Set button name

        //Get button name
        String getButtonName() {
          
            return this->buttonName;
          
        }
        //Get button name

        //Set button name by means of remote control codes
        void setRemoteControlButton(unsigned long a) {

            if (a == 1570963200) {

                setButtonName("POWER"); //Power LCD on/off

            } else if (a == 534839040) {

                setButtonName("LEFT"); //Move LCD cursor to the right

            } else if (a == 1871773440) {

                setButtonName("RIGHT"); //Move LCD cursor to the left

            } else if (a == 1336999680) {

                setButtonName("EDIT_ON"); //Switch time setting mode on (blinking is cursor)

            } else if (a == 1470693120) {

                setButtonName("EDIT_OFF"); //Switch time setting mode off (blinking is not cursor)

            }  else if (a == 4161273600) {

                //setButtonName("MINUS"); //Decrease display brightness 

            }  else if (a == 3927310080) {

                //setButtonName("PLUS"); //Increase display brightness 

            }

        }
        //Set button name by means of remote control codes

        //LCD brightness control (take jumper from LCD 1602 and connect it to 9-nth PWM (when using Arduino Uno))
        /*
        void setDisplayBrightness() {

            if (this->getButtonName() == "MINUS") {

                if (this->brightness > 0) {

                    this->brightness -= 5;
                    //Serial.println(this->brightness);
                    analogWrite(9, this->brightness);

                }

            } else if (this->getButtonName() == "PLUS") {

                if (this->brightness < 255) {

                    this->brightness += 5;
                    //Serial.println(this->brightness);
                    analogWrite(9, this->brightness);

                }
            }
        }
        */
        //LCD brightness control (take jumper from LCD 1602 and connect it to 9-nth PWM (when using Arduino Uno))

        //Default contructor
        Button() {}
        //Default contructor

};
//Implement class "Remote Control Button"

//Implement class "Cursor"
class Cursor {
	
    private:
  
        int cursor = 0;

        int pos = 0;
        int row = 0;

        bool isSetMode = 0;

        float timing = 0;

    public:

        void setCursorPos(int cursor) { //Set LCD cursor value

            this->cursor = cursor;

        }

        void setLCDCursor(int pos, int row) { //Set cursor position on LCD display
          
            if ((pos >= 0 && pos <= 15) && (row == 0 || row == 1)) {

                this->pos = pos;
                this->row = row;
                lcd.setCursor(this->pos, this->row);

            }
    
        }

        int getCursorPos() { //Get cursor position

            return this->cursor;

        }

        void setIsSetModeValue(bool isSetMode) { //Set time assigning status (cursor is blinking or not)

            this->isSetMode = isSetMode;

        }

        bool getIsSetModeValue() { //Get time assigning status (cursor is blinking or not)

            return this->isSetMode;

        }

        void cursorStopBlink() { //Switch cursor blinking off
          
             lcd.noBlink();

        }

        void cursorStartBlink() { //Switch cursor blinking on
          
            lcd.blink();

        }

        void switchEditTimeMode(unsigned long a) { //Describe time setting mode (for cursor)

            if (a == 1336999680 && this->getIsSetModeValue() == 0) {

                lcd.setCursor(this->getCursorPos(), 0);
                this->cursorStartBlink();
                this->isSetMode = 1;

            } else if (a == 1470693120 && this->getIsSetModeValue() == 1) {

                lcd.setCursor(0, 0);
                this->cursorStopBlink();
                this->isSetMode = 0;

            }

        }

        /*
        void resetCursorAfterPause() { //If user didn't leave time setting mode

            if (((millis() - this->getCursorTiming()) >= 10000) && (this->getIsSetModeValue() == 1)) {

                this->cursorStopBlink();
                this->isSetMode = 0;
                this->setLCDCursor(this->getCursorPos(), 0);

            }

        }
        */

        //Конструктор по умолчанию
        Cursor() {}
        //Конструктор по умолчанию

};
//Implement class "Cursor"

//Create instances of the classes described above 
Cursor* cur = new Cursor();
Button* btn = new Button();
//Create instances of the classes described above 

//Implement class "Display"
class LCD {
	
    private:

        int digit = 0;

        //Time
        int h = 0; 
        int hh = 0;
        int m = 0;
        int mm = 0;
        //Time

        //Date
        int d = 0; 
        int dd = 1;
        int mth = 0;
        int mmth = 1;

        int y = 2;
        int yy = 0;
        int yyy = 0;
        int yyyy = 0;
        //Date

        //Weekday
        int weekDay = 0;
        //Weekday

        bool isOff = 0;

    public:

        bool checkHours(int h) { //Check hours range

            if (h >= 0 && h <= 23) {

                return true;

            }
            return false;

        }

        bool checkMinutes(int m) { //Check minutes range

            if (m >= 0 && m <= 59) {

                return true;

            }
            return false;

        }

        bool checkDays(int d) { //Check days range

            int days[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

            if (checkLeapYear(this->getYear()) == true && days[1] == 28) {

                days[1]++;

            }

            if (d >= 1 && d <= days[this->getMonth() - 1]) {

                return true;

            }
            return false;

        }

        bool checkMonths(int m) { //Check months range

            if (m >= 1 && m <= 12) {

                return true;

            }
            return false;

        }

        bool checkYears(int y) { //Check years range

            if (y >= 2000 && y <= 2030) {

                return true;

            }
            return false;

        }

        bool checkLeapYear(int y) { //Check leap year

            if (y % 400 == 0) {

                return true;

            } else if (y % 100 == 0) {

                return false;

            } else if (y % 4 == 0) {

                return true;

            } else {

                return false;

            }

        }

        int getMinute() { //Get a minute

            return this->m * 10 + this->mm;

        }

        int getHour() { //Get an hour

            return this->h * 10 + this->hh;

        }

        int getDay() { //Get a day

            return this->d * 10 + this->dd;

        }

        int getMonth() { //Get a month

            return this->mth * 10 + this->mmth;

        }

        int getYear() { //Get a year

            return this->y * 1000 + this->yy * 100 + this->yyy * 10 + this->yyyy;

        }

        int getWeekDay() { //Get a weekday

            return this->weekDay;

        }

        bool checkDisplayData() { //Check input data format

            if (this->checkHours(this->getHour()) == true && 
                this->checkMinutes(this->getMinute()) == true &&
                this->checkDays(this->getDay()) == true &&
                this->checkMonths(this->getMonth()) == true &&
                this->checkYears(this->getYear()) == true) {
                
                    return true;
                
                }
                return false;

        }
      
        void setIsOff(bool isOff) { //Set LCD status (if display is on or off)

            this->isOff = isOff;

        }

        bool getIsOff() { //Get LCD status (if display is on or off)

            return this->isOff;

        }

        void setClockTimeMode(unsigned long a) { //Time setting function

            if (cur->getIsSetModeValue() == 1) {

                if (a == 534839040) { //Move cursor to the left

                    if (cur->getCursorPos() > 0) {

                        //Time
                        if (cur->getCursorPos() == 3) {

                            cur->setCursorPos(1);
                            cur->setLCDCursor(cur->getCursorPos(), 0);

                        } 
                        //Time

                        //Date
                        else if (cur->getCursorPos() == 6) {

                            cur->setCursorPos(4);
                            cur->setLCDCursor(cur->getCursorPos(), 0);

                        } else if (cur->getCursorPos() == 9) {

                            cur->setCursorPos(7);
                            cur->setLCDCursor(cur->getCursorPos(), 0);

                        } else if (cur->getCursorPos() == 12) {

                            cur->setCursorPos(10);
                            cur->setLCDCursor(cur->getCursorPos(), 0);

                        } else {

                            cur->setCursorPos(cur->getCursorPos() - 1);
                            cur->setLCDCursor(cur->getCursorPos(), 0);

                        }
                        //Date

                    }

                } else if (a == 1871773440) { //Move cursor to the right

                    if (cur->getCursorPos() < 16) {

                        //Time
                        if (cur->getCursorPos() == 1) {

                            cur->setCursorPos(3);
                            cur->setLCDCursor(cur->getCursorPos(), 0);

                        } 
                        //Time

                        //Date
                        else if (cur->getCursorPos() == 4) {

                            cur->setCursorPos(6);
                            cur->setLCDCursor(cur->getCursorPos(), 0);

                        }  else if (cur->getCursorPos() == 7) {

                            cur->setCursorPos(9);
                            cur->setLCDCursor(cur->getCursorPos(), 0);

                        }  else if (cur->getCursorPos() == 10) {

                            cur->setCursorPos(12);
                            cur->setLCDCursor(cur->getCursorPos(), 0);

                        }  else {

                            cur->setCursorPos(cur->getCursorPos() + 1);
                            cur->setLCDCursor(cur->getCursorPos(), 0);

                        }
                        //Date

                    }

                } else {

                    if (cur->getCursorPos() >= 0 && cur->getCursorPos() <= 15) {

                        if (a == 2540240640) {  // Key 0

                            this->setClockDigit(0);
                            btn->setButtonName("DIGIT");

                        } else if (a == 3476094720) {  // Key 1

                            this->setClockDigit(1);
                            btn->setButtonName("DIGIT");

                        } else if (a == 3877175040) {  // Key 2

                            this->setClockDigit(2);
                            btn->setButtonName("DIGIT");

                        } else if (a == 2239430400) {  // Key 3

                            this->setClockDigit(3);
                            btn->setButtonName("DIGIT");

                        } else if (a == 4010868480) {  // Key 4

                            this->setClockDigit(4);
                            btn->setButtonName("DIGIT");

                        } else if (a == 3342401280) {  // Key 5

                            this->setClockDigit(5);
                            btn->setButtonName("DIGIT");

                        } else if (a == 2774204160) {  // Key 6

                            this->setClockDigit(6);
                            btn->setButtonName("DIGIT");

                        } else if (a == 3175284480) {  // Key 7

                            this->setClockDigit(7);
                            btn->setButtonName("DIGIT");

                        } else if (a == 3041591040) {  // Key 8

                            this->setClockDigit(8);
                            btn->setButtonName("DIGIT");

                        } else if (a == 2907897600) {  // Key 9

                            this->setClockDigit(9);
                            btn->setButtonName("DIGIT");
                            
                        }
                    }
                }

            } else { //


            }

        }

        void setClockDigit(int digit) { //Input and output LCD dispay digits from 1 to 9
          
            this->digit = digit;

            if (this->digit >= 0 && this->digit <= 9) {

                if (cur->getCursorPos() == 0) { //Hours

                    this->h = this->digit;

                    cur->cursorStopBlink();
                    this->setDisplay(this->h);
                    cur->cursorStartBlink();

                } else if (cur->getCursorPos() == 1) { //Hours

                    this->hh = this->digit;

                    cur->cursorStopBlink();
                    this->setDisplay(this->hh);
                    cur->cursorStartBlink();

                } else if (cur->getCursorPos() == 3) { //Minutes

                    this->m = this->digit;

                    cur->cursorStopBlink();
                    this->setDisplay(this->m);
                    cur->cursorStartBlink();

                } else if (cur->getCursorPos() == 4) { //Minutes

                    this->mm = this->digit;

                    cur->cursorStopBlink();
                    this->setDisplay(this->mm);
                    cur->cursorStartBlink();

                } else if (cur->getCursorPos() == 6) { //Days

                    this->d = this->digit;

                    cur->cursorStopBlink();
                    this->setDisplay(this->d);
                    cur->cursorStartBlink();

                } else if (cur->getCursorPos() == 7) { //Days

                    this->dd = this->digit;

                    cur->cursorStopBlink();
                    this->setDisplay(this->dd);
                    cur->cursorStartBlink();

                } else if (cur->getCursorPos() == 9) { //Months

                    this->mth = this->digit;

                    cur->cursorStopBlink();
                    this->setDisplay(this->mth);
                    cur->cursorStartBlink();

                } else if (cur->getCursorPos() == 10) { //Months

                    this->mmth = this->digit;

                    cur->cursorStopBlink();
                    this->setDisplay(this->mmth);
                    cur->cursorStartBlink();

                } else if (cur->getCursorPos() == 12) { //Years

                    this->y = this->digit;

                    cur->cursorStopBlink();
                    this->setDisplay(this->y);
                    cur->cursorStartBlink();

                } else if (cur->getCursorPos() == 13) { //Years

                    this->yy = this->digit;

                    cur->cursorStopBlink();
                    this->setDisplay(this->yy);
                    cur->cursorStartBlink();

                } else if (cur->getCursorPos() == 14) { //Years

                    this->yyy = this->digit;

                    cur->cursorStopBlink();
                    this->setDisplay(this->yyy);
                    cur->cursorStartBlink();

                } else if (cur->getCursorPos() == 15) { //Years

                    this->yyyy = this->digit;

                    cur->cursorStopBlink();
                    this->setDisplay(this->yyyy);
                    cur->cursorStartBlink();

                } 

                if (this->checkDisplayData() == true) { //Move through the first display row to the right after a digit was confirmed to be set

                    if (cur->getCursorPos() != 1 && 
                        cur->getCursorPos() != 4 && 
                        cur->getCursorPos() != 7 && 
                        cur->getCursorPos() != 10) {

                        if (cur->getCursorPos() >= 0 && cur->getCursorPos() < 15) {

                            cur->setCursorPos(cur->getCursorPos() + 1);
                            cur->setLCDCursor(cur->getCursorPos(), 0);

                        }

                    } else {

                        cur->setCursorPos(cur->getCursorPos() + 1);
                        cur->setCursorPos(cur->getCursorPos() + 1);
                        cur->setLCDCursor(cur->getCursorPos(), 0);

                    }

                }
            }
        }

        void setAdjustedClockTime() { //Set a new time (clock)

            if (this->getIsOff() == 0) { //If LCD display is on

                DateTime now = rtc.now();

                if (now.hour() < 10) {

                    this->h = 0;
                    this->hh = now.hour();

                    Serial.print(" h >>> ");
                    Serial.print(this->h);
                    Serial.print(" hh >>> ");
                    Serial.print(this->hh);

                } else {

                    this->h =  now.hour() / 10;
                    this->hh = now.hour() % 10;

                    Serial.print(" h >>> ");
                    Serial.print(this->h);
                    Serial.print(" hh >>> ");
                    Serial.print(this->hh);

                }

                if (now.minute() < 10) {

                    this->m = 0;
                    this->mm = now.minute();

                    Serial.print(" m >>> ");
                    Serial.print(this->m);
                    Serial.print(" mm >>> ");
                    Serial.print(this->mm);

                } else {

                    this->m = now.minute() / 10;
                    this->mm = now.minute() % 10;

                    Serial.print(" m >>> ");
                    Serial.print(this->m);
                    Serial.print(" mm >>> ");
                    Serial.print(this->mm);
                    
                }

                Serial.print(" ");
                Serial.print(now.second());
                Serial.println();

                if (now.day() >= 0 && now.day() < 10) {

                    this->d = 0;
                    this->dd = now.day();

                } else {
                    
                    if (now.day() >= 1 && now.day() <= 31) {
                    
                        this->d = now.day() / 10;
                        this->dd = now.day() % 10;

                    }

                }

                if (now.month() >= 0 && now.month() < 10) {

                    this->mth = 0;
                    this->mmth = now.month();

                } else {

                    if (now.month() >= 10 && now.month() <= 12) {

                        this->mth = now.month() / 10;
                        this->mmth = now.month() % 10;
                    
                    }

                }

                if (now.year() >= 2000 && now.year() <= 2030) {

                    this->y    = now.year() / 1000;
                    this->yy   = now.year() / 100 % 10;
                    this->yyy  = now.year() / 10 % 10;
                    this->yyyy = now.year() % 10;

                }

                this->weekDay = now.dayOfTheWeek();

                this->displayProcessedTime();
                this->displayProcessedDate();
                this->displayProcessedWeekDay();

                delay(1000);

            } else { //Else

                //Reset time
                this->clearDisplay();
                rtc.adjust((DateTime(2000, 1, 1, 0, 0, 0)));
                //Reset time

            }

        }

        void setDisplay(int data) { //Output info to the display (number)
          
             lcd.print((String)data);

        }

        void setDisplay(String data) { //Output info to the display (string)
          
             lcd.print(data);

        }

        void displayProcessedDate() { //Output calculated date

            cur->setLCDCursor(6, 0);

            //Day
            if (this->d * 10 + this->dd < 10) {

                this->setDisplay("0" + (String)(this->d * 10 + this->dd));

            } else {

                this->setDisplay((String)(this->d * 10 + this->dd));

            }
            //Day

            this->setDisplay("-");

            //Month
            if (this->mth * 10 + this->mmth < 10) {

                this->setDisplay("0" + (String)(this->mth * 10 + this->mmth));

            } else {

                this->setDisplay((String)(this->mth * 10 + this->mmth));

            }
            //Month

            this->setDisplay("-");

            //Year
            int tmp_year = this->y * 1000 + this->yy * 100 + this->yyy * 10 + this->yyyy;
            if (tmp_year >= 2000 && tmp_year <= 2030) {

                this->setDisplay((String)(tmp_year));

            } else {

                this->setDisplay("2000");

            }
            //Year

        }

        void displayProcessedWeekDay() { //Output calculated weekday

            String week[7] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
          
            cur->setLCDCursor(0, 1);
            this->setDisplay(week[this->weekDay]);

        }

        void displayProcessedTime() { //Output calculated time

            cur->setLCDCursor(0, 0);

            //Hour
            if (this->h * 10 + this->hh < 10) {

                this->setDisplay("0" + (String)(this->h * 10 + this->hh));

            } else {

                this->setDisplay((String)(this->h * 10 + this->hh));

            }
            //Hour

            this->setDisplay(":");

            //Minute
            if (this->m * 10 + this->mm < 10) {

                this->setDisplay("0" + (String)(this->m * 10 + this->mm));

            } else {

                this->setDisplay((String)(this->m * 10 + this->mm));

            }
            //Minute

        }
 
        void switchDisplayOn() { //Switch LCD display on
          
            lcd.display();

        }

        void switchBackLightOn() { //Switch LCD backlight on
          
            lcd.backlight();

        }

        void switchDisplayOff() { //Switch LCD display off
          
            lcd.noDisplay();

        }

        void switchBackLightOff() { //Switch LCD backlight off
          
            lcd.noBacklight();

        }

        void clearDisplay() { //Clear the whole display
          
            cur->cursorStopBlink();
            cur->setIsSetModeValue(0);
            lcd.clear();

        }

        void switchDisplayOnOff(unsigned long a) { //Compound function to switch LCD display on/off
       
            if (this->getIsOff() == 0) {

                this->switchBackLightOff();
                this->switchDisplayOff();
                this->setIsOff(1);
                cur->cursorStopBlink();
                  
            } else if (this->getIsOff() == 1) {

                this->switchDisplayOn();
                this->switchBackLightOn();
                cur->setIsSetModeValue(0);
                this->setIsOff(0);

            }

        }

        //Default constructor
        LCD() {

            lcd.init();
            lcd.backlight();
            lcd.setCursor(0, 0);

            //pinMode(9, OUTPUT); //if LCD backlight jumper is removed (for Arduino Uno)
            //analogWrite(9, 10);

        }
        //Default constructor

};
//Implement class "Display"

//Create an instance of the class described above 
LCD* disp;
//Create an instance of the class described above 

//Setup
void setup() {

    disp = new LCD();
    disp->clearDisplay();

    Wire.begin();
    rtc.begin();
    rtc.adjust((DateTime(2000, 1, 1, 0, 0, 0)));
    
    pinMode(A0, INPUT);

    IrReceiver.begin(A0, ENABLE_LED_FEEDBACK);
    Serial.begin(9600);

}
//Setup

//Implement class "Sound"
class Sound {
	
    private:

        String msg = "";
  
    public:

        void setErrorMessage(String msg) { //Set an error message restriction

            if (msg.length() == 16) {

                this->msg = msg;

            }

        }

        void showErrorMessage() { //Output an error message (time or date were set incorrectly)
          
            cur->cursorStopBlink();
            cur->setLCDCursor(0, 1);
            disp->setDisplay(this->msg);
            cur->setLCDCursor(cur->getCursorPos(), 0);
            cur->cursorStartBlink();
        
        }

        void playErrorSound() { //Make an error sound if the clock input data were incorrect

            if (disp->checkDisplayData() == false) {

                buzzer.beep(100);
                this->setErrorMessage("Incorrect format");
                this->showErrorMessage();

            } else {

                this->setErrorMessage("                ");
                this->showErrorMessage();
                
            }

        }

};
//Implement class "Sound"

Sound* snd = new Sound();

//Program loop
void loop() {

    buzzer.loop();
    
    //Output time that was set with RTC
    if (disp->checkDisplayData() == true) { //If clock data is correct

        if (cur->getIsSetModeValue() == 0) { //If clock is not in edit mode
          
            disp->setAdjustedClockTime();

        } else { //Switch cursor highlight off if the user didn't leave time setting mode

            //cur->resetCursorAfterPause();

        }

    }
    //Output time that was set with RTC

    if (IrReceiver.decode()) {

        unsigned long a = IrReceiver.decodedIRData.decodedRawData; //Receive a value from remote control
        IrReceiver.resume();

        btn->setRemoteControlButton(a); //Get button name by its code (remote control)

        //btn->setDisplayBrightness(); //Display brightness control

        disp->setClockTimeMode(a); //Set a clock time and date

        cur->switchEditTimeMode(a); //Switch between edit mode and running time (cursor) 

        if (btn->getButtonName() == "EDIT_OFF" && disp->checkDisplayData() == true) { //Set new RTC values if some new time settings' values were confirmed (a user entered new time and date)

            disp->clearDisplay();

            /*
            Serial.print(" year > ");
            Serial.println(disp->getYear());
            Serial.print(" month > ");
            Serial.println(disp->getMonth());
            Serial.print(" day > ");
            Serial.println(disp->getDay());
            Serial.print(" hour > ");
            Serial.println(disp->getHour());
            Serial.print(" minute > ");
            Serial.println(disp->getMinute());
            */

            rtc.adjust((DateTime(disp->getYear(), disp->getMonth(), disp->getDay(), disp->getHour(), disp->getMinute(), 0)));

        }

        if (btn->getButtonName() == "POWER") { //Power LCD on/off

            disp->switchDisplayOnOff(a);
            
        }

        if (btn->getButtonName() == "DIGIT") { //Emit a sound if some incorrect time settings were applied (wrong time or date formats)

            snd->playErrorSound();
            
        }
    }
}
//Program loop
GND5VSDASCLSQWRTCDS1307+