#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include <algorithm>  // Add this for std::min

// Define GPIO pin
#define GPIO_PIN 13

// Initialize U8G2 display - rotation will be set in setup
U8G2_SH1106_128X64_NONAME_F_HW_I2C display(U8G2_R1);  // Default rotation



// Animation parameters
const uint8_t SAND_PARTICLES = 25;
const uint8_t ANIMATION_DELAY = 50;
const unsigned long HOURGLASS_DURATION = 60000; // 1 minute
const uint8_t NUM_FALLING_PARTICLES = 8;
const uint8_t PARTICLE_SPEED_MIN = 1;
const uint8_t PARTICLE_SPEED_MAX = 2;

// Hourglass dimensions
const uint8_t GLASS_WIDTH = 50;
const uint8_t GLASS_HEIGHT = 100;
const uint8_t GLASS_X = (64 - GLASS_WIDTH) / 2;
const uint8_t GLASS_Y = 14;
const uint8_t WALL_THICKNESS = 2;
const uint8_t TOP_THICKNESS = 5;
const uint8_t BASE_PROTRUSION = 2;
const uint8_t NECK_WIDTH = 2;
const uint8_t NECK_TOTAL = NECK_WIDTH + (WALL_THICKNESS * 2);
const uint8_t CURVE_STEPS = 15;
const uint8_t TOP_FILL_PERCENT = 60;
const uint8_t BOTTOM_FILL_PERCENT = 50;
const uint8_t DOME_MAX_HEIGHT = 15;  // Maximum height of the initial dome
const uint8_t SPREAD_THRESHOLD = 8;  // Height at which sand starts to spread more
const float DOME_CURVE_FACTOR = 0.7; // Controls dome roundness (0.5-1.0)

uint32_t topPixelCount = 0;    // Using uint32_t for larger numbers
uint32_t bottomPixelCount = 0;  // Using uint32_t for larger numbers


int calculateDomeHeight(int distanceFromCenter, int maxHeight) {
    float normalizedDist = (float)distanceFromCenter / (GLASS_WIDTH / 2);
    return maxHeight * (1 - pow(normalizedDist, DOME_CURVE_FACTOR));
}

// Structures for particles
struct Particle {
    int8_t x;
    int8_t y;
    int8_t velocity;
    bool active;
};

struct FallingParticle {
    int8_t x;
    int8_t y;
    int8_t speed;
    bool active;
};

// Global variables
Particle particles[SAND_PARTICLES];
FallingParticle fallingParticles[NUM_FALLING_PARTICLES];
unsigned long startTime;
bool isRunning = true;
uint8_t topFillPercent = TOP_FILL_PERCENT;
uint8_t bottomFillPercent = 0;
int16_t leftBoundary[GLASS_HEIGHT];
int16_t rightBoundary[GLASS_HEIGHT];

// Function declarations
void calculateBoundaries();
void initializeFallingParticles();
void bezierPoint(float t, int x0, int y0, int x1, int y1, int x2, int y2, float &outX, float &outY);
void drawTopBase(bool isTop);
void drawHourglass();
void updateFallingParticles();
void drawFallingParticles();
void updateSandLevels();
void drawTopSand();
void drawBottomSand();
void drawSand();

void checkGPIOAndRotation() {
    static bool lastPinState = HIGH;
    bool currentPinState = digitalRead(GPIO_PIN);
    
    if (currentPinState != lastPinState) {
        // Pin state changed
        display.setDisplayRotation(currentPinState ? U8G2_R3 : U8G2_R1);
        
        // Restart animation
        startTime = millis();
        topFillPercent = TOP_FILL_PERCENT;
        bottomFillPercent = 0;
        initializeFallingParticles();
        
        lastPinState = currentPinState;
    }
}

// Bezier curve calculation function
void bezierPoint(float t, int x0, int y0, int x1, int y1, int x2, int y2, float &outX, float &outY) {
    float mt = 1 - t;
    outX = mt * mt * x0 + 2 * mt * t * x1 + t * t * x2;
    outY = mt * mt * y0 + 2 * mt * t * y1 + t * t * y2;
}

// Calculate the boundaries of the hourglass
void calculateBoundaries() {
    // ... (No changes in this function)
    int middleY = GLASS_Y + GLASS_HEIGHT / 2;
    for (int y = 0; y < GLASS_HEIGHT; y++) {
        float t;
        float xL, yL, xR, yR;

        if (y < GLASS_HEIGHT / 2) { // Top half
            t = (float)(y) / (GLASS_HEIGHT / 2);
            bezierPoint(t,
                        GLASS_X + WALL_THICKNESS - BASE_PROTRUSION, GLASS_Y,
                        GLASS_X + WALL_THICKNESS, GLASS_Y + GLASS_HEIGHT / 3,
                        GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + WALL_THICKNESS, middleY,
                        xL, yL);
            bezierPoint(t,
                        GLASS_X + GLASS_WIDTH - WALL_THICKNESS + BASE_PROTRUSION, GLASS_Y,
                        GLASS_X + GLASS_WIDTH - WALL_THICKNESS, GLASS_Y + GLASS_HEIGHT / 3,
                        GLASS_X + GLASS_WIDTH - (GLASS_WIDTH - NECK_TOTAL) / 2 - WALL_THICKNESS, middleY,
                        xR, yR);
        } else { // Bottom half
            t = (float)(y - GLASS_HEIGHT / 2) / (GLASS_HEIGHT / 2);
            bezierPoint(t,
                        GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + WALL_THICKNESS, middleY,
                        GLASS_X + WALL_THICKNESS, GLASS_Y + GLASS_HEIGHT - GLASS_HEIGHT / 3,
                        GLASS_X + WALL_THICKNESS - BASE_PROTRUSION, GLASS_Y + GLASS_HEIGHT,
                        xL, yL);
            bezierPoint(t,
                        GLASS_X + GLASS_WIDTH - (GLASS_WIDTH - NECK_TOTAL) / 2 - WALL_THICKNESS, middleY,
                        GLASS_X + GLASS_WIDTH - WALL_THICKNESS, GLASS_Y + GLASS_HEIGHT - GLASS_HEIGHT / 3,
                        GLASS_X + GLASS_WIDTH - WALL_THICKNESS + BASE_PROTRUSION, GLASS_Y + GLASS_HEIGHT,
                        xR, yR);
        }

        leftBoundary[y] = round(xL) + 1;
        rightBoundary[y] = round(xR) - 1;
    }
}

// Draw top or bottom base of the hourglass
void drawTopBase(bool isTop) {
    // ... (No changes in this function)
    int yPos = isTop ? GLASS_Y : GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS;
    int xExtension = 6;  // Amount to extend beyond glass width on EACH side

    // Original glass edges
    int glassStartX = GLASS_X;
    int glassEndX = GLASS_X + GLASS_WIDTH;

    // Base edges (extending beyond glass)
    int baseStartX = glassStartX - xExtension;
    int baseEndX = glassEndX + xExtension;

    // Draw main rectangle without corners
    for (int x = baseStartX + 2; x <= baseEndX - 2; x++) {
        display.drawPixel(x, yPos);    // Top line
        display.drawPixel(x, yPos + TOP_THICKNESS - 1);    // Bottom line
    }

    // Draw vertical sides without top and bottom pixels
    for (int y = yPos + 1; y < yPos + TOP_THICKNESS - 1; y++) {
        display.drawPixel(baseStartX, y);    // Left side
        display.drawPixel(baseEndX, y);      // Right side
    }

    // Draw rounded corners
    // Top-left corner
    display.drawPixel(baseStartX + 1, yPos);
    display.drawPixel(baseStartX + 1, yPos + 1);
    display.drawPixel(baseStartX, yPos + 1);

    // Top-right corner
    display.drawPixel(baseEndX - 1, yPos);
    display.drawPixel(baseEndX - 1, yPos + 1);
    display.drawPixel(baseEndX, yPos + 1);

    // Bottom-left corner
    display.drawPixel(baseStartX + 1, yPos + TOP_THICKNESS - 1);
    display.drawPixel(baseStartX + 1, yPos + TOP_THICKNESS - 2);
    display.drawPixel(baseStartX, yPos + TOP_THICKNESS - 2);

    // Bottom-right corner
    display.drawPixel(baseEndX - 1, yPos + TOP_THICKNESS - 1);
    display.drawPixel(baseEndX - 1, yPos + TOP_THICKNESS - 2);
    display.drawPixel(baseEndX, yPos + TOP_THICKNESS - 2);

    // Fill the base
    for (int x = baseStartX + 1; x < baseEndX; x++) {
        for (int y = yPos + 1; y < yPos + TOP_THICKNESS - 1; y++) {
            // display.drawPixel(x, y);
        }
    }
}
// Initialize falling particles
void initializeFallingParticles() {
    for (uint8_t i = 0; i < NUM_FALLING_PARTICLES; i++) {
        fallingParticles[i].active = false;
        fallingParticles[i].x = 0;
        fallingParticles[i].y = 0;
        fallingParticles[i].speed = 0;
    }
}

void updateFallingParticles() {
    int middleY = GLASS_Y + GLASS_HEIGHT / 2;
    int neckLeft = GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + WALL_THICKNESS + 1;
    int neckWidth = NECK_WIDTH - 2;
    int bottomLimit = GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS - (bottomFillPercent * GLASS_HEIGHT / 200);

    // Activate new particles
    for (uint8_t i = 0; i < NUM_FALLING_PARTICLES; i++) {
        if (!fallingParticles[i].active && random(100) < 30 && topFillPercent > 0) {
            fallingParticles[i].active = true;
            fallingParticles[i].x = neckLeft + random(neckWidth);
            fallingParticles[i].y = middleY;
            fallingParticles[i].speed = random(PARTICLE_SPEED_MIN, PARTICLE_SPEED_MAX + 1);
        }
    }

    // Update active particles
    for (uint8_t i = 0; i < NUM_FALLING_PARTICLES; i++) {
        if (fallingParticles[i].active) {
            fallingParticles[i].y += fallingParticles[i].speed;

            // Reduced horizontal movement chance
            if (random(100) < 15) { // Reduced to 15%
                fallingParticles[i].x += random(-1, 2);
                // Keep within boundaries
                int currentY = fallingParticles[i].y - GLASS_Y;
                if (currentY >= 0 && currentY < GLASS_HEIGHT) {
                    fallingParticles[i].x = constrain(fallingParticles[i].x,
                                                      leftBoundary[currentY],
                                                      rightBoundary[currentY]);
                }
            }

            // Deactivate if reached bottom fill level
            if (fallingParticles[i].y >= bottomLimit) {
                fallingParticles[i].active = false;
            }
        }
    }
}

// Draw the falling particles
void drawFallingParticles() {
    for (uint8_t i = 0; i < NUM_FALLING_PARTICLES; i++) {
        if (fallingParticles[i].active) {
            display.drawPixel(fallingParticles[i].x, fallingParticles[i].y);
        }
    }
}

// Update sand levels based on time
void updateSandLevels() {
    unsigned long elapsedTime = millis() - startTime;
    float progress = (float)elapsedTime / HOURGLASS_DURATION;
    
    // Enhanced non-linear function for more realistic hourglass behavior
    float topProgressFactor;
    if (progress <= 1.0) {
        // This formula creates three distinct phases:
        // 1. Slow initial drop (wide part)
        // 2. Accelerating middle section (curved part)
        // 3. Fast final drop (neck part)
        float x = progress;
        // Cubic function with adjustable parameters
        topProgressFactor = 0.3 * pow(x, 3) + 0.7 * x;
        
        // Add small random variations for more natural look
        float randomFactor = 1.0 + (random(-10, 11) / 1000.0); // ±1% variation
        topProgressFactor *= randomFactor;
    } else {
        topProgressFactor = 1.0;
    }
    
    // Calculate new fill percentages
    topFillPercent = TOP_FILL_PERCENT * (1.0 - topProgressFactor);
    
    // Bottom chamber fills proportionally to top chamber's emptying
    bottomFillPercent = BOTTOM_FILL_PERCENT * topProgressFactor;
    
    // Constrain values
    topFillPercent = constrain(topFillPercent, 0, TOP_FILL_PERCENT);
    bottomFillPercent = constrain(bottomFillPercent, 0, BOTTOM_FILL_PERCENT);
}

// Draw the sand in both chambers
// Function to draw sand in top chamber
void drawTopSand() {
    int middleY = GLASS_Y + GLASS_HEIGHT / 2;
    
    if (topFillPercent > 0) {
        int topHeight = (GLASS_HEIGHT / 2 - TOP_THICKNESS) * topFillPercent / 100;
        int sandTop = middleY - topHeight;
        
        for (int y = middleY - 1; y >= sandTop; y--) {
            if (y >= GLASS_Y + TOP_THICKNESS) {
                int leftX = leftBoundary[y - GLASS_Y];
                int rightX = rightBoundary[y - GLASS_Y];

                if (y == sandTop) {
                    // Slightly uneven surface at the top
                    for (int x = leftX; x <= rightX; x++) {
                        if (random(100) < 90) {
                            display.drawPixel(x, y);
                            topPixelCount++;
                        }
                    }
                } else {
                    // Fill complete rows
                    for (int x = leftX; x <= rightX; x++) {
                        display.drawPixel(x, y);
                        topPixelCount++;
                    }
                }
            }
        }
    }
}

// Function to draw sand in bottom chamber
void drawBottomSand() {
    int middleY = GLASS_Y + GLASS_HEIGHT / 2;
    
    if (bottomFillPercent > 0) {
        int sandBottom = GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS;
        int maxFillHeight = (GLASS_HEIGHT / 2 - TOP_THICKNESS) * bottomFillPercent / 100;
        int centerX = GLASS_X + GLASS_WIDTH / 2;
        
        // Calculate current dome height based on fill percentage
        int currentDomeHeight = std::min(maxFillHeight, (int)DOME_MAX_HEIGHT);
        int spreadHeight = maxFillHeight - currentDomeHeight;
        
        // Draw the main sand body (if any)
        if (spreadHeight > 0) {
            int flatSandTop = sandBottom - spreadHeight;
            
            // Draw the flat accumulated sand
            for (int y = sandBottom - 1; y >= flatSandTop; y--) {
                if (y >= middleY) {
                    int leftX = leftBoundary[y - GLASS_Y];
                    int rightX = rightBoundary[y - GLASS_Y];
                    
                    for (int x = leftX; x <= rightX; x++) {
                        display.drawPixel(x, y);
                        bottomPixelCount++;
                    }
                }
            }
            
            // Adjust sandBottom for dome drawing
            sandBottom = flatSandTop;
        }
        
        // Draw the dome shape with smoother top
        for (int y = sandBottom; y >= sandBottom - currentDomeHeight; y--) {
            if (y >= middleY) {
                int leftX = leftBoundary[y - GLASS_Y];
                int rightX = rightBoundary[y - GLASS_Y];
                
                for (int x = leftX; x <= rightX; x++) {
                    int distFromCenter = abs(x - centerX);
                    int domeHeightAtDist = calculateDomeHeight(distFromCenter, currentDomeHeight);
                    
                    if (sandBottom - y <= domeHeightAtDist) {
                        // Only add randomness at the very top edge of the dome
                        if (sandBottom - y == domeHeightAtDist) {
                            // Increased randomness at the dome's edge
                            if (random(100) < 70) { // 70% chance to skip pixel at the edge
                                continue;
                            }
                        }
                        display.drawPixel(x, y);
                        bottomPixelCount++;
                    }
                }
            }
        }
    }
}
        
        // Add some randomness to the top surface
   /*     int topSurfaceY = sandBottom - currentDomeHeight;
        if (topSurfaceY >= middleY) {
            int leftX = leftBoundary[topSurfaceY - GLASS_Y];
            int rightX = rightBoundary[topSurfaceY - GLASS_Y];
            
            for (int x = leftX; x <= rightX; x++) {
                if (random(100) < 20) {
                    display.drawPixel(x, topSurfaceY - 1);
                    bottomPixelCount++;
                }
            }
        }
    }
}
*/

// Main draw sand function that calls both chambers
void drawSand() {
    topPixelCount = 0;
    bottomPixelCount = 0;
    
    drawTopSand();
    drawBottomSand();
}

// Draw the hourglass frame - THIS WAS LIKELY MISSING OR INCOMPLETE
void drawHourglass() {
    int middleY = GLASS_Y + GLASS_HEIGHT / 2;

    // Draw the filled walls
    for (int y = GLASS_Y + TOP_THICKNESS; y < GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS; y++) {
        float t;
        float xL1, yL1, xR1, yR1;    // Inner curve points
        float xL2, yL2, xR2, yR2;    // Outer curve points

        if (y < middleY) { // Top half
            t = (float)(y - (GLASS_Y + TOP_THICKNESS)) / (GLASS_HEIGHT / 2 - TOP_THICKNESS);
            // Inner curves
            bezierPoint(t, GLASS_X + WALL_THICKNESS - 1, GLASS_Y + TOP_THICKNESS, GLASS_X + WALL_THICKNESS - 1, GLASS_Y + GLASS_HEIGHT / 3, GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + WALL_THICKNESS - 1, middleY, xL1, yL1);
            bezierPoint(t, GLASS_X + GLASS_WIDTH - WALL_THICKNESS + 1, GLASS_Y + TOP_THICKNESS, GLASS_X + GLASS_WIDTH - WALL_THICKNESS + 1, GLASS_Y + GLASS_HEIGHT / 3, GLASS_X + GLASS_WIDTH - (GLASS_WIDTH - NECK_TOTAL) / 2 - WALL_THICKNESS + 1, middleY, xR1, yR1);
            // Outer curves
            bezierPoint(t, GLASS_X - BASE_PROTRUSION + 1, GLASS_Y + TOP_THICKNESS, GLASS_X + 1, GLASS_Y + GLASS_HEIGHT / 3, GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + 1, middleY, xL2, yL2);
            bezierPoint(t, GLASS_X + GLASS_WIDTH + BASE_PROTRUSION - 1, GLASS_Y + TOP_THICKNESS, GLASS_X + GLASS_WIDTH - 1, GLASS_Y + GLASS_HEIGHT / 3, GLASS_X + GLASS_WIDTH - (GLASS_WIDTH - NECK_TOTAL) / 2 - 1, middleY, xR2, yR2);
        } else { // Bottom half
            t = (float)(y - middleY) / (GLASS_HEIGHT / 2 - TOP_THICKNESS);
            // Inner curves
            bezierPoint(t, GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + WALL_THICKNESS - 1, middleY, GLASS_X + WALL_THICKNESS - 1, GLASS_Y + GLASS_HEIGHT - GLASS_HEIGHT / 3, GLASS_X + WALL_THICKNESS - 1, GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS, xL1, yL1);
            bezierPoint(t, GLASS_X + GLASS_WIDTH - (GLASS_WIDTH - NECK_TOTAL) / 2 - WALL_THICKNESS + 1, middleY, GLASS_X + GLASS_WIDTH - WALL_THICKNESS + 1, GLASS_Y + GLASS_HEIGHT - GLASS_HEIGHT / 3, GLASS_X + GLASS_WIDTH - WALL_THICKNESS + 1, GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS, xR1, yR1);
            // Outer curves
            bezierPoint(t, GLASS_X + (GLASS_WIDTH - NECK_TOTAL) / 2 + 1, middleY, GLASS_X + 1, GLASS_Y + GLASS_HEIGHT - GLASS_HEIGHT / 3, GLASS_X - BASE_PROTRUSION + 1, GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS, xL2, yL2);
            bezierPoint(t, GLASS_X + GLASS_WIDTH - (GLASS_WIDTH - NECK_TOTAL) / 2 - 1, middleY, GLASS_X + GLASS_WIDTH - 1, GLASS_Y + GLASS_HEIGHT - GLASS_HEIGHT / 3, GLASS_X + GLASS_WIDTH + BASE_PROTRUSION - 1, GLASS_Y + GLASS_HEIGHT - TOP_THICKNESS, xR2, yR2);
        }

        // Draw the walls
        int xL2i = round(xL2);
        int xR2i = round(xR2);

        display.drawPixel(xL2i, y);      // Left wall outer
        display.drawPixel(xL2i + 1, y);   // Left wall inner
        display.drawPixel(xR2i, y);      // Right wall outer
        display.drawPixel(xR2i - 1, y);   // Right wall inner
    }

    // Draw top and bottom bases
    drawTopBase(true);
    drawTopBase(false);
}

void setup() {
    // Initialize GPIO13 as output and set it HIGH
    pinMode(GPIO_PIN, OUTPUT);
    digitalWrite(GPIO_PIN, HIGH);
    
    // Initialize display with rotation based on GPIO state
    if (digitalRead(GPIO_PIN)) {
        display.setDisplayRotation(U8G2_R3);
    } else {
        display.setDisplayRotation(U8G2_R1);
    }
    
    // Initialize display
    display.begin();
    display.setFont(u8g2_font_6x10_tf);

    // Calculate boundaries for the hourglass shape
    calculateBoundaries();

    // Initialize particles
    initializeFallingParticles();

    // Set start time
    startTime = millis();

    // Initialize random seed
    randomSeed(os_random());
}

void loop() {
    // Check GPIO state and handle rotation if needed
    checkGPIOAndRotation();

    // Calculate progress
    unsigned long elapsedTime = millis() - startTime;
    int progress = map(elapsedTime, 0, HOURGLASS_DURATION, 0, 100);
    progress = constrain(progress, 0, 100);

    // Begin drawing
    display.clearBuffer();

    // Draw progress percentage
    char progressStr[5];
    sprintf(progressStr, "%d%%", progress);
    display.drawStr(23, 8, progressStr);
    display.drawStr(1,127,"Sand Clock");

    // Draw all hourglass elements
    drawHourglass();
    updateSandLevels();
    drawSand();
    updateFallingParticles();
    drawFallingParticles();

    // Send the buffer to the display
    display.sendBuffer();

    // Check if time's up
    if (elapsedTime >= HOURGLASS_DURATION) {
        // Instead of showing "Time's Up", just keep showing the final state
        startTime = millis() - HOURGLASS_DURATION;  // This keeps the progress at 100%
    }

    delay(ANIMATION_DELAY);
}