// sorry for messy connections  
//GND: ILI9341 GND to ESP32 GND.
//VCC: ILI9341 VCC to ESP32 VIN.
//CS: ILI9341 CS to ESP32 D15.
//RST: ILI9341 RST to ESP32 D4.
//D/C: ILI9341 D/C to ESP32 D2.
//SCK: ILI9341 SCK to ESP32 D18.
//MISO: ILI9341 MISO to ESP32 D19.
//MOSI: ILI9341 MOSI to ESP32 D23.
//GND: 74HC165 GND to ESP32 GND.
//VCC: 74HC165 VCC to ESP32 3V3.
//Q7: 74HC165 Q7 to ESP32 D16.
//CP: 74HC165 CP to ESP32 D17.
//PL: 74HC165 PL to ESP32 D22.
//!CE: 74HC165 !CE to ESP32 D5.
//Button 1: Connected to 74HC165 D1 through resistor .
//Button 2: Connected to 74HC165 D2 through resistor .
//Button 3: Connected to 74HC165 D3 through resistor .
//Button 4: Connected to 74HC165 D4 through resistor .
//Button 5: Connected to 74HC165 D5 through resistor .
//Button 6: Connected to 74HC165 D6 through resistor .
//Use 1k resistor 
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include <Arduino.h>
#include <SPI.h>
#include <vector>
#include "M16.h"
#include "Glock.h"
#include "Glock_item.h"

#define TFT_CS 15
#define TFT_RST 4
#define TFT_DC 2
#define TFT_WIDTH 320
#define TFT_HEIGHT 240
#define MAP_WIDTH 30
#define MAP_HEIGHT 30
#define TILE_SIZE 20
#define MINIMAP_SIZE 100
#define MINIMAP_X (TFT_WIDTH - MINIMAP_SIZE - 10)
#define MINIMAP_Y (TFT_HEIGHT - MINIMAP_SIZE - 10)
bool showMinimap = false;
const int dataPin = 13; // Replace with the actual pin number for SERIAL_DATA
const int clockPin = 17; // Replace with the actual pin number for SHIFT_CLK
const int latchPin = 22; // Replace with the actual pin number for STORAGE_CLK
const int numBits = 6;
#define GRADIENT_STEPS 100  // Adjust the value as needed

Adafruit_ILI9341 tft = Adafruit_ILI9341(TFT_CS, TFT_DC, TFT_RST);

const int worldMap[MAP_WIDTH][MAP_HEIGHT] PROGMEM = {
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1},
    {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
};

struct Enemy {
    double x;
    double y;
    double width;
    double height;
    bool defeated;
};

std::vector<Enemy> enemies;

double posX = 13.0, posY = 7.0, posZ = 0;
double dirX = -1.0, dirY = 0.0, planeX = 0.0, planeY = 0.66;
double speedVar = 0.50, rotSpeedVar = 0.1;

int enemyTexture[5][5] = {
  {0, 1, 1, 1, 1},
   {1, 1, 0, 1, 1},
   {1, 1, 1, 1, 1},
   {0, 1, 0, 1, 1},
   {0, 1, 0, 1, 1}
};

int deadEnemyTexture[5][5] = {
      {0, 0, 0, 0, 0},
   {0, 0, 0, 0, 0},
   {0, 0, 1, 0, 0},
   {0, 1, 1, 1, 0},
   {0, 0, 1, 0, 0}
};

void setup() {
    tft.begin();
    tft.setRotation(1);
    Serial.begin(115200);
    setCpuFrequencyMhz(240);
    pinMode(dataPin, INPUT);
    pinMode(clockPin, OUTPUT);
    pinMode(latchPin, OUTPUT);
    Serial.begin(115200);

    // Spawn enemies based on the worldMap
   for (int x = 0; x < MAP_WIDTH; x++) {
       for (int y = 0; y < MAP_HEIGHT; y++) {
           int mapValue = pgm_read_word(&worldMap[x][y]);
           if (mapValue == 2) {
               Enemy enemy = {static_cast<double>(x), static_cast<double>(y), 0.5, 0.5, false};
               enemies.push_back(enemy);
           }
       }
   }

   // Find the initial player spawn location
   bool foundSpawn = false;
   for (int y = 0; y < MAP_HEIGHT; y++) {
       for (int x = 0; x < MAP_WIDTH; x++) {
           int mapValue = pgm_read_word(&worldMap[x][y]);
           if (mapValue == 3) {
               posX = x + 0.5;
               posY = y + 0.5;
               foundSpawn = true;
               break;
           }
       }
       if (foundSpawn) break;
   }

   if (!foundSpawn) {
       // No spawn point found, set default position
       posX = 13.0;
       posY = 7.0;
   }


}
void loop() {
    handleInput();

    // Clear the areas for rendering walls, enemies, aim, and minimap
    tft.fillRect(0, 0, TFT_WIDTH, TFT_HEIGHT, ILI9341_BLACK);
    renderWalls();
    renderEnemies();
    renderAim();
    Glock();
    if (showMinimap) {
        renderMiniMap();
    }
}


uint8_t readShiftRegister() {
    uint8_t data = 0;

    // Step 1: Sample
    digitalWrite(latchPin, LOW);

    // Step 2: Shift
    for (int i = 0; i < numBits; i++) {
        int bit = digitalRead(dataPin);
        bitWrite(data, i, bit);
        digitalWrite(clockPin, HIGH); // Shift out the next bit
        digitalWrite(clockPin, LOW);
    }

    // Step 3: Update
    digitalWrite(latchPin, HIGH);

    return data;
}
double moveX = 0.0; // Initialize with a default value
double moveY = 0.0;
void handleInput() {
    uint8_t shiftRegisterData = readShiftRegister();

    // Reset rotation variables
    double rotateX = 0.0;
    double rotateY = 0.0;
    moveX = 0.0; // Reset or update moveX as needed
    moveY -= speedVar; // Up button
    moveY += speedVar; // Down button

    // Update movement variables based on button presses
    if (shiftRegisterData & 0b00000001) {
        moveY -= speedVar;  // Up button
        Serial.print("Up");
    }
    if (shiftRegisterData & 0b00000010) {
        moveY += speedVar;  // Down button
        Serial.print("Down");
    }
    if (shiftRegisterData & 0b00000100) {
        rotateX = -rotSpeedVar; // Left button
    }
    if (shiftRegisterData & 0b00001000) {
        rotateX = rotSpeedVar; // Right button
    }
    if (shiftRegisterData & 0b00010000) {
        shootEnemies(); // Fire button
    }
    if (shiftRegisterData & 0b00100000) {
        showMinimap = !showMinimap; // Toggle minimap visibility
    }

    // Update player position and rotation based on movement
    double newX = posX + dirX * moveY + planeX * moveX;
    double newY = posY + dirY * moveY + planeY * moveX;
    int mapValue = pgm_read_word(&worldMap[int(newX)][int(newY)]);
    if (mapValue == 0) {
        posX = newX;
        posY = newY;
    }

    // Update player direction based on rotation
    double oldDirX = dirX;
    dirX = dirX * cos(rotateX) - dirY * sin(rotateX);
    dirY = oldDirX * sin(rotateX) + dirY * cos(rotateX);
    double oldPlaneX = planeX;
    planeX = planeX * cos(rotateX) - planeY * sin(rotateX);
    planeY = oldPlaneX * sin(rotateX) + planeY * cos(rotateX);
}
void shootEnemies() {
    double cameraX = 2 * (TFT_WIDTH / 2) / double(TFT_WIDTH) - 1;
    double rayDirX = dirX + planeX * cameraX;
    double rayDirY = dirY + planeY * cameraX;

    int mapX = int(posX);
    int mapY = int(posY);

    double sideDistX;
    double sideDistY;

    double deltaDistX = sqrt(1 + (rayDirY * rayDirY) / (rayDirX * rayDirX));
    double deltaDistY = sqrt(1 + (rayDirX * rayDirX) / (rayDirY * rayDirY));

    int stepX, stepY;

    int hit = 0;
    int side;

    if (rayDirX < 0) {
        stepX = -1;
        sideDistX = (posX - mapX) * deltaDistX;
    } else {
        stepX = 1;
        sideDistX = (mapX + 1.0 - posX) * deltaDistX;
    }

    if (rayDirY < 0) {
        stepY = -1;
        sideDistY = (posY - mapY) * deltaDistY;
    } else {
        stepY = 1;
        sideDistY = (mapY + 1.0 - posY) * deltaDistY;
    }

    while (!hit) {
        if (sideDistX < sideDistY) {
            sideDistX += deltaDistX;
            mapX += stepX;
            side = 0;
        } else {
            sideDistY += deltaDistY;
            mapY += stepY;
            side = 1;
        }

        bool isEnemy = false;
        for (auto it = enemies.begin(); it != enemies.end(); ++it) {
            if (int(it->x) == mapX && int(it->y) == mapY) {
                // Calculate the angle between the ray direction and the vector from the player to the enemy
                double enemyAngle = angleBetween(posX, posY, it->x, it->y);
                double rayAngle = atan2(rayDirY, rayDirX);
                double angleDiff = fabs(enemyAngle - rayAngle);

                // If the angle difference is small enough, mark the enemy as defeated
                const double aimThreshold = 0.1; // Adjust this value to change the aim tolerance
                if (angleDiff < aimThreshold) {
                    it->defeated = true;
                }
                break;
            }
        }

        int mapValue = pgm_read_word(&worldMap[mapX][mapY]);
        if (mapValue > 0) {
            hit = 1;
        }
    }
}

double angleBetween(double x1, double y1, double x2, double y2) {
    double dx = x2 - x1;
    double dy = y2 - y1;
    return atan2(dy, dx);
}
void renderWalls() {
    const int WALL_HEIGHT = 200; // Set the desired height of the buildings

    for (int x = 0; x < TFT_WIDTH; ++x) {
        double cameraX = 2 * x / double(TFT_WIDTH) - 1;
        double rayDirX = dirX + planeX * cameraX;
        double rayDirY = dirY + planeY * cameraX;
        int mapX = int(posX);
        int mapY = int(posY);
        double sideDistX;
        double sideDistY;
        double deltaDistX = sqrt(1 + (rayDirY * rayDirY) / (rayDirX * rayDirX));
        double deltaDistY = sqrt(1 + (rayDirX * rayDirX) / (rayDirY * rayDirY));
        int stepX, stepY;

        if (rayDirX < 0) {
            stepX = -1;
            sideDistX = (posX - mapX) * deltaDistX;
        } else {
            stepX = 1;
            sideDistX = (mapX + 1.0 - posX) * deltaDistX;
        }

        if (rayDirY < 0) {
            stepY = -1;
            sideDistY = (posY - mapY) * deltaDistY;
        } else {
            stepY = 1;
            sideDistY = (mapY + 1.0 - posY) * deltaDistY;
        }

        bool hit = false;
        int side;
        while (!hit) {
            if (sideDistX < sideDistY) {
                sideDistX += deltaDistX;
                mapX += stepX;
                side = 0;
            } else {
                sideDistY += deltaDistY;
                mapY += stepY;
                side = 1;
            }

            bool isEnemy = false;
            for (const auto& enemy : enemies) {
                if (int(enemy.x) == mapX && int(enemy.y) == mapY) {
                    isEnemy = true;
                    break;
                }
            }

            int mapValue = pgm_read_word(&worldMap[mapX][mapY]);
            if (!isEnemy && mapValue > 0) {
                hit = true;
            }
        }

        double perpWallDist;
        if (side == 0) {
            perpWallDist = fabs((mapX - posX + (1 - stepX) / 2) / rayDirX);
        } else {
            perpWallDist = fabs((mapY - posY + (1 - stepY) / 2) / rayDirY);
        }

        int lineHeight = int(WALL_HEIGHT / perpWallDist);
        int drawStart = TFT_HEIGHT / 2 - lineHeight / 2;
        int drawEnd = drawStart + lineHeight;
        int color;
        int mapValue = pgm_read_word(&worldMap[mapX][mapY]);

        if (mapValue == 1) {
            color = ILI9341_BLUE;
            tft.fillRect(x, drawStart, 1, drawEnd - drawStart, color); // Draw a rectangular building
        } else if (mapValue == 2) {
            color = ILI9341_RED;
        } else {
            color = ILI9341_RED;
        }
    }
}
void renderEnemies() {
    for (const auto& enemy : enemies) {
        // Calculate enemy position relative to the player
        double relX = enemy.x - posX;
        double relY = enemy.y - posY;

        // Calculate the inverse of the player's transformation matrix
        double invDet = 1.0 / (planeX * dirY - dirX * planeY);
        double transformX = invDet * (dirY * relX - dirX * relY);
        double transformY = invDet * (-planeY * relX + planeX * relY);

        // Calculate screen coordinates of the enemy
        int enemyScreenX = int((TFT_WIDTH / 2) * (1 + transformX / transformY));
        int enemyScreenHeight = abs(int(TFT_HEIGHT / transformY));

        // Adjust enemy size and position based on distance from the player
        int enemyTileY = int(enemy.y);
        int enemyScreenY = TFT_HEIGHT / 2 - enemyScreenHeight / 2;
        int enemyScreenWidth = int(enemy.width * TFT_WIDTH / MAP_WIDTH);

        // Limit the maximum size of the enemy sprite
        int maxEnemySize = 50; // Adjust this value as needed
        enemyScreenWidth = min(enemyScreenWidth, maxEnemySize);
        enemyScreenHeight = min(enemyScreenHeight, maxEnemySize);

        // Render the enemy using the appropriate texture map
        int (*texture)[5];
        if (enemy.defeated) {
            for (int y = 0; y < enemyScreenHeight; y++) {
                for (int x = 0; x < enemyScreenWidth; x++) {
                    int textureX = int(x * 4.0 / enemyScreenWidth);
                    int textureY = int(y * 4.0 / enemyScreenHeight);
                    if (deadEnemyTexture[textureY][textureX] == 1) {
                        tft.drawPixel(enemyScreenX - enemyScreenWidth / 2 + x, enemyScreenY + y, ILI9341_RED);
                    }
                }
            }

            // Draw a small circle at the enemy's position
            int itemX = enemyScreenX - enemyScreenWidth / 2;
            int itemY = enemyScreenY + enemyScreenHeight / 2;
            int circleRadius = 7; // Adjust this value to change the size of the circle
            tft.drawCircle(itemX, itemY, circleRadius, ILI9341_WHITE);
        } else {
            for (int y = 0; y < enemyScreenHeight; y++) {
                for (int x = 0; x < enemyScreenWidth; x++) {
                    int textureX = int(x * 4.0 / enemyScreenWidth);
                    int textureY = int(y * 4.0 / enemyScreenHeight);
                    if (enemyTexture[textureY][textureX] == 1) {
                        tft.drawPixel(enemyScreenX - enemyScreenWidth / 2 + x, enemyScreenY + y, ILI9341_RED);
                    }
                }
            }
        }
    }
}

void renderAim() {
    int aimX = TFT_WIDTH / 2;
    int aimY = TFT_HEIGHT / 2;
    int aimRadius = 3;
    tft.fillCircle(aimX, aimY, aimRadius, ILI9341_GREEN);
}



void renderMiniMap() {
    // Clear the screen
    tft.fillRect(0, 0, TFT_WIDTH, TFT_HEIGHT, ILI9341_BLACK);

    for (int y = 0; y < MAP_HEIGHT; y++) {
        for (int x = 0; x < MAP_WIDTH; x++) {
            int mapValue = pgm_read_word(&worldMap[x][y]);
            int screenX = x * TFT_WIDTH / MAP_WIDTH;
            int screenY = y * TFT_HEIGHT / MAP_HEIGHT;
            int tileSize = min(TFT_WIDTH / MAP_WIDTH, TFT_HEIGHT / MAP_HEIGHT);

            if (mapValue == 1) {
                // Draw walls as blue rectangles
                tft.fillRect(screenX, screenY, tileSize, tileSize, ILI9341_BLUE);
            } else if (mapValue == 2) {
                // Draw enemies as red rectangles
                tft.fillRect(screenX, screenY, tileSize, tileSize, ILI9341_RED);
            }
        }
    }

    // Draw the player's position and orientation
    int playerX = int(posX * TFT_WIDTH / MAP_WIDTH);
    int playerY = int(posY * TFT_HEIGHT / MAP_HEIGHT);
    tft.fillCircle(playerX, playerY, 2, ILI9341_GREEN);

    double angleRad = atan2(dirY, dirX);
    int lineX = playerX + 5 * cos(angleRad);
    int lineY = playerY + 5 * sin(angleRad);
    tft.drawLine(playerX, playerY, lineX, lineY, ILI9341_GREEN);
}
void fadeOutEffect() {
  static uint8_t fadeStep = 0;

  if (fadeStep <= GRADIENT_STEPS) {
    uint8_t alpha = map(fadeStep, 0, GRADIENT_STEPS, 255, 0);  // Map step to alpha value

    // Calculate semi-transparent color (RGB with alpha)
    uint16_t semiTransparentColor = tft.color565(0, 0, 0);  // Black color
    semiTransparentColor |= ((alpha / 8) << 11);  // Adjust alpha level

    tft.fillScreen(semiTransparentColor);  // Fill screen with semi-transparent black

    delay(10);  // Adjust delay for desired fade speed

    fadeStep++;
  } else {
    // Reset variables or set flags as needed when fade out completes
    fadeStep = 0;
    // Additional actions after fade out completes
  }
}
void Glock(){
  tft.drawBitmap(10,180,Glock_item,50,50,ILI9341_WHITE);
}
74HC165