/**
 **************************************************
 * @file        Inkplate4TEMPERA_Image_Frame_Gesture.ino
 * @brief       Battery-friendly SD card slideshow controlled by left/right
 *              gestures using the APDS9960 sensor and deep sleep wake-up.
 *
 * @details     This example implements an image-frame slideshow on Inkplate 4
 *              TEMPERA. Images are loaded from a specified folder on a FAT-
 *              formatted microSD card and rendered to the e-paper display in
 *              3-bit grayscale mode (INKPLATE_3BIT).
 *
 *              Navigation is controlled by the onboard APDS9960 gesture sensor:
 *              a LEFT gesture advances to the next image and a RIGHT gesture
 *              goes to the previous one. Between image changes, the ESP32 is
 *              placed into deep sleep to reduce power consumption. The APDS9960
 *              remains powered and configured so it can wake the ESP32 via an
 *              interrupt chain (APDS9960 -> IO expander -> ESP32 GPIO).
 *
 *              To preserve state across deep sleep resets, the sketch stores the
 *              image directory indices, file count, and current position in RTC
 *              memory (RTC_DATA_ATTR). Due to RTC memory constraints, the image
 *              list is limited to 512 entries.
 *
 * Requirements:
 * - Board:      Soldered Inkplate 4 TEMPERA
 * - Hardware:   Inkplate 4 TEMPERA, USB-C cable, microSD card
 * - Extra:      microSD card (FAT/FAT32), image files on the card
 *
 * Configuration:
 * - Boards Manager -> Inkplate Boards -> Soldered Inkplate 4 TEMPERA
 * - Set folderPath to the image directory on the SD card (must end with '/')
 *
 * Don't have Inkplate Boards in Arduino Boards Manager?
 * See https://docs.soldered.com/inkplate/10/quick-start-guide/
 *
 * How to use:
 * 1) Format a microSD card as FAT/FAT32.
 * 2) Create a folder (e.g. /images/) and copy your images into it.
 * 3) Set folderPath in the sketch to match your folder (must end with '/').
 * 4) Upload the sketch and insert the SD card.
 * 5) The current image is displayed; swipe LEFT/RIGHT over the APDS9960 sensor
 *    area to change images.
 *
 * Expected output:
 * - E-paper: One image displayed full-screen. LEFT/RIGHT gestures change the
 *   image; the device sleeps between gestures for low power usage.
 *
 * Notes:
 * - Display mode is 3-bit grayscale (8 levels). Partial update is not available
 *   in grayscale mode; each image change uses a full refresh (display.display()).
 * - Supported file types depend on the Inkplate image decoder. The sketch
 *   attempts to open and draw images and will skip files it cannot render.
 * - Image size: keep images within the display resolution (e.g. <= 600x600) to
 *   avoid cropping or decode failures.
 * - File limit: maximum of 512 images due to RTC memory allocation for
 *   imageIndexes[].
 * - Deep sleep restarts the ESP32 on every wake. RTC_DATA_ATTR variables retain
 *   state, but normal RAM does not.
 * - Gesture sensitivity is set to the lowest gain to reduce accidental triggers.
 *
 * Docs:         https://docs.soldered.com/inkplate
 * Support:      https://forum.soldered.com/
 *
 * @author      Soldered
 * @date        2023-10-02
 * @license     GNU GPL V3
 **************************************************/

// Next 3 lines are a precaution, you can ignore those, and the example would also work without them
#ifndef ARDUINO_INKPLATE4TEMPERA
#error "Wrong board selection for this example, please select Inkplate 4 TEMPERA in the boards menu."
#endif

/******************CHANGE HERE***********************/

// Path to the folder with pictures (e.g. there is a folder called images on the SD card)
const char folderPath[] = "/images/"; // NOTE: Must end with /

/****************************************************/

#include "Inkplate.h"            // Include Inkplate library to the sketch
Inkplate display(INKPLATE_3BIT); // Create an object on Inkplate library and also set library into 3 Bit mode
SdFile folder, file;             // Create SdFile objects used for accessing files on SD card

// These variables are stored in RTC's memory, so they stay alive even if Inkplate goes to deep sleep

// Remember if we completed the first boot process
// The reason for this is that the APDS needs to only be configured once, as it stays on during sleep
RTC_DATA_ATTR bool firstBootcompleted = false;

// A flag to see if a gesture woke up Inkplate which wasn't left or right
RTC_DATA_ATTR bool skip = false;

// Last image index stored in RTC RAM that stores variable even if deep sleep is used
RTC_DATA_ATTR uint16_t lastImageIndex = 0;
// Dir indexes of all the images which are in the given folder
RTC_DATA_ATTR uint16_t imageIndexes[512];
// How many images are there in total
RTC_DATA_ATTR int numFiles = 0;
// Which file in imageIndexes are we currently observing
RTC_DATA_ATTR int currentImageIndex = 0;

void setup()
{
    display.begin();             // Init Inkplate library (you should call this function ONLY ONCE)
    display.clearDisplay();      // Clear frame buffer of display
    display.setCursor(30, 30);   // Set the cursor a bit further from the corner so the text is visible
    display.setTextColor(BLACK); // Set text color to black
    display.setTextSize(5);      // Scale text to be five times bigger then original (5x7 px)

    // Enable the APDS9960 gesture sensor.
    display.wakePeripheral(INKPLATE_APDS9960);

    // Init SD card
    if (!display.sdCardInit())
    {
        // If the SD card init is not successful, display an error on the screen
        display.println("SD Card error!");
        display.display();

        // Go to deep sleep and do nothing
        deepSleep();
    }

    if (!firstBootcompleted)
    {
        // If this is the first boot...

        // Init APDS and enable the gesture sensor
        display.apds9960.init();
        display.apds9960.enableGestureSensor(true); // Setting 'true' here enables interrupts

        // Make sure gesture is at lowest sensitivity so it doesn't accidentially get triggered
        display.apds9960.setGestureGain(0);

        // Get the file count and file indexes in the directory and save them to RTC memory
        getFileCount();

        // Remember that this config was completed
        firstBootcompleted = true;
    }
    else
    {
        // This happens from the second boot onwards
        // This must mean Inkplate was woken up by an interrupt from the APDS
        // Let's read which gesture woke it up
        if (display.apds9960.isGestureAvailable())
        {
            // Read the gesture and act accordingly
            switch (display.apds9960.readGesture())
            {
            case DIR_LEFT:
                skip = false;        // Picture should be changed
                currentImageIndex++; // Go to next picture
                // reset counter if it overflowed
                if (currentImageIndex > numFiles - 1)
                    currentImageIndex = 0;
                break;
            case DIR_RIGHT:
                skip = false;        // Picture should be changed
                currentImageIndex--; // Go to previous picture
                // Loop back counter if it went below 0
                if (currentImageIndex < 0)
                    currentImageIndex = numFiles - 1;
                break;
            default:
                // If it's not a left or right gesture, do nothing
                skip = true;
            }
        }
    }
}

void loop()
{
    // If the gesture was either left or right
    if (!skip)
    {
        // Open the folder with pictures
        if (folder.open(folderPath))
        {
            // Open the according file
            if (!file.open(&folder, imageIndexes[currentImageIndex], O_READ))
            {
                // If it can't open the next file, there is an end of the file so set the index of the last file to 0
                // because it's used later for restart
                lastImageIndex = 0;
            }
            else
            {
                // The file was opened successfully!
                // Save the index of the last opened file
                lastImageIndex = file.dirIndex();

                // Skip hidden files and subdirectories
                skipHidden();

                // Get name of the picture, create path and draw image on the screen
                if (!displayImage())
                {
                    // reset the loop if there is an error displaying the image
                    return;
                }

                // Close the file
                file.close();
            }
            // Close the folder
            folder.close();

            // That's it, go to sleep!
            deepSleep();
        }
        else
        {
            display.printf(
                "Error opening folder! Make sure \nthat you have entered the proper \nname and add / to the end "
                "of the \npath");
            display.display();
            deepSleep();
        }

        // If the index is equal to 0, it is the end of the file so repeat the code in loop again
        // If not, go into a deep sleep and wait for displaying next image
        if (lastImageIndex != 0)
        {
            // Go to deep sleep
            deepSleep();
        }
    }
    else
    {
        // If a gesture was made which isn't left or right, just go to sleep
        deepSleep();
    }
}

/**
 * @brief     Activate the SD card and count the files in the folder. Close the folder and file and return the number of
 *            files
 */
void getFileCount()
{
    // Open the folder
    if (folder.open(folderPath))
    {
        // Opening the next file until it reaches the end
        while (file.openNext(&folder, O_READ))
        {
            // If the file is not hidden, increase the counter
            if (!file.isHidden())
            {
                imageIndexes[numFiles] = file.dirIndex();
                Serial.println("just added to image indexes: ");
                Serial.println(imageIndexes[numFiles]);
                numFiles++;
            }

            // Close the file
            file.close();
        }
        // Close the folder
        folder.close();

        Serial.print("just counted the files, there are : ");
        Serial.println(numFiles);
    }
    else
    {
        display.println("The folder doesn't exist");
        display.display();
        deepSleep();
    }
}

/**
 * @brief     Go to deep sleep and enable wakeup on the wake button.
 */
void deepSleep()
{
    // Turn off the power supply for the SD card
    display.sdCardSleep();

    // First, configure the interrupt from APDS to the GPIO expander
    display.expander1.pinMode(9, INPUT_PULLUP);
    display.expander1.setIntPin(9);

    // Now, the internal GPIO expander will fire an interrupt on it's INT pin
    // On pin change of pin 9

    // Now, configure the interrupt from the GPIO expander to ESP32
    // Set it as an input
    pinMode(GPIO_NUM_34, INPUT);
    // Enable wakeup from deep sleep on GPIO 34 when it goes LOW
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_34, LOW);

    // Just in case, wait until the APDS interrupt clears
    while (!display.expander1.digitalRead(9))
    {
        delay(10);
    }

    // Finally, put ESP32 into deep sleep (low power mode)
    esp_deep_sleep_start();
}

/**
 * @brief     If it's the first file, the file open at index 0 won't work so skip this for the index zero (first file
 *            because the index is declared as 0).
 */
void openLastFile()
{
    if (lastImageIndex != 0)
    {
        if (file.open(&folder, lastImageIndex, O_READ))
        {
            // Close the file so the code below can open the next one
            file.close();
        }
    }
}

/**
 * @brief     Get name of the pucture, create path and draw image on the screen.
 *
 * @return    0 if there is an error, 1 if the image is drawn.
 */
bool displayImage()
{
    // Get the name of the picture
    int maxCharacters = 100;
    char pictureName[maxCharacters];
    file.getName(pictureName, maxCharacters);

    // Copy of the folder path just for creating path for the image
    char path[maxCharacters];
    strcpy(path, folderPath);

    // Create picture path (folder path + picture name)
    char *picturePath = strcat(path, pictureName);

    // Draw the image on the screen
    if (!display.image.draw(picturePath, 0, 0, 1, 0))
    {
        // Close folder and file
        file.close();
        folder.close();

        // Return 0 to signalize an error
        return 0;
    }

    // Display the picture on the screen
    display.display();
    return 1;
}

/**
 * @brief     Skip hidden files and subdirectories.
 */
void skipHidden()
{
    while (file.isHidden() || file.isSubDir())
    {
        file.close();

        // Opening the next file while the file is hidden or the file is the directory
        if (!file.openNext(&folder, O_RDONLY))
        {
            // It is end of file
            lastImageIndex = 0;
        }
        else
        {
            // Save the index of the last opened file
            lastImageIndex = file.dirIndex();
        }
    }
}
