#include "raylib.h"
#include "rcamera.h"
#include "raymath.h"
#include "stdbool.h"
#include "stddef.h"
#include "stdlib.h"
#include "stdint.h"
#include "rlgl.h"
#include "float.h"
#include "raygui.h"

//----------------------------------------------------------------------------------
// Defines and Macros
//----------------------------------------------------------------------------------
// Movement constants
#define GRAVITY         32.0f
#define MAX_SPEED       20.0f
#define CROUCH_SPEED     5.0f
#define JUMP_FORCE      12.0f
#define MAX_ACCEL      150.0f

// Grounded drag
#define FRICTION         0.86f

// Increasing air drag, increases strafing speed
#define AIR_DRAG         0.98f

// Responsiveness for turning movement direction to looked direction
#define CONTROL         15.0f
#define CROUCH_HEIGHT    0.0f
#define STAND_HEIGHT     2.0f
#define BOTTOM_HEIGHT    0.5f

// Additional definitions
#define NORMALIZE_INPUT  0
#define ARRLEN(a) (sizeof(a) / sizeof((a)[0]))

#define FLOOR_EXTENT 25
#define TILE_SIZE 5.0f

#define PLAYER_RADIUS 0.5f
#define PLAYER_HEIGHT 1.8f

enum game_state {
    STATE_MENU,    // 0 by default
    STATE_GAME,    // 1
};

//----------------------------------------------------------------------------------
//Defining program structure
//----------------------------------------------------------------------------------
typedef struct
{
    Camera camera;   // <- now part of the game state
    Model  model;    // you can keep model & its position here too
    Vector3 position; 
    Vector3 velocity;
    Vector3 dir;
    Vector2 sensitivity;
    Vector2 lookRotation;
    Vector2 lean;
    float   headTimer;
    float   walkLerp;
    float   headLerp;
    bool isGrounded;
    int cameraMode;
} Player;

typedef struct 
{
    int index;
    enum game_state state;
    int shouldExit;
    Player player;
    int nearAcorn;
    int isJumpscared;
    float jumpscareTimer;

    Sound jumpScare;
    Music bgm;
} Master;

//------------------------------------------------------------------------------------
// Function Initation
//------------------------------------------------------------------------------------
void Simple_Center_Box(double percent_verticle_gap, double percent_horizontal_gap, int screenWidth, int screenHeight, Color color);
void Thick_Center_Box(double percent_verticle_gap, double percent_horizontal_gap, float line_width, int screenWidth, int screenHeight, Color color);
void Simple_Menu(const char *array[], int count, int index, int x1, int y1, int total_width, int total_height, int line_thickess, int font_size, Color color, Color alt_color);
void UpdateBody(Master* g, float rot, char side, char forward, bool jumpPressed, bool crouchHold);
static void UpdateCameraAngle(Master* g);
static void DrawLevel(Model mdlTile);
static void CenterMouseAndFlush(void);
int Random(int x); 

float square(float num);

//------------------------------------------------------------------------------------
// Program main entry point
//------------------------------------------------------------------------------------
int main(void)
{
    // Initialization
    //--------------------------------------------------------------------------------------

    // Before InitWindow
    SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_RESIZABLE | FLAG_MSAA_4X_HINT | FLAG_FULLSCREEN_MODE);

    InitWindow(0, 0, "raylib");

    int screenWidth  = GetRenderWidth();
    int screenHeight = GetRenderHeight();

    DisableCursor();    
    CenterMouseAndFlush();

    SetTargetFPS(60);               // Set our game to run at 60 frames-per-second

    //Define menu characteristics
    const char *menu[] = {"Game", "Option 2", "Exit"};
    int index_length = ARRLEN(menu);

    //General game state initiation
    Master g = (Master){0};
    g.index = 0;
    g.state = STATE_MENU;
    g.shouldExit = 0;
    g.nearAcorn = 0;
    g.isJumpscared = 0;

    //Define player values
    g.player.sensitivity = (Vector2){ 0.001f, 0.001f };
    g.player.lookRotation = (Vector2){ 0 };
    g.player.headTimer = 0.0f;
    g.player.walkLerp = 0.0f;
    g.player.headLerp = STAND_HEIGHT;
    g.player.lean = (Vector2){ 0 };

    //Define the player camera
    g.player.camera = (Camera){0};
    g.player.camera.position = (Vector3){
        g.player.position.x,
        g.player.position.y + (BOTTOM_HEIGHT + g.player.headLerp),
        g.player.position.z,
    };
    g.player.camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    g.player.camera.fovy = 60.0f;
    g.player.camera.projection = CAMERA_PERSPECTIVE;
    g.player.cameraMode = CAMERA_FIRST_PERSON;

    g.player.position.x = 15.0;
    g.player.position.z = 15.0;

    UpdateCameraAngle(&g);

    // Grass tile setup
    Texture2D texGrass = LoadTexture("assets/textures/Grass_01.png");
    GenTextureMipmaps(&texGrass);
    SetTextureFilter(texGrass, TEXTURE_FILTER_TRILINEAR);    // nicer at angles
    SetTextureWrap(texGrass, TEXTURE_WRAP_REPEAT);

    // A 3D plane (centered at origin) of size tileSize x tileSize
    const float tileSize = TILE_SIZE;
    Mesh meshTile = GenMeshPlane(tileSize, tileSize, 1, 1);
    Model mdlTile = LoadModelFromMesh(meshTile);
    mdlTile.materials[0].maps[MATERIAL_MAP_ALBEDO].texture = texGrass;

    
        // --- Skybox setup using 6 face images ---
    Mesh cube = GenMeshCube(1.0f, 1.0f, 1.0f);
    Model skybox = LoadModelFromMesh(cube);

    // Load skybox shader
    Shader skyboxShader = LoadShader(
        "assets/shaders/skybox.vs",
        "assets/shaders/skybox.fs"
    );
    skybox.materials[0].shader = skyboxShader;

    // Load the 6 faces (+X, -X, +Y, -Y, +Z, -Z)
    const char* facePaths[6] = {
        "assets/textures/cubemaps/right.png",   // +X
        "assets/textures/cubemaps/left.png",    // -X
        "assets/textures/cubemaps/top.png",     // +Y
        "assets/textures/cubemaps/bottom.png",  // -Y
        "assets/textures/cubemaps/front.png",   // +Z
        "assets/textures/cubemaps/back.png"     // -Z
    };

    Image faces[6];
    for (int i = 0; i < 6; i++) {
        faces[i] = LoadImage(facePaths[i]);
        // Ensure consistent format
        if (faces[i].format != PIXELFORMAT_UNCOMPRESSED_R8G8B8A8)
            ImageFormat(&faces[i], PIXELFORMAT_UNCOMPRESSED_R8G8B8A8);
    }

    // Verify same size
    int fw = faces[0].width, fh = faces[0].height;
    for (int i = 1; i < 6; i++) {
        if (faces[i].width != fw || faces[i].height != fh) {
            TraceLog(LOG_ERROR, "Cubemap face sizes differ");
            // (optional) handle error/cleanup here
        }
    }

    // Stitch into a vertical strip (CUBEMAP_LAYOUT_LINE_VERTICAL)
    Image strip = GenImageColor(fw, fh*6, BLANK);
    for (int i = 0; i < 6; i++) {
        ImageDraw(&strip, faces[i],
                (Rectangle){0,0,(float)fw,(float)fh},
                (Rectangle){0,(float)(i*fh),(float)fw,(float)fh},
                WHITE);
        UnloadImage(faces[i]); // free each face once copied
    }

    // IMPORTANT: skip CPU mipmap generation to avoid the buggy path
    strip.mipmaps = 1;

    // Load cubemap from the vertical strip
    TextureCubemap cubeTex = LoadTextureCubemap(strip, CUBEMAP_LAYOUT_LINE_VERTICAL);
    UnloadImage(strip);

    // Assign to material & use a non-mipmap filter (skyboxes don't need mips)
    skybox.materials[0].maps[MATERIAL_MAP_CUBEMAP].texture = cubeTex;
    SetTextureFilter(cubeTex, TEXTURE_FILTER_BILINEAR);

    // Tell the shader which material map is the environmentMap
    int mapType = MATERIAL_MAP_CUBEMAP;
    SetShaderValue(skyboxShader, GetShaderLocation(skyboxShader, "environmentMap"),
                &mapType, SHADER_UNIFORM_INT);

    // Optional flags (keep as you had)
    int vflipped = 0, doGamma = 0;
    SetShaderValue(skyboxShader, GetShaderLocation(skyboxShader, "vflipped"),
                &vflipped, SHADER_UNIFORM_INT);
    SetShaderValue(skyboxShader, GetShaderLocation(skyboxShader, "doGamma"),
                &doGamma,  SHADER_UNIFORM_INT);

    //--------------------------------------------------------------------------------------

    // Main game loop
    //--------------------------------------------------------------------------------------
    while (!g.shouldExit && !WindowShouldClose())    // Detect window close button or ESC key
    {
        // Update
        //----------------------------------------------------------------------------------
        switch (g.state) 
        {
            case STATE_MENU:
            {
                if (IsKeyPressed(KEY_UP) && g.index > 0) 
                {
                    g.index -= 1;
                    break;
                };

                if (IsKeyPressed(KEY_DOWN) && g.index < index_length - 1) 
                {
                    g.index += 1;
                    break;
                };
                
                if (IsKeyPressed(KEY_ENTER))
                {
                    switch (g.index) 
                    {
                        case 0:
                        {
                            g.state = 1;
                            CenterMouseAndFlush();
                            break;
                        }

                        case 1:
                        {
                            break;
                        }

                        case 2:
                        {
                            g.shouldExit = 1;
                            break;
                        }
                     }
                };
                break;
            };
            case STATE_GAME:
            {
                Vector2 mouseDelta = GetMouseDelta();
                g.player.lookRotation.x -= mouseDelta.x*g.player.sensitivity.x;
                g.player.lookRotation.y += mouseDelta.y*g.player.sensitivity.y;

                char sideway = (IsKeyDown(KEY_D) - IsKeyDown(KEY_A));
                char forward = (IsKeyDown(KEY_W) - IsKeyDown(KEY_S));
                bool crouching = IsKeyDown(KEY_LEFT_CONTROL);
                UpdateBody(&g, g.player.lookRotation.x, sideway, forward, IsKeyDown(KEY_SPACE), crouching);

                float delta = GetFrameTime();
                g.player.headLerp = Lerp(g.player.headLerp, (crouching ? CROUCH_HEIGHT : STAND_HEIGHT), 20.0f*delta);
                g.player.camera.position = (Vector3){
                    g.player.position.x,
                    g.player.position.y + (BOTTOM_HEIGHT + g.player.headLerp),
                    g.player.position.z,
                };

                if (g.player.isGrounded && ((forward != 0) || (sideway != 0)))
                {
                    g.player.headTimer += delta*3.0f;
                    g.player.walkLerp = Lerp(g.player.walkLerp, 1.0f, 10.0f*delta);
                    g.player.camera.fovy = Lerp(g.player.camera.fovy, 55.0f, 5.0f*delta);
                }
                else
                {
                    g.player.walkLerp = Lerp(g.player.walkLerp, 0.0f, 10.0f*delta);
                    g.player.camera.fovy = Lerp(g.player.camera.fovy, 60.0f, 5.0f*delta);
                }

                g.player.lean.x = Lerp(g.player.lean.x, sideway*0.02f, 10.0f*delta);
                g.player.lean.y = Lerp(g.player.lean.y, forward*0.015f, 10.0f*delta);

                UpdateCameraAngle(&g);
                break; 
            };
        }
        //----------------------------------------------------------------------------------

        // Draw
        //----------------------------------------------------------------------------------
        BeginDrawing();
            switch (g.state) 
            {
                case STATE_MENU:
                {
                    ClearBackground(BLACK);                        

                    Simple_Menu(menu, ARRLEN(menu), g.index, screenWidth * 0.15, screenHeight * 0.15, 500, screenHeight * 0.7, 10, 50, GREEN, LIGHTGRAY);

                    Thick_Center_Box(0.1, 0.1, 10, screenWidth, screenHeight, GREEN);
                    break;
                }
                case STATE_GAME:
                {
                    ClearBackground(BLACK);
                    
                    Vector3 camPos = g.player.camera.position;
                    Vector3 camFwd = Vector3Normalize(Vector3Subtract(g.player.camera.target, camPos));

                    // Camera forward (normalize target - position)
                    Vector3 forward = Vector3Normalize(Vector3Subtract(g.player.camera.target, g.player.camera.position));

                    BeginMode3D(g.player.camera);

                        rlDisableBackfaceCulling();
                        rlDisableDepthMask();

                        // keep it centered on the camera
                        skybox.transform = MatrixTranslate(
                            g.player.camera.position.x,
                            g.player.camera.position.y,
                            g.player.camera.position.z
                        );

                        DrawModel(skybox, (Vector3){0}, 1.0f, WHITE);

                        rlEnableDepthMask();
                        rlEnableBackfaceCulling();  

                        DrawLevel(mdlTile);

                    EndMode3D();   

                    // Draw info box
                    DrawRectangle(5, 5, 330*1.5, 75*1.25, Fade(SKYBLUE, 0.5f));
                    DrawRectangleLines(5, 5, 330*1.5, 75*1.25, BLUE);

                    DrawText("Camera controls:", 15, 15, 20, BLACK);
                    DrawText("- Move keys: W, A, S, D, Space, Left-Ctrl", 15, 30, 20, BLACK);
                    DrawText("- Look around: arrow keys or mouse", 15, 45, 20, BLACK);
                    DrawText(TextFormat("- Velocity Len: (%06.3f)", Vector2Length((Vector2){ g.player.velocity.x, g.player.velocity.z })), 15, 60, 20, BLACK);

                    break;
                }
            }
        EndDrawing();
        //----------------------------------------------------------------------------------
    };

    //--------------------------------------------------------------------------------------
    // De-Initialization
    //--------------------------------------------------------------------------------------
    // Detach the texture from the model so UnloadModel won't try to free it.
    mdlTile.materials[0].maps[MATERIAL_MAP_ALBEDO].texture = (Texture2D){0};

    // Now free resources you loaded:
    UnloadModel(mdlTile);      // frees the model and its mesh/VBOs
    UnloadTexture(texGrass);   // frees the grass texture you loaded
    
    CloseWindow();        // Close window and OpenGL context
    //--------------------------------------------------------------------------------------
    return 0;
};

//------------------------------------------------------------------------------------
// Function Definitions
//------------------------------------------------------------------------------------
//Function creates a simple box outline centered in the screen with a symetric ofset from the screen borders.
void Simple_Center_Box(double percent_verticle_gap, double percent_horizontal_gap, int screenWidth, int screenHeight, Color color)
{
    int x1 = percent_horizontal_gap * screenWidth;
    int y1 = percent_verticle_gap * screenHeight;

    int w = screenWidth * (1 - (2 * percent_horizontal_gap));
    int h = screenHeight * (1 - (2 * percent_verticle_gap));
    
    DrawRectangleLines(x1, y1, w, h, color);
}

//Function creates a thick walled box outline centered in the screen with a symetric ofset from the screen borders.
void Thick_Center_Box(double percent_verticle_gap, double percent_horizontal_gap, float line_width, int screenWidth, int screenHeight, Color color)
{
    int x1 = percent_horizontal_gap * screenWidth;
    int y1 = percent_verticle_gap * screenHeight;

    int w = screenWidth * (1 - (2 * percent_horizontal_gap));
    int h = screenHeight * (1 - (2 * percent_verticle_gap));

    struct Rectangle rec = {x1,y1,w,h};
    
    DrawRectangleLinesEx(rec, line_width, color);
}

//Make a simple menu with selection features
void Simple_Menu(const char *array[], int count, int index, int x1, int y1, int total_width, int total_height, int line_thickess, int font_size, Color color, Color alt_color)
{
    float height = 0.8f * total_height;
    int space = 0.2 * total_height / (count - 1);

    struct Rectangle rec = {x1,y1,total_width,(float)height/count};

    for (int i = 0; i < count; ++i) 
    {
        int text_width = MeasureText(array[i], font_size);

        if(i == index)
        {
            DrawRectangleLinesEx(rec, line_thickess, alt_color);
            DrawText(array[i], rec.x + (((total_width - 2.0 * line_thickess) - text_width)/2), rec.y + (height/count/2) - font_size/2.0f, font_size, alt_color);
        }
        else 
        {
            DrawRectangleLinesEx(rec, line_thickess, color);
            DrawText(array[i], rec.x + (((total_width - 2.0 * line_thickess) - text_width)/2), rec.y + (height/count/2) - font_size/2.0f, font_size, color);
        }

        rec.y = rec.y + (float)height/count + space;
    }
}

void UpdateBody(Master* g, float rot, char side, char forward, bool jumpPressed, bool crouchHold)
{
    Vector2 input = (Vector2){ (float)side, (float)-forward };

#if defined(NORMALIZE_INPUT)
    // Slow down diagonal movement
    if ((side != 0) && (forward != 0)) input = Vector2Normalize(input);
#endif

    float delta = GetFrameTime();

    if (!g->player.isGrounded) g->player.velocity.y -= GRAVITY*delta;

    if (g->player.isGrounded && jumpPressed)
    {
        g->player.velocity.y = JUMP_FORCE;
        g->player.isGrounded = false;

        // Sound can be played at this moment
        //SetSoundPitch(fxJump, 1.0f + (GetRandomValue(-100, 100)*0.001));
        //PlaySound(fxJump);
    }

    Vector3 front = (Vector3){ sin(rot), 0.f, cos(rot) };
    Vector3 right = (Vector3){ cos(-rot), 0.f, sin(-rot) };

    Vector3 desiredDir = (Vector3){ input.x*right.x + input.y*front.x, 0.0f, input.x*right.z + input.y*front.z, };
    g->player.dir = Vector3Lerp(g->player.dir, desiredDir, CONTROL*delta);

    float decel = (g->player.isGrounded ? FRICTION : AIR_DRAG);
    Vector3 hvel = (Vector3){ g->player.velocity.x*decel, 0.0f, g->player.velocity.z*decel };

    float hvelLength = Vector3Length(hvel); // Magnitude
    if (hvelLength < (MAX_SPEED*0.01f)) hvel = (Vector3){ 0 };

    // This is what creates strafing
    float speed = Vector3DotProduct(hvel, g->player.dir);

    // Whenever the amount of acceleration to add is clamped by the maximum acceleration constant,
    // a Player can make the speed faster by bringing the direction closer to horizontal velocity angle
    // More info here: https://youtu.be/v3zT3Z5apaM?t=165
    float maxSpeed = (crouchHold? CROUCH_SPEED : MAX_SPEED);
    float accel = Clamp(maxSpeed - speed, 0.f, MAX_ACCEL*delta);
    hvel.x += g->player.dir.x*accel;
    hvel.z += g->player.dir.z*accel;

    g->player.velocity.x = hvel.x;
    g->player.velocity.z = hvel.z;

    g->player.position.x += g->player.velocity.x*delta;
    g->player.position.y += g->player.velocity.y*delta;
    g->player.position.z += g->player.velocity.z*delta;

    // Fancy collision system against the floor
    if (g->player.position.y <= 0.0f)
    {
        g->player.position.y = 0.0f;
        g->player.velocity.y = 0.0f;
        g->player.isGrounded = true; // Enable jumping
    }
}

// Update camera
static void UpdateCameraAngle(Master* g)
{
    const Vector3 up = (Vector3){ 0.0f, 1.0f, 0.0f };
    const Vector3 targetOffset = (Vector3){ 0.0f, 0.0f, -1.0f };

    // Left and right
    Vector3 yaw = Vector3RotateByAxisAngle(targetOffset, up, g->player.lookRotation.x);

    // Clamp view up
    float maxAngleUp = Vector3Angle(up, yaw);
    maxAngleUp -= 0.001f; // Avoid numerical errors
    if ( -(g->player.lookRotation.y) > maxAngleUp) { g->player.lookRotation.y = -maxAngleUp; }

    // Clamp view down
    float maxAngleDown = Vector3Angle(Vector3Negate(up), yaw);
    maxAngleDown *= -1.0f; // Downwards angle is negative
    maxAngleDown += 0.001f; // Avoid numerical errors
    if ( -(g->player.lookRotation.y) < maxAngleDown) { g->player.lookRotation.y = -maxAngleDown; }

    // Up and down
    Vector3 right = Vector3Normalize(Vector3CrossProduct(yaw, up));

    // Rotate view vector around right axis
    float pitchAngle = -g->player.lookRotation.y - 
    g->player.lean.y;
    pitchAngle = Clamp(pitchAngle, -PI / 2 + 0.0001f, PI / 2 - 0.0001f); // Clamp angle so it doesn't go past straight up or straight down
    Vector3 pitch = Vector3RotateByAxisAngle(yaw, right, pitchAngle);

    // Head animation
    // Rotate up direction around forward axis
    float headSin = sin(g->player.headTimer*PI);
    float headCos = cos(g->player.headTimer*PI);
    const float stepRotation = 0.005f;
    g->player.camera.up = Vector3RotateByAxisAngle(up, pitch, headSin*stepRotation + g->player.lean.x);

    // Camera BOB
    const float bobSide = 0.01f;
    const float bobUp = 0.01f;
    Vector3 bobbing = Vector3Scale(right, headSin*bobSide);
    bobbing.y = fabsf(headCos*bobUp);

    g->player.camera.position = Vector3Add(g->player.camera.position, Vector3Scale(bobbing, g->player.walkLerp));
    g->player.camera.target = Vector3Add(g->player.camera.position, pitch);
}

// Draw game level
static void DrawLevel(Model mdlTile)
{
    const int floorExtent = FLOOR_EXTENT;
    const float tileSize = TILE_SIZE;
    const Color tileColor1 = (Color){ 150, 200, 200, 255 };

    // Floor tiles
    for (int y = -floorExtent; y < floorExtent; y++)
    {
        for (int x = -floorExtent; x < floorExtent; x++)
        {
            /*
            if ((y & 1) && (x & 1))
            {
                DrawPlane((Vector3){ x*tileSize, 0.0f, y*tileSize}, (Vector2){ tileSize, tileSize }, tileColor1);
            }
            else if (!(y & 1) && !(x & 1))
            {
                DrawPlane((Vector3){ x*tileSize, 0.0f, y*tileSize}, (Vector2){ tileSize, tileSize }, LIGHTGRAY);
            }
            */
            Vector3 pos = (Vector3){ x*tileSize, 0.0f, y*tileSize };
            // mdlTile must be accessible here: make it global/static or pass it in
            DrawModel(mdlTile, pos, 1.0f, WHITE);

        }
    }

    /*
    const Vector3 towerSize = (Vector3){ 16.0f, 32.0f, 16.0f };
    const Color towerColor = (Color){ 150, 200, 200, 255 };

    Vector3 towerPos = (Vector3){ 16.0f, 16.0f, 16.0f };
    DrawCubeV(towerPos, towerSize, towerColor);
    DrawCubeWiresV(towerPos, towerSize, DARKBLUE);

    towerPos.x *= -1;
    DrawCubeV(towerPos, towerSize, towerColor);
    DrawCubeWiresV(towerPos, towerSize, DARKBLUE);

    towerPos.z *= -1;
    DrawCubeV(towerPos, towerSize, towerColor);
    DrawCubeWiresV(towerPos, towerSize, DARKBLUE);

    towerPos.x *= -1;
    DrawCubeV(towerPos, towerSize, towerColor);
    DrawCubeWiresV(towerPos, towerSize, DARKBLUE);

    // Red sun
    DrawSphere((Vector3){ 300.0f, 300.0f, 0.0f }, 100.0f, (Color){ 255, 0, 0, 255 });
    */
}

//fix mouse
static void CenterMouseAndFlush() 
{
    SetMousePosition(GetRenderWidth()/2, GetRenderHeight()/2);
    (void)GetMouseDelta();
}

//Random number gen
int Random(int x) 
{
    int r = (rand() % x) + 1;  // 1-40
    return r;
}

float square(float num)
{
    float out = num * num;
    return out;
}