#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"

//----------------------------------------------------------------------------------
// 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
};

//----------------------------------------------------------------------------------
// RNG
//----------------------------------------------------------------------------------
// One-time world seed (set in main)
static uint32_t gWorldSeed = 123456789u;

// Fast 2D int hash → stable pseudo-random per cell
static inline uint32_t hash2i(int x, int z, uint32_t seed)
{
    uint32_t h = (uint32_t)x * 374761393u ^ (uint32_t)z * 668265263u ^ seed * 2654435761u;
    h ^= h >> 13; h *= 1274126177u; h ^= h >> 16;
    return h;
}

// 1-in-N chance using a hashed value
static inline bool one_in_n(uint32_t h, int n)
{
    return (h % (uint32_t)n) == 0u;
}

// Map a few hash bits to [-0.5, +0.5] for jitter
static inline float hash01_to_m05p05(uint32_t h_bits)
{
    return ((h_bits & 0xFFu) / 255.0f) - 0.5f;
}

//----------------------------------------------------------------------------------
//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;
} 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); 
static void RenderTrees(Master* g, Texture2D tree, Vector3 treepos, float treeSize);
static void ApplyFogShaderToModel(Model *m, Shader fog);

static BoundingBox MakeModelWorldAABB(Model m, Vector3 pos, Vector3 scale, float yawDeg);
static BoundingBox PlayerAABB(Vector3 p);
static void ResolveAgainstBoxXZ(Master* g, BoundingBox box);

static float ModelLengthXUnits(Model m);
static float ModelLengthZUnits(Model m);
static void  DrawPerimeter(Model wall, float border, float scale);
static void  CollidePerimeter(Master* g, Model wall, float border, float scale); // optional

//------------------------------------------------------------------------------------
// Program main entry point
//------------------------------------------------------------------------------------
int main(void)
{
    // Initialization
    //--------------------------------------------------------------------------------------
    //Define window size, create window, establish fps target, fullscreen.
    const int screenWidth = 1440*2;
    const int screenHeight = 960*2;

    // BEFORE InitWindow (optional but nice): enable MSAA for smoother alpha edges
    SetConfigFlags(FLAG_MSAA_4X_HINT);
    
    InitWindow(screenWidth, screenHeight, "raylib");
    SetTargetFPS(60);               // Set our game to run at 60 frames-per-second
    ToggleFullscreen();
    CenterMouseAndFlush();
    DisableCursor();                    // Limit cursor to relative movement inside the window

    //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;

    //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;

    UpdateCameraAngle(&g);

    
    // Load GLTF model (no animations)
    //Model wall = LoadModel("resources/3d_models/Tiny_Treats_Homely_House_1.0_FREE/Assets/gltf/fence_straight.gltf");
    Model wall = LoadModel("resources/3d_models/RuinsGLB/Walls/WallNormalXL.glb");
    Vector3 wall_position = { 10.0f, 0.0f, 0.0f };
    float   wall_yaw = 0.0f;     // rotate around Y
    float   wall_pitch = 0.0f;   // rotate around X
    float   wall_roll = 0.0f;    // rotate around Z

    Model acorn = LoadModel("resources/3d_models/my_models/my_acorn.glb");
    Vector3 acorn_position = { 0.0f, 0.0f, 0.0f };
    float   acorn_yaw = 0.0f;     // rotate around Y
    float   acorn_pitch = 0.0f;   // rotate around X
    float   acorn_roll = 0.0f;    // rotate around Z

    // Load a 2D image as texture for the billboard
    //Billboards always point toward the active camera.
    Texture2D sign = LoadTexture("resources/sprites/husk/ZOMBC1.png");
    //Texture2D sign = LoadTexture("resources/sprites/diard-charles.jpg");
    GenTextureMipmaps(&sign);
    SetTextureFilter(sign, TEXTURE_FILTER_POINT); // optional but looks nicer
    Vector3 bbPos = (Vector3){ -1.0f, 2.0f, -1.0f };  // where in 3D world to place it
    float   bbSize = 4.0f;                          // billboard height in world units
    
    // Load a 2D image as texture for the billboard
    //Billboards always point toward the active camera.
    Texture2D tree = LoadTexture("resources/sprites/tree.png");
    GenTextureMipmaps(&tree);
    SetTextureFilter(tree, TEXTURE_FILTER_BILINEAR); // optional but looks nicer
    Vector3 treepos = (Vector3){ 0.0f, 0.0f, 0.0f };        // where in 3D world to place it
    float   treeSize = 15.0f;                                        // billboard height in world units
    float cutoffAlpha = 0.7f;  //Needs to be aggressive to remove the alpha.

    // Grass tile setup
    Texture2D texGrass = LoadTexture("resources/textures/grass_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 ---
    // Generate cube mesh
    Mesh cube = GenMeshCube(1.0f, 1.0f, 1.0f);
    Model skybox = LoadModelFromMesh(cube);

    // Load skybox shader
    Shader skyboxShader = LoadShader
    (
        "shaders/skybox.vs",
        "shaders/skybox.fs"
    );

    // Attach shader to skybox
    skybox.materials[0].shader = skyboxShader;
    Image img = LoadImage("resources/cubemap/cubemap.png");
    skybox.materials[0].maps[MATERIAL_MAP_CUBEMAP].texture = LoadTextureCubemap(img, CUBEMAP_LAYOUT_AUTO_DETECT);
    UnloadImage(img); //no longer need image loaded.

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

    //Flip/gamma options (set to 0 for PNG cross)
    int vflipped = 0;
    int doGamma  = 0;
    SetShaderValue(skyboxShader, GetShaderLocation(skyboxShader, "vflipped"), &vflipped, SHADER_UNIFORM_INT);
    SetShaderValue(skyboxShader, GetShaderLocation(skyboxShader, "doGamma"),  &doGamma,  SHADER_UNIFORM_INT);

    // Load the fog shader
    Shader fog = LoadShader("shaders/fog.vs", "shaders/fog.fs");

    // Grab uniform locations (cache them)
    int locPlayer    = GetShaderLocation(fog, "u_playerPos");
    int locRadius    = GetShaderLocation(fog, "u_fogRadius");
    int locFeather   = GetShaderLocation(fog, "u_fogFeather");
    int locFogColor  = GetShaderLocation(fog, "u_fogColor");
    int locCutoff_fog    = GetShaderLocation(fog, "u_alphaCutoff");

    int locInner     = GetShaderLocation(fog, "u_inner");
    int locOuter     = GetShaderLocation(fog, "u_outer");
    int locMinShadow = GetShaderLocation(fog, "u_minShadow");
    // Optional if you added the sharpness curve in the shader
    int locSharpness = GetShaderLocation(fog, "u_sharpness");

    //Flashlight
    int locSpotDir     = GetShaderLocation(fog, "u_spotDir");
    int locSpotCos     = GetShaderLocation(fog, "u_spotCos");
    int locSpotSoft    = GetShaderLocation(fog, "u_spotSoft");
    int locLightRange  = GetShaderLocation(fog, "u_lightRange");
    int locLightFade   = GetShaderLocation(fog, "u_lightFade");
    int locIntensity  = GetShaderLocation(fog, "u_intensity");
    int locBeamSpread = GetShaderLocation(fog, "u_beamSpread");

    // Let raylib know which matrices the shader expects
    fog.locs[SHADER_LOC_MATRIX_MODEL]      = GetShaderLocation(fog, "matModel");
    fog.locs[SHADER_LOC_MATRIX_VIEW]       = GetShaderLocation(fog, "matView");
    fog.locs[SHADER_LOC_MATRIX_PROJECTION] = GetShaderLocation(fog, "matProjection");

    // --- Apply shader to your ground tile model ---
    mdlTile.materials[0].shader = fog;   // same shader also works for opaque

    //Apply shader to wall
    ApplyFogShaderToModel(&wall, fog);
    ApplyFogShaderToModel(&acorn, fog);

    // Defaults for opaque geometry (no alpha cut)
    float cutoffOpaque = 0.0f;
    SetShaderValue(fog, locCutoff_fog, &cutoffOpaque, SHADER_UNIFORM_FLOAT);

    // Fog/vignette tuning
    float inner      = 0.0f;    // start radius (center under player)
    float outer      = 20.0f;   // how far until full fog
    float minShadow  = 0.85f;   // baseline darkness at the center (0 = none, 1 = pitch black)
    float sharpness  = 2.0f;    // steeper edge (optional if shader uses it)
    Vector4 fogColor = {0.0f, 0.0f, 0.0f, 1.0f};

    // Flashligh tuning
    float spotAngleDeg = 35.0f; //cone angle
    float spotCos      = cosf(DEG2RAD * (spotAngleDeg * 0.5f));
    float spotSoft     = 0.10f;
    float lightRange   = 150.0f;//100.0f;
    float lightFade    = 1.0f;
    float intensity  = 0.25f;  // try 0.7–1.5 to taste
    float beamSpread = 0.05f; // try 0.15–0.35; higher = less “laser” at distance

    SetShaderValue(fog, locIntensity,  &intensity,  SHADER_UNIFORM_FLOAT);
    SetShaderValue(fog, locBeamSpread, &beamSpread, SHADER_UNIFORM_FLOAT);
    SetShaderValue(fog, locSpotCos,    &spotCos,    SHADER_UNIFORM_FLOAT);
    SetShaderValue(fog, locSpotSoft,   &spotSoft,   SHADER_UNIFORM_FLOAT);
    SetShaderValue(fog, locLightRange, &lightRange, SHADER_UNIFORM_FLOAT);
    SetShaderValue(fog, locLightFade,  &lightFade,  SHADER_UNIFORM_FLOAT);

    SetShaderValue(fog, locInner,     &inner,             SHADER_UNIFORM_FLOAT);
    SetShaderValue(fog, locOuter,     &outer,             SHADER_UNIFORM_FLOAT);
    SetShaderValue(fog, locMinShadow, &minShadow,         SHADER_UNIFORM_FLOAT);
    SetShaderValue(fog, locFogColor,  &fogColor,          SHADER_UNIFORM_VEC4);
    SetShaderValue(fog, locSharpness, &sharpness,         SHADER_UNIFORM_FLOAT);
    //--------------------------------------------------------------------------------------

    // 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);

                // Build wall collider (scale must match your DrawModelEx scale)
                BoundingBox acornBox = MakeModelWorldAABB(acorn, acorn_position, (Vector3){0.25,0.25,0.25}, acorn_yaw);

                // Collide player vs wall (XZ slide)
                ResolveAgainstBoxXZ(&g, acornBox);


                // put near your update logic
                float B = 122;
                float T = 1.0f;                       // wall half-thickness
                float H = 4.0f;                       // wall height

                BoundingBox walls[4] = {
                    // left  (x = -B)
                    { (Vector3){-B - T, 0.0f, -B - T}, (Vector3){-B + T, H,  B + T} },
                    // right (x = +B)
                    { (Vector3){ B - T, 0.0f, -B - T}, (Vector3){ B + T, H,  B + T} },
                    // bottom (z = -B)
                    { (Vector3){-B - T, 0.0f, -B - T}, (Vector3){ B + T, H, -B + T} },
                    // top    (z = +B)
                    { (Vector3){-B - T, 0.0f,  B - T}, (Vector3){ B + T, H,  B + T} },
                };

                for (int i = 0; i < 4; ++i) ResolveAgainstBoxXZ(&g, walls[i]);

                // (Optional) debug draw inside BeginMode3D/EndMode3D:
                for (int i = 0; i < 4; ++i) DrawBoundingBox(walls[i], RED);


                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);



                bbPos.x += (g.player.position.x - bbPos.x)*.005;
                bbPos.z += (g.player.position.z - bbPos.z)*.005;




                // wall editor
                    if (IsKeyDown(KEY_RIGHT) && IsKeyDown(KEY_LEFT_SHIFT)) acorn_position.x += 0.5f;
                    else if (IsKeyPressed(KEY_RIGHT)) acorn_position.x += 0.5f;

                    if (IsKeyDown(KEY_LEFT) && IsKeyDown(KEY_LEFT_SHIFT)) acorn_position.x -= 0.5f;
                    else if (IsKeyPressed(KEY_LEFT)) acorn_position.x -= 0.5f;

                    if (IsKeyDown(KEY_UP) && IsKeyDown(KEY_LEFT_SHIFT)) acorn_position.z += 0.5f;
                    else if (IsKeyPressed(KEY_UP)) acorn_position.z += 0.5f;

                    if (IsKeyDown(KEY_DOWN) && IsKeyDown(KEY_LEFT_SHIFT)) acorn_position.z -= 0.5f;
                    else if (IsKeyPressed(KEY_DOWN)) acorn_position.z -= 0.5f;



                    float rotSpeed = 45.0f * GetFrameTime();  // deg/sec
                    if (IsKeyDown(KEY_Q)) wall_yaw  -= rotSpeed;
                    if (IsKeyDown(KEY_E)) wall_yaw  += rotSpeed;
                //

                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));
                    
                    SetShaderValue(fog, locPlayer,    &camPos, SHADER_UNIFORM_VEC3);
                    SetShaderValue(fog, locSpotDir,  &camFwd,     SHADER_UNIFORM_VEC3);

                    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);
                        
                        // --- Trees use same fog shader but with alpha-cut enabled ---
                        
                        SetShaderValue(fog, locCutoff_fog, &cutoffAlpha, SHADER_UNIFORM_FLOAT);

                        BeginShaderMode(fog);

                            //DrawModel(wall, wall_position, 5.0, WHITE);
                            DrawModelEx(acorn, acorn_position, (Vector3){0,1,0}, wall_yaw, (Vector3){0.25,0.25,0.25}, WHITE);
                            RenderTrees(&g, tree, treepos, treeSize);
                            //DrawBillboard(g.player.camera, sign, bbPos, bbSize, WHITE); //zombie

                            DrawPerimeter(wall, 122.0f, 1.5f);

                        EndShaderMode();

                        // Restore opaque cutoff for anything else you draw with that material later
                        SetShaderValue(fog, locCutoff_fog, &cutoffOpaque, SHADER_UNIFORM_FLOAT);

                    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);
                    DrawText(
                        TextFormat("x: (%06.3f), y: (%06.3f), z: (%06.3f)", wall_position.x, wall_position.y, wall_position.z),
                        15, 75, 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
    UnloadTexture(tree);       // frees tree texture
    UnloadTexture(sign);
    UnloadModel(wall);
    UnloadModel(acorn);
    
    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;
}

static inline void DrawTreeBillboardStable(Camera cam, Texture2D tex, Vector3 groundPos, float heightWorld, Color tint)
{
    // Keep sprite aspect ratio
    float aspect = (float)tex.width / (float)tex.height;
    Vector2 size   = (Vector2){ heightWorld * aspect, heightWorld };

    // Bottom-center pivot so base sits on groundPos.y
    Vector2 origin = (Vector2){ 0.5f, 0.75f };

    // Fixed world up (ignore camera.up roll)
    Vector3 up     = (Vector3){ 0, 1, 0 };

    Rectangle src  = (Rectangle){ 0, 0, (float)tex.width, (float)tex.height };
    DrawBillboardPro(cam, tex, src, groundPos, up, size, origin, 0.0f, tint);
}

static void RenderTrees(Master* g, Texture2D tree, Vector3 baseTreePos, float treeSize)
{
    const int   floorExtent = FLOOR_EXTENT - 3;
    const float tileSize    = TILE_SIZE;

    // Placement knobs
    const int   densityA = 10;                 // 1-in-10 on (odd,odd)
    const int   densityB = 13;                 // 1-in-14 on (even,even)
    const float jitter    = tileSize * 0.35f;  // random in-tile offset

    const float groundY   = baseTreePos.y;     // <-- use this as terrain height

    for (int z = -floorExtent; z < floorExtent; ++z)
    for (int x = -floorExtent; x < floorExtent; ++x)
    {
        uint32_t h  = hash2i(x, z, gWorldSeed);
        uint32_t hA = h;
        uint32_t hB = h * 0x9E3779B1u + 0x85EBCA6Bu;

        // (odd,odd)
        if ((z & 1) && (x & 1) && one_in_n(hA, densityA)) {
            float ox = hash01_to_m05p05(hA)      * jitter;
            float oz = hash01_to_m05p05(hA >> 8) * jitter;

            Vector3 pos = { x*tileSize + ox, groundY, z*tileSize + oz };
            DrawTreeBillboardStable(g->player.camera, tree, pos, /*height*/ treeSize, WHITE);
        }

        // (even,even)
        if (!(z & 1) && !(x & 1) && one_in_n(hB, densityB)) {
            float ox = hash01_to_m05p05(hB)      * jitter;
            float oz = hash01_to_m05p05(hB >> 8) * jitter;

            Vector3 pos = { x*tileSize + ox, groundY, z*tileSize + oz };
            DrawTreeBillboardStable(g->player.camera, tree, pos, /*height*/ treeSize, WHITE);
        }
    }
}

static void ApplyFogShaderToModel(Model *m, Shader fog)
{
    // 1) Tell raylib which uniform in your shader is the ALBEDO map
    fog.locs[SHADER_LOC_MATRIX_MODEL]      = GetShaderLocation(fog, "matModel");
    fog.locs[SHADER_LOC_MATRIX_VIEW]       = GetShaderLocation(fog, "matView");
    fog.locs[SHADER_LOC_MATRIX_PROJECTION] = GetShaderLocation(fog, "matProjection");
    fog.locs[SHADER_LOC_MAP_ALBEDO]        = GetShaderLocation(fog, "texture0"); // your FS: uniform sampler2D texture0;

    // (optional but nice)
    fog.locs[SHADER_LOC_COLOR_DIFFUSE]     = GetShaderLocation(fog, "colDiffuse");

    // 2) For each material, make sure the base color texture ends up in the ALBEDO slot
    for (int i = 0; i < m->materialCount; ++i) {
        Material *mat = &m->materials[i];

        // Some importers put the base map in DIFFUSE, copy to ALBEDO if ALBEDO is empty.
        if (mat->maps[MATERIAL_MAP_ALBEDO].texture.id == 0 &&
            mat->maps[MATERIAL_MAP_DIFFUSE].texture.id != 0) {
            mat->maps[MATERIAL_MAP_ALBEDO].texture = mat->maps[MATERIAL_MAP_DIFFUSE].texture;
        }

        // Ensure non-zero tint
        if (mat->maps[MATERIAL_MAP_ALBEDO].color.a == 0) mat->maps[MATERIAL_MAP_ALBEDO].color = WHITE;

        // 3) Attach your fog shader to this material
        mat->shader = fog;
    }
}

// Build player's AABB from its position (XZ radius, fixed height)
static BoundingBox PlayerAABB(Vector3 p)
{
    BoundingBox bb;
    bb.min = (Vector3){ p.x - PLAYER_RADIUS, p.y,               p.z - PLAYER_RADIUS };
    bb.max = (Vector3){ p.x + PLAYER_RADIUS, p.y + PLAYER_HEIGHT, p.z + PLAYER_RADIUS };
    return bb;
}

// Combine all mesh AABBs of a Model in LOCAL space
static BoundingBox ModelLocalAABB(Model m)
{
    BoundingBox out;
    out.min = (Vector3){  FLT_MAX,  FLT_MAX,  FLT_MAX };
    out.max = (Vector3){ -FLT_MAX, -FLT_MAX, -FLT_MAX };

    for (int i = 0; i < m.meshCount; ++i) {
        BoundingBox b = GetMeshBoundingBox(m.meshes[i]); // local-space box of this mesh

        if (b.min.x < out.min.x) out.min.x = b.min.x;
        if (b.min.y < out.min.y) out.min.y = b.min.y;
        if (b.min.z < out.min.z) out.min.z = b.min.z;

        if (b.max.x > out.max.x) out.max.x = b.max.x;
        if (b.max.y > out.max.y) out.max.y = b.max.y;
        if (b.max.z > out.max.z) out.max.z = b.max.z;
    }
    return out;
}

// Transform a BoundingBox by a matrix and return its world AABB
static BoundingBox TransformAABB(BoundingBox bb, Matrix M)
{
    Vector3 c[8] = {
        {bb.min.x, bb.min.y, bb.min.z}, {bb.max.x, bb.min.y, bb.min.z},
        {bb.min.x, bb.max.y, bb.min.z}, {bb.max.x, bb.max.y, bb.min.z},
        {bb.min.x, bb.min.y, bb.max.z}, {bb.max.x, bb.min.y, bb.max.z},
        {bb.min.x, bb.max.y, bb.max.z}, {bb.max.x, bb.max.y, bb.max.z},
    };

    BoundingBox out;
    out.min = (Vector3){  FLT_MAX,  FLT_MAX,  FLT_MAX };
    out.max = (Vector3){ -FLT_MAX, -FLT_MAX, -FLT_MAX };

    for (int i = 0; i < 8; ++i) {
        Vector3 t = Vector3Transform(c[i], M);
        if (t.x < out.min.x) out.min.x = t.x;
        if (t.y < out.min.y) out.min.y = t.y;
        if (t.z < out.min.z) out.min.z = t.z;

        if (t.x > out.max.x) out.max.x = t.x;
        if (t.y > out.max.y) out.max.y = t.y;
        if (t.z > out.max.z) out.max.z = t.z;
    }
    return out;
}

// Build world AABB for a model using the same T*R*S you render with
static BoundingBox MakeModelWorldAABB(Model m, Vector3 pos, Vector3 scale, float yawDeg)
{
    Matrix S = MatrixScale(scale.x, scale.y, scale.z);
    Matrix R = MatrixRotateY(DEG2RAD * yawDeg);
    Matrix T = MatrixTranslate(pos.x, pos.y, pos.z);

    Matrix SRT = MatrixMultiply(MatrixMultiply(S, R), T); // S * R * T
    Matrix M   = MatrixMultiply(m.transform, SRT);        // model.transform * SRT

    BoundingBox out = { .min = { FLT_MAX,  FLT_MAX,  FLT_MAX },
                        .max = { -FLT_MAX, -FLT_MAX, -FLT_MAX } };

    for (int i = 0; i < m.meshCount; ++i) {
        BoundingBox local = GetMeshBoundingBox(m.meshes[i]);
        BoundingBox world = TransformAABB(local, M);
        if (world.min.x < out.min.x) out.min.x = world.min.x;
        if (world.min.y < out.min.y) out.min.y = world.min.y;
        if (world.min.z < out.min.z) out.min.z = world.min.z;
        if (world.max.x > out.max.x) out.max.x = world.max.x;
        if (world.max.y > out.max.y) out.max.y = world.max.y;
        if (world.max.z > out.max.z) out.max.z = world.max.z;
    }
    return out;
}

// Push player out of a box on the smallest X/Z overlap (slide) and zero that velocity axis
static void ResolveAgainstBoxXZ(Master* g, BoundingBox box)
{
    BoundingBox p = PlayerAABB(g->player.position);

    // Quick reject
    if (!CheckCollisionBoxes(p, box)) return;

    // Overlaps on X and Z
    float ox = fminf(p.max.x, box.max.x) - fmaxf(p.min.x, box.min.x);
    float oz = fminf(p.max.z, box.max.z) - fmaxf(p.min.z, box.min.z);
    if (ox <= 0 || oz <= 0) return;

    // Centers to know which way to push
    float cxp = 0.5f * (p.min.x + p.max.x);
    float czp = 0.5f * (p.min.z + p.max.z);
    float cxb = 0.5f * (box.min.x + box.max.x);
    float czb = 0.5f * (box.min.z + box.max.z);

    // Resolve along the smallest axis
    if (ox < oz) {
        float push = (cxp < cxb) ? -ox : ox;
        g->player.position.x += push;
        g->player.velocity.x = 0.0f;
    } else {
        float push = (czp < czb) ? -oz : oz;
        g->player.position.z += push;
        g->player.velocity.z = 0.0f;
    }
}

// Length of the model along local X (used to tile segments cleanly)
static float ModelLengthXUnits(Model m) {
    BoundingBox bb = ModelLocalAABB(m);           // you already have this helper
    return (bb.max.x - bb.min.x);
}

static float ModelLengthZUnits(Model m) {
    BoundingBox bb = ModelLocalAABB(m);
    return (bb.max.z - bb.min.z);
}

// Draw repeated wall segments around the perimeter (z=±border, x=±border)
static void DrawPerimeter(Model wall, float border, float scale) {
    const float HALF = FLOOR_EXTENT * TILE_SIZE;   // e.g. 25*5 = 125
    Vector3 scl = { scale, scale, scale };

    //float segLen = ModelLengthXUnits(wall) * scale;
    float segLen = ModelLengthZUnits(wall) * scale;
    if (segLen <= 0.001f) segLen = TILE_SIZE;      // safety fallback

    int count = (int)ceilf((2.0f * HALF) / segLen) + 1;

    scl.x += 0.1;
    scl.y += 0.1;
    scl.z += 0.1;

    // Top & bottom edges: fence runs along +X (yaw 0/180)
    for (int i = 0; i < count; ++i) {
        float x = -HALF + i * segLen;
        DrawModelEx(wall, (Vector3){ x, 0.0f,  border }, (Vector3){0,1,0},   90.0f, scl, WHITE);
        DrawModelEx(wall, (Vector3){ x, 0.0f, -border }, (Vector3){0,1,0}, 270.0f, scl, WHITE);
    }

    // Left & right edges: rotate 90° so fence runs along Z
    for (int i = 0; i < count; ++i) {
        float z = -HALF + i * segLen;
        DrawModelEx(wall, (Vector3){  border, 0.0f, z }, (Vector3){0,1,0},  0.0f, scl, WHITE);
        DrawModelEx(wall, (Vector3){ -border, 0.0f, z }, (Vector3){0,1,0},  180.0f, scl, WHITE);
    }
}


// Optional: collide player with the same perimeter using your AABB helpers
static void CollidePerimeter(Master* g, Model wall, float border, float scale) {
    const float HALF = FLOOR_EXTENT * TILE_SIZE;
    const Vector3 scl = { scale, scale, scale };

    float segLen = ModelLengthXUnits(wall) * scale;
    if (segLen <= 0.001f) segLen = TILE_SIZE;

    int count = (int)ceilf((2.0f * HALF) / segLen) + 1;

    for (int i = 0; i < count; ++i) {
        float x = -HALF + i * segLen;
        ResolveAgainstBoxXZ(g, MakeModelWorldAABB(wall, (Vector3){ x, 0.0f,  border }, scl,   0.0f));
        ResolveAgainstBoxXZ(g, MakeModelWorldAABB(wall, (Vector3){ x, 0.0f, -border }, scl, 180.0f));
    }
    for (int i = 0; i < count; ++i) {
        float z = -HALF + i * segLen;
        ResolveAgainstBoxXZ(g, MakeModelWorldAABB(wall, (Vector3){  border, 0.0f, z }, scl,  90.0f));
        ResolveAgainstBoxXZ(g, MakeModelWorldAABB(wall, (Vector3){ -border, 0.0f, z }, scl,  90.0f));
    }
}
