#include "app.h"
#include "world_manager.h"

#include <algorithm>
#include <cmath>
#include <string>

void App::addToInventory(BlockType type) {
    if (type == BlockType::Air || type == BlockType::Water) return;
    // 1. Stack with existing slot of same type (priority, any position, bottom→top right→left)
    for (int i = 15; i >= 0; --i) {
        if (inventory_[i].type == type && inventory_[i].count < 256) {
            inventory_[i].count++;
            return;
        }
    }
    // 2. Fill empty slot bottom→top, right→left (slot 15 first, slot 0 last)
    for (int i = 15; i >= 0; --i) {
        if (inventory_[i].type == BlockType::Air) {
            inventory_[i].type  = type;
            inventory_[i].count = 1;
            return;
        }
    }
    // Inventory full — drop silently
}

BlockType App::activeHotbarType() const {
    const InvSlot& s = inventory_[12 + hotbarSlot_];
    return (s.count > 0) ? s.type : BlockType::Air;
}

bool App::consumeHotbarSlot() {
    InvSlot& s = inventory_[12 + hotbarSlot_];
    if (s.count <= 0) return false;
    s.count--;
    if (s.count == 0) s.type = BlockType::Air;
    return true;
}

// ── Per-state per-frame handlers ──────────────────────────────────────────

void App::handleMainMenu(float dt) {
    (void)dt;
    // Mouse-click detection for buttons
    static bool mbWasDown = false;
    const bool mbNow = glfwGetMouseButton(window_, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
    const bool mbClick = mbNow && !mbWasDown;
    mbWasDown = mbNow;

    if (!mbClick) return;

    double mx, my;
    glfwGetCursorPos(window_, &mx, &my);
    // Scale cursor from logical to physical pixels (HiDPI support)
    int winW, winH;
    glfwGetWindowSize(window_, &winW, &winH);
    const float sw = static_cast<float>(swapchainExtent_.width);
    const float sh = static_cast<float>(swapchainExtent_.height);
    mx *= sw / winW;
    my *= sh / winH;
    // Mirror shader: SC = max(3, int(sh)/240); CW=5*SC, CH=5*SC
    const float cx = sw * 0.5f;
    const int   SC = std::max(3, (int)sh / 240);
    const float CW = 5.0f * SC;
    const float CH = 5.0f * SC;

    if (menuSubState_ == 0) {
        // ── World list screen ──────────────────────────────────────────────
        // Mirror shader: entryH=CH+8, entryS=CH+16, listW=CW*20
        const float entryH = CH + 8.0f;
        const float entryS = CH + 16.0f;
        const float listW  = CW * 20.0f;
        const float listTop = sh * 0.33f;
        for (int i = 0; i < static_cast<int>(worldList_.size()); ++i) {
            const float ey = listTop + i * entryS;
            if (my < ey || my >= ey + entryH) continue;
            // Settings button: rightmost CW*3 pixels of entry
            const float settBtnStart = cx + listW*0.5f - CW*3.0f;
            if (mx >= settBtnStart && mx < cx+listW*0.5f) {
                audio_.playUIClick();
                settingsWorldSel_ = i;
                nameInput_ = worldList_[i].name;
                deleteConfirm_ = 0;
                textField_ = 0;
                menuSubState_ = 2;
                return;
            }
            // Rest of entry: start world
            if (mx >= cx-listW*0.5f && mx < settBtnStart) {
                audio_.playUIClick();
                menuSel_ = i;
                startWorld(worldList_[i]);
                return;
            }
        }

        // "CREATE WORLD" button: btnY=sh*0.78, background: [cx-CW*6-8, cx+CW*6+8) × [btnY-4, btnY-4+CH+10)
        const float btnY   = sh * 0.78f;
        const float bHalfW = CW * 6.0f + 8.0f;
        if (mx >= cx-bHalfW && mx < cx+bHalfW && my >= btnY-4.0f && my < btnY-4.0f+CH+10.0f) {
            audio_.playUIClick();
            menuSubState_ = 1; textField_ = 0;
            nameInput_.clear(); seedInput_.clear();
            return;
        }
    } else if (menuSubState_ == 1) {
        // ── Create-world form ──────────────────────────────────────────────
        // Mirror shader: fw = min(CW*22+8, sw*7/10); ffx0 = cx - fw/2
        const float fw   = std::min(CW * 22.0f + 8.0f, sw * 0.7f);
        const float ffx0 = cx - fw * 0.5f;
        const float fy   = sh * 0.35f;
        const float nameY0 = fy + CH + 4.0f;
        const float nameY1 = nameY0 + CH + 8.0f;
        const float sy2    = fy + (CH + 8.0f) * 2.0f + 8.0f;
        const float seedY0 = sy2 + CH + 4.0f;
        const float seedY1 = seedY0 + CH + 8.0f;

        if (mx >= ffx0 && mx < ffx0+fw && my >= nameY0 && my < nameY1)
            textField_ = 0;
        else if (mx >= ffx0 && mx < ffx0+fw && my >= seedY0 && my < seedY1)
            textField_ = 1;

        // "CREATE" button: btnY2=sh*0.72, btnX2=cx-CW*3
        // background: [cx-CW*3-8, cx+CW*3+8) × [btnY2-4, btnY2-4+CH+8)
        const float btnY2   = sh * 0.72f;
        const float b2HalfW = CW*3.0f + 8.0f;
        if (mx >= cx-b2HalfW && mx < cx+b2HalfW && my >= btnY2-4.0f && my < btnY2-4.0f+CH+8.0f) {
            if (!nameInput_.empty()) {
                uint64_t seed = 12345ULL;
                if (!seedInput_.empty()) {
                    try { seed = std::stoull(seedInput_, nullptr, 16); } catch (...) {}
                } else {
                    seed = static_cast<uint64_t>(
                        std::chrono::steady_clock::now().time_since_epoch().count());
                }
                if (WorldManager::createWorld(nameInput_, seed)) {
                    worldList_ = WorldManager::listWorlds();
                    WorldInfo wi{nameInput_, seed};
                    nameInput_.clear(); seedInput_.clear();
                    menuSubState_ = 0; textField_ = 0;
                    startWorld(wi);
                }
            }
            return;
        }

        // "BACK" button: btnY3=sh*0.82, btnX3=cx-CW*2
        // background: [cx-CW*2-8, cx+CW*2+8) × [btnY3-4, btnY3-4+CH+8)
        const float btnY3   = sh * 0.82f;
        const float b3HalfW = CW*2.0f + 8.0f;
        if (mx >= cx-b3HalfW && mx < cx+b3HalfW && my >= btnY3-4.0f && my < btnY3-4.0f+CH+8.0f) {
            menuSubState_ = 0; nameInput_.clear(); seedInput_.clear(); textField_ = 0;
        }
    } else if (menuSubState_ == 2) {
        // ── World settings screen ──────────────────────────────────────────
        // Rename input field: ffx0=cx-fw/2, renY=sh*0.38, field at renY+CH+4
        const float fw     = std::min(CW * 22.0f + 8.0f, sw * 0.7f);
        const float ffx0   = cx - fw * 0.5f;
        const float renY   = sh * 0.38f;
        const float renFY0 = renY + CH + 4.0f;
        const float renFY1 = renFY0 + CH + 8.0f;
        if (mx >= ffx0 && mx < ffx0+fw && my >= renFY0 && my < renFY1)
            textField_ = 0;

        if (!deleteConfirm_) {
            // "RENAME" button: cx-CW*3-8 to cx+CW*3+8, sh*0.57
            const float btnRenY = sh * 0.57f;
            const float bRenHW  = CW*3.0f + 8.0f;
            if (mx >= cx-bRenHW && mx < cx+bRenHW
                && my >= btnRenY-4.0f && my < btnRenY-4.0f+CH+8.0f) {
                if (!nameInput_.empty()
                    && settingsWorldSel_ >= 0
                    && settingsWorldSel_ < static_cast<int>(worldList_.size())) {
                    std::string oldName = worldList_[settingsWorldSel_].name;
                    if (WorldManager::renameWorld(oldName, nameInput_)) {
                        worldList_ = WorldManager::listWorlds();
                        menuSubState_ = 0;
                        nameInput_.clear();
                        textField_ = 0;
                        settingsWorldSel_ = -1;
                    }
                }
                return;
            }
            // "DELETE" button: same width, sh*0.67
            const float btnDelY = sh * 0.67f;
            const float bDelHW  = CW*3.0f + 8.0f;
            if (mx >= cx-bDelHW && mx < cx+bDelHW
                && my >= btnDelY-4.0f && my < btnDelY-4.0f+CH+8.0f) {
                deleteConfirm_ = 1;
                return;
            }
        } else {
            // Confirming delete — "YES" at cx-CW*5, "NO" at cx+CW*2, sh*0.72
            const float btnConfY = sh * 0.72f;
            // YES button: btnX=cx-CW*5, width=CW*3+16
            const float yesX = cx - CW*5.0f;
            if (mx >= yesX-8.0f && mx < yesX+CW*3.0f+8.0f
                && my >= btnConfY-4.0f && my < btnConfY-4.0f+CH+8.0f) {
                if (settingsWorldSel_ >= 0
                    && settingsWorldSel_ < static_cast<int>(worldList_.size())) {
                    WorldManager::deleteWorld(worldList_[settingsWorldSel_].name);
                    worldList_ = WorldManager::listWorlds();
                }
                menuSubState_ = 0;
                deleteConfirm_ = 0;
                nameInput_.clear();
                textField_ = 0;
                settingsWorldSel_ = -1;
                return;
            }
            // NO button: btnX=cx+CW*2, width=CW*2+16
            const float noX = cx + CW*2.0f;
            if (mx >= noX-8.0f && mx < noX+CW*2.0f+8.0f
                && my >= btnConfY-4.0f && my < btnConfY-4.0f+CH+8.0f) {
                deleteConfirm_ = 0;
                return;
            }
        }

        // "BACK" button: sh*0.82, cx-CW*2-8 to cx+CW*2+8
        const float bkY  = sh * 0.82f;
        const float bkHW = CW*2.0f + 8.0f;
        if (mx >= cx-bkHW && mx < cx+bkHW && my >= bkY-4.0f && my < bkY-4.0f+CH+8.0f) {
            menuSubState_ = 0;
            nameInput_.clear();
            textField_ = 0;
            deleteConfirm_ = 0;
            settingsWorldSel_ = -1;
        }
    }
}

void App::handlePaused(float dt) {
    (void)dt;
    static bool mbWasDown = false;
    const bool mbNow = glfwGetMouseButton(window_, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
    const bool mbClick = mbNow && !mbWasDown;
    mbWasDown = mbNow;
    if (!mbClick) return;

    double mx, my;
    glfwGetCursorPos(window_, &mx, &my);
    int winW, winH;
    glfwGetWindowSize(window_, &winW, &winH);
    const float sw = static_cast<float>(swapchainExtent_.width);
    const float sh = static_cast<float>(swapchainExtent_.height);
    mx *= sw / winW;
    my *= sh / winH;

    // Mirror shader: SC2=max(3,sh/240), BW=max(240,CW2*16), GAP=max(10,SC2*4), BH=CH2+GAP, PAD=max(16,SC2*8)
    const float cx  = sw*0.5f;
    const float cy  = sh*0.5f;
    const int   SC2 = std::max(3, (int)sh / 240);
    const float CW2 = 5.0f * SC2;
    const float CH2 = 5.0f * SC2;
    const float BW  = std::max(240.0f, CW2 * 16.0f);
    const float GAP = std::max(10.0f, SC2 * 4.0f);
    const float BH  = CH2 + GAP;
    const float PAD = std::max(16.0f, SC2 * 8.0f);
    const float PANEL_H = CH2 + 20.0f + 3.0f*(BH+GAP) + PAD;
    const float py0  = cy - PANEL_H * 0.5f;
    const float bx0  = cx - BW * 0.5f;
    const float bw   = BW;
    const float by0  = py0 + PAD + CH2 + 20.0f;  // Resume
    const float by1  = by0 + BH + GAP;            // Main Menu
    const float by2  = by1 + BH + GAP;            // Quit

    auto inBtn = [&](float y0) {
        return mx >= bx0 && mx < bx0+bw && my >= y0 && my < y0+BH;
    };

    if (inBtn(by0)) {
        // Resume
        gameState_ = GameState::Playing;
        glfwSetInputMode(window_, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
        firstMouse_ = true;
    } else if (inBtn(by1)) {
        // Main menu
        leaveWorld();
    } else if (inBtn(by2)) {
        // Quit → show quitting splash, then close
        leaveWorld(); // save position first
        gameState_ = GameState::Quitting;
    }
}

void App::handleQuitting(float dt) {
    // After 1.5 s show "GOODBYE", then close
    static float quitTimer = 0.0f;
    quitTimer += dt;
    if (quitTimer > 1.5f) {
        quitTimer = 0.0f;
        glfwSetWindowShouldClose(window_, GLFW_TRUE);
    }
}

void App::handleInventoryClick() {
    static bool mbWasDown = false;
    const bool mbNow = glfwGetMouseButton(window_, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS;
    const bool mbClick = mbNow && !mbWasDown;
    mbWasDown = mbNow;
    if (!mbClick) return;

    double mx, my;
    glfwGetCursorPos(window_, &mx, &my);
    int winW, winH;
    glfwGetWindowSize(window_, &winW, &winH);
    const float sw = static_cast<float>(swapchainExtent_.width);
    const float sh = static_cast<float>(swapchainExtent_.height);
    mx *= sw / winW;
    my *= sh / winH;

    // Mirror shader: SLOT_SZ = max(48, sh/15), GAP = max(4, SLOT_SZ/12)
    const int SLOT_SZ  = std::max(48, (int)sh / 15);
    const int GAP      = std::max(4, SLOT_SZ / 12);
    const int PANEL_W  = 4 * SLOT_SZ + 3 * GAP + 16;
    const int PANEL_H  = 4 * SLOT_SZ + 3 * GAP + 16;
    const int px0      = (int)(sw * 0.5f) - PANEL_W / 2;
    const int py0      = (int)(sh * 0.5f) - PANEL_H / 2;

    // ── Crafting panel (to the right of the 4×4 inventory grid) ──────────
    {
        const int CGAP    = std::max(4, SLOT_SZ / 12);
        const int CRAFT_X = px0 + PANEL_W + 20;
        const int CRAFT_Y = py0;

        // Check 3×3 craft grid slots
        for (int ci = 0; ci < 9; ++ci) {
            int ccol = ci % 3, crow = ci / 3;
            int slotX = CRAFT_X + ccol * (SLOT_SZ + CGAP) + 8;
            int slotY = CRAFT_Y + crow * (SLOT_SZ + CGAP) + 8;
            int cpx = (int)mx - slotX, cpy = (int)my - slotY;
            if (cpx < 0 || cpx >= SLOT_SZ || cpy < 0 || cpy >= SLOT_SZ) continue;
            // Handle click on craft grid slot ci
            InvSlot& cs = craftGrid_[ci];
            if (heldItem_.type == BlockType::Air) {
                if (cs.type != BlockType::Air) { heldItem_ = cs; cs = {}; }
            } else if (cs.type == BlockType::Air) {
                cs = heldItem_; heldItem_ = {};
            } else if (cs.type == heldItem_.type) {
                int space = 256 - cs.count;
                int take  = std::min(space, heldItem_.count);
                cs.count += take; heldItem_.count -= take;
                if (heldItem_.count <= 0) heldItem_ = {};
            } else {
                std::swap(cs, heldItem_);
            }
            updateCraftOutput();
            return;
        }

        // Check craft output slot (right of 3×3 grid, vertically centered)
        const int outX = CRAFT_X + 3 * (SLOT_SZ + CGAP) + 30;
        const int outY = CRAFT_Y + (SLOT_SZ + CGAP) + 8;
        int opx = (int)mx - outX, opy = (int)my - outY;
        if (opx >= 0 && opx < SLOT_SZ && opy >= 0 && opy < SLOT_SZ) {
            if (craftOutput_.type != BlockType::Air) {
                addToInventory(craftOutput_.type);
                // Consume one set of ingredients from the 3×3 grid
                for (int ci = 0; ci < 9; ++ci) {
                    if (craftGrid_[ci].type != BlockType::Air) {
                        craftGrid_[ci].count--;
                        if (craftGrid_[ci].count <= 0) craftGrid_[ci] = {};
                    }
                }
                updateCraftOutput();
            }
            return;
        }

        // Check recipe list clicks (right of output slot)
        const int RECIPE_X = CRAFT_X + 3 * (SLOT_SZ + CGAP) + SLOT_SZ + 50;
        const int RECIPE_Y = CRAFT_Y;
        const int RECIPE_ROW_H = SLOT_SZ + CGAP;
        const int RECIPE_ROW_W = 2 * SLOT_SZ + CGAP + 8;

        static const BlockType RECIPE_GRIDS[7][9] = {
            // 0: Sticks (4) — 2 Wood in center column
            {BlockType::Air,BlockType::Wood,BlockType::Air,
             BlockType::Air,BlockType::Wood,BlockType::Air,
             BlockType::Air,BlockType::Air, BlockType::Air},
            // 1: WoodPickaxe
            {BlockType::Wood,BlockType::Wood,BlockType::Wood,
             BlockType::Air,BlockType::Stick,BlockType::Air,
             BlockType::Air,BlockType::Stick,BlockType::Air},
            // 2: StonePickaxe
            {BlockType::Stone,BlockType::Stone,BlockType::Stone,
             BlockType::Air, BlockType::Stick,BlockType::Air,
             BlockType::Air, BlockType::Stick,BlockType::Air},
            // 3: WoodAxe
            {BlockType::Wood,BlockType::Wood,BlockType::Air,
             BlockType::Wood,BlockType::Stick,BlockType::Air,
             BlockType::Air, BlockType::Stick,BlockType::Air},
            // 4: StoneAxe
            {BlockType::Stone,BlockType::Stone,BlockType::Air,
             BlockType::Stone,BlockType::Stick,BlockType::Air,
             BlockType::Air,  BlockType::Stick,BlockType::Air},
            // 5: WoodSword
            {BlockType::Air,BlockType::Wood, BlockType::Air,
             BlockType::Air,BlockType::Wood, BlockType::Air,
             BlockType::Air,BlockType::Stick,BlockType::Air},
            // 6: StoneSword
            {BlockType::Air,BlockType::Stone,BlockType::Air,
             BlockType::Air,BlockType::Stone,BlockType::Air,
             BlockType::Air,BlockType::Stick,BlockType::Air},
        };

        for (int ri = 0; ri < 7; ++ri) {
            int rowX = RECIPE_X;
            int rowY = RECIPE_Y + ri * RECIPE_ROW_H;
            int rpx  = (int)mx - rowX;
            int rpy  = (int)my - rowY;
            if (rpx < 0 || rpx >= RECIPE_ROW_W || rpy < 0 || rpy >= SLOT_SZ) continue;
            // Fill craft grid with this recipe
            for (int gi = 0; gi < 9; ++gi) {
                craftGrid_[gi].type  = RECIPE_GRIDS[ri][gi];
                craftGrid_[gi].count = (RECIPE_GRIDS[ri][gi] != BlockType::Air) ? 1 : 0;
            }
            updateCraftOutput();
            return;
        }
    }

    const int bpx = (int)mx - px0;
    const int bpy = (int)my - py0;
    if (bpx < 0 || bpx >= PANEL_W || bpy < 0 || bpy >= PANEL_H) return;

    const int cx3  = bpx - 8;
    const int cy3  = bpy - 8;
    const int col  = cx3 / (SLOT_SZ + GAP);
    const int row  = cy3 / (SLOT_SZ + GAP);
    if (col < 0 || col >= 4 || row < 0 || row >= 4) return;
    const int sx = cx3 - col * (SLOT_SZ + GAP);
    const int sy = cy3 - row * (SLOT_SZ + GAP);
    if (sx < 0 || sx >= SLOT_SZ || sy < 0 || sy >= SLOT_SZ) return;

    InvSlot& slot = inventory_[row * 4 + col];

    if (heldItem_.type == BlockType::Air) {
        // Pick up whole stack
        if (slot.type != BlockType::Air) {
            heldItem_ = slot;
            slot = {};
        }
    } else if (slot.type == BlockType::Air) {
        // Place into empty slot
        slot = heldItem_;
        heldItem_ = {};
    } else if (slot.type == heldItem_.type) {
        // Stack same type up to 256
        int space = 256 - slot.count;
        int take  = std::min(space, heldItem_.count);
        slot.count += take;
        heldItem_.count -= take;
        if (heldItem_.count <= 0) heldItem_ = {};
    } else {
        // Swap
        std::swap(slot, heldItem_);
    }
    updateCraftOutput();
}

void App::updateWindowTitle() {
    if (flyModeActive_) {
        constexpr int FLY_SPEEDS[] = { 30, 150, 600, 2500 };
        char buf[64];
        std::snprintf(buf, sizeof(buf), "minecraft | fly %d b/s  [scroll to change]",
                      FLY_SPEEDS[flySpeedLevel_]);
        glfwSetWindowTitle(window_, buf);
    } else {
        glfwSetWindowTitle(window_, "minecraft");
    }
}

void App::updateHotbarSelection() {
    // Sync selectedBlockType_ from active hotbar slot (used by voxel edit mode)
    selectedBlockType_ = activeHotbarType();
    if (selectedBlockType_ == BlockType::Air)
        selectedBlockType_ = BlockType::Stone; // fallback for voxel mode

    bool changed = false;
    if (changed) {
        updateWindowTitle();
    }
}

void App::updateSelectionOutline() {
    previousHit_ = currentHit_;
    currentHit_ = Raycast::cast(world_, camera_, 8.0f);

    const bool changed =
        currentHit_.hit != previousHit_.hit ||
        currentHit_.blockX != previousHit_.blockX ||
        currentHit_.blockY != previousHit_.blockY ||
        currentHit_.blockZ != previousHit_.blockZ;

    if (changed) {
        rebuildOutlineMesh(); // uses destroyBufferDeferred internally — no stall needed
    }
}

void App::handleBlockInteraction(float dt) {
    const bool leftNow  = glfwGetMouseButton(window_, GLFW_MOUSE_BUTTON_LEFT)  == GLFW_PRESS;
    const bool rightNow = glfwGetMouseButton(window_, GLFW_MOUSE_BUTTON_RIGHT) == GLFW_PRESS;

    // Advance hold timers; reset when button is released.
    constexpr float REPEAT_INTERVAL = 0.2f; // seconds between repeated actions
    if (leftNow)  leftHoldTimer_  += dt; else leftHoldTimer_  = REPEAT_INTERVAL;
    if (rightNow) rightHoldTimer_ += dt; else rightHoldTimer_ = REPEAT_INTERVAL;

    // Fire on first press (leftHoldTimer_ just crossed REPEAT_INTERVAL from reset)
    // or after each REPEAT_INTERVAL of continuous hold.
    const bool leftFire  = leftNow  && leftHoldTimer_  >= REPEAT_INTERVAL;
    const bool rightFire = rightNow && rightHoldTimer_ >= REPEAT_INTERVAL;
    if (leftFire)  leftHoldTimer_  = 0.0f;
    if (rightFire) rightHoldTimer_ = 0.0f;

    // When inventory is open, skip world interaction
    if (inventoryOpen_) {
        leftHoldTimer_  = REPEAT_INTERVAL;
        rightHoldTimer_ = REPEAT_INTERVAL;
        return;
    }

    // ── Mob hit detection (left press, 4.5-block reach) ─────────────────────
    const bool leftJustPressed = leftNow && !leftMousePressedLast_;
    if (leftJustPressed && attackCooldown_ <= 0.0f) {
        // Camera forward direction
        float fwdX = camera_.orientForward.x;
        float fwdY = camera_.orientForward.y;
        float fwdZ = camera_.orientForward.z;
        constexpr float REACH   = 4.5f;
        constexpr float DAMAGE  = 2.0f;
        int   hitIdx  = -1;
        float hitDist = REACH;
        for (int i = 0; i < static_cast<int>(mobs_.size()); ++i) {
            const Mob& m = mobs_[i];
            if (m.dead) continue;
            // Hit sphere centred on visual body mid-point along mob's gravity up axis
            float bodyMidH = (m.type == MobType::Human) ? 0.88f : 0.50f;
            float radius   = (m.type == MobType::Human) ? 0.55f : 0.65f;
            float mhx = static_cast<float>(m.x + m.upX * bodyMidH - cameraX_);
            float mhy = static_cast<float>(m.y + m.upY * bodyMidH - cameraY_);
            float mhz = static_cast<float>(m.z + m.upZ * bodyMidH - cameraZ_);
            // Closest point on ray to mob centre
            float t = mhx*fwdX + mhy*fwdY + mhz*fwdZ;
            if (t < 0.0f || t > REACH) continue;
            float cx = mhx - fwdX*t, cy = mhy - fwdY*t, cz = mhz - fwdZ*t;
            if (cx*cx + cy*cy + cz*cz < radius*radius && t < hitDist) {
                hitDist = t;
                hitIdx  = i;
            }
        }
        if (hitIdx >= 0) {
            Mob& hit = mobs_[hitIdx];
            hit.hp -= DAMAGE;
            hit.hitFlash = 0.35f;
            audio_.playMobHit();
            // Knockback: push mob away from player along camera forward
            constexpr float KB = 7.0f;
            hit.knockVelX += fwdX * KB;
            hit.knockVelZ += fwdZ * KB;
            if (hit.hp <= 0.0f) {
                hit.dead = true;
                audio_.playMobDeath();
            } else {
                // Reaction: wolf fights back, others flee
                if (hit.type == MobType::Wolf) {
                    hit.state = MobState::Chase;
                } else {
                    // Flee away from player
                    float fdx = static_cast<float>(hit.x - cameraX_);
                    float fdz = static_cast<float>(hit.z - cameraZ_);
                    float fl  = std::max(0.01f, std::sqrt(fdx*fdx + fdz*fdz));
                    hit.targetX = static_cast<float>(hit.x) + fdx/fl * 16.0f;
                    hit.targetY = static_cast<float>(hit.y);
                    hit.targetZ = static_cast<float>(hit.z) + fdz/fl * 16.0f;
                    hit.state   = MobState::Flee;
                    hit.aiTimer = 4.0f;
                }
            }
            attackCooldown_ = 0.4f;
            leftHoldTimer_ = 0.0f;
            leftMousePressedLast_ = leftNow;
            rightMousePressedLast_ = rightNow;
            return;
        }
    }

    // Block breaking (hold left button) — voxel mode uses instant break
    if (!inventoryOpen_) {
        if (leftNow && currentHit_.hit) {
            if (voxelEditMode_ || world_.isBlockSubdivided(currentHit_.blockX, currentHit_.blockY, currentHit_.blockZ)) {
                // Instant break for voxel/subdivided mode
                if (leftFire) {
                    vkDeviceWaitIdle(device_);
                    world_.setVoxel(
                        currentHit_.blockX, currentHit_.blockY, currentHit_.blockZ,
                        currentHit_.voxelX, currentHit_.voxelY, currentHit_.voxelZ,
                        BlockType::Air);
                    rebuildChunksAroundBlock(currentHit_.blockX, currentHit_.blockY, currentHit_.blockZ);
                    updateSelectionOutline();
                }
            } else {
                // Accumulate break progress
                if (breakX_ == currentHit_.blockX && breakY_ == currentHit_.blockY && breakZ_ == currentHit_.blockZ) {
                    BlockType bt = world_.getBlock(breakX_, breakY_, breakZ_);
                    float hard = blockHardness(bt);
                    float mult = toolSpeedMultiplier(activeHotbarType(), bt);
                    breakProgress_ += dt * mult / hard;
                    rebuildBreakMesh();
                    if (breakProgress_ >= 1.0f) {
                        BlockType broken = world_.getBlock(breakX_, breakY_, breakZ_);
                        audio_.playBlockBreak(broken);
                        vkDeviceWaitIdle(device_);
                        world_.setBlock(breakX_, breakY_, breakZ_, BlockType::Air);
                        world_.clearBlockPlayerPlaced(breakX_, breakY_, breakZ_);
                        if (broken == BlockType::Wood) enqueueLeafDecayAround(breakX_, breakY_, breakZ_);
                        rebuildChunksAroundBlock(breakX_, breakY_, breakZ_);
                        updateSelectionOutline();
                        // Spawn drops
                        if (broken == BlockType::Leaves) {
                            uint64_t rng = (uint64_t)((uint64_t)breakX_*73856093)^(uint64_t)((uint64_t)breakY_*19349663)^(uint64_t)((uint64_t)breakZ_*83492791);
                            rng ^= rng >> 33; rng *= 0xff51afd7ed558ccdULL; rng ^= rng >> 33;
                            if ((rng & 3) == 0) spawnDrop(breakX_+0.5+gravityUp_.x*0.3, breakY_+0.5+gravityUp_.y*0.3, breakZ_+0.5+gravityUp_.z*0.3, BlockType::Stick);
                        } else {
                            spawnDrop(breakX_+0.5+gravityUp_.x*0.3, breakY_+0.5+gravityUp_.y*0.3, breakZ_+0.5+gravityUp_.z*0.3, broken);
                        }
                        breakProgress_ = 0.0f;
                        destroyBreakMesh();
                    }
                } else {
                    breakX_ = currentHit_.blockX;
                    breakY_ = currentHit_.blockY;
                    breakZ_ = currentHit_.blockZ;
                    breakProgress_ = 0.0f;
                    destroyBreakMesh();
                }
            }
        } else {
            if (breakProgress_ > 0.0f) {
                breakProgress_ = 0.0f;
                destroyBreakMesh();
            }
        }
    }

    if (rightFire && currentHit_.hit) {
        const BlockType placeType = activeHotbarType();
        if (placeType == BlockType::Air) return; // nothing to place

        if (voxelEditMode_) {
            if (placeType != BlockType::Water) {
                vkDeviceWaitIdle(device_);
                world_.setVoxel(
                    currentHit_.placeX, currentHit_.placeY, currentHit_.placeZ,
                    currentHit_.placeVoxelX, currentHit_.placeVoxelY, currentHit_.placeVoxelZ,
                    placeType);
                consumeHotbarSlot();
                rebuildChunksAroundBlock(currentHit_.placeX, currentHit_.placeY, currentHit_.placeZ);
                updateSelectionOutline();
            }
        } else {
            if (placeType == BlockType::Water) {
                BlockType existing = world_.getBlock(currentHit_.placeX, currentHit_.placeY, currentHit_.placeZ);
                if (existing == BlockType::Air || existing == BlockType::Water) {
                    vkDeviceWaitIdle(device_);
                    world_.setWaterLevel(currentHit_.placeX, currentHit_.placeY, currentHit_.placeZ, 16);
                    consumeHotbarSlot();
                    rebuildChunksAroundBlock(currentHit_.placeX, currentHit_.placeY, currentHit_.placeZ);
                    updateSelectionOutline();
                }
            } else {
                if (world_.getBlock(currentHit_.placeX, currentHit_.placeY, currentHit_.placeZ) == BlockType::Air) {
                    audio_.playBlockPlace(placeType);
                    vkDeviceWaitIdle(device_);
                    world_.setBlock(currentHit_.placeX, currentHit_.placeY, currentHit_.placeZ, placeType);
                    // Mark player-placed blocks (Leaves won't decay; Wood won't trigger decay)
                    world_.markBlockPlayerPlaced(currentHit_.placeX, currentHit_.placeY, currentHit_.placeZ);
                    consumeHotbarSlot();
                    rebuildChunksAroundBlock(currentHit_.placeX, currentHit_.placeY, currentHit_.placeZ);
                    updateSelectionOutline();
                }
            }
        }
    }

    leftMousePressedLast_  = leftNow;
    rightMousePressedLast_ = rightNow;
}

int App::countInventory(BlockType bt) const {
    int n = 0;
    for (int i = 0; i < 16; ++i)
        if (inventory_[i].type == bt) n += inventory_[i].count;
    return n;
}

void App::updateCraftOutput() {
    auto t = [&](int i) -> BlockType { return craftGrid_[i].type; };
    auto c = [&](int i) -> int      { return craftGrid_[i].count; };
    craftOutput_ = {};

    // Grid layout (row-major, 3 cols):
    // 0 1 2
    // 3 4 5
    // 6 7 8

    using BT = BlockType;

    // Helper: check grid matches pattern exactly
    auto match = [&](const BT pat[9]) -> bool {
        for (int i = 0; i < 9; ++i)
            if (t(i) != pat[i] || (pat[i] != BT::Air && c(i) < 1)) return false;
        return true;
    };

    // 0: Sticks (4) — Wood in slots 1 and 4
    const BT pat0[9] = {BT::Air,BT::Wood,BT::Air, BT::Air,BT::Wood,BT::Air, BT::Air,BT::Air,BT::Air};
    if (match(pat0)) { craftOutput_ = {BT::Stick, 4}; return; }

    // 1: WoodPickaxe — top row Wood, slots 4+7 Stick
    const BT pat1[9] = {BT::Wood,BT::Wood,BT::Wood, BT::Air,BT::Stick,BT::Air, BT::Air,BT::Stick,BT::Air};
    if (match(pat1)) { craftOutput_ = {BT::WoodPickaxe, 1}; return; }

    // 2: StonePickaxe
    const BT pat2[9] = {BT::Stone,BT::Stone,BT::Stone, BT::Air,BT::Stick,BT::Air, BT::Air,BT::Stick,BT::Air};
    if (match(pat2)) { craftOutput_ = {BT::StonePickaxe, 1}; return; }

    // 3: WoodAxe
    const BT pat3[9] = {BT::Wood,BT::Wood,BT::Air, BT::Wood,BT::Stick,BT::Air, BT::Air,BT::Stick,BT::Air};
    if (match(pat3)) { craftOutput_ = {BT::WoodAxe, 1}; return; }

    // 4: StoneAxe
    const BT pat4[9] = {BT::Stone,BT::Stone,BT::Air, BT::Stone,BT::Stick,BT::Air, BT::Air,BT::Stick,BT::Air};
    if (match(pat4)) { craftOutput_ = {BT::StoneAxe, 1}; return; }

    // 5: WoodSword
    const BT pat5[9] = {BT::Air,BT::Wood, BT::Air, BT::Air,BT::Wood, BT::Air, BT::Air,BT::Stick,BT::Air};
    if (match(pat5)) { craftOutput_ = {BT::WoodSword, 1}; return; }

    // 6: StoneSword
    const BT pat6[9] = {BT::Air,BT::Stone,BT::Air, BT::Air,BT::Stone,BT::Air, BT::Air,BT::Stick,BT::Air};
    if (match(pat6)) { craftOutput_ = {BT::StoneSword, 1}; return; }
}
