import Signal from "@antivivi/lemon-signal";
import { getAllInstanceInfo } from "@antivivi/vrldk";
import React, { Fragment, useEffect, useMemo, useRef, useState } from "@rbxts/react";
import { RunService, TweenService } from "@rbxts/services";
import SingleDocumentManager from "client/components/sidebar/SingleDocumentManager";
import useSingleDocument from "client/components/sidebar/useSingleDocumentWindow";
import { showErrorToast } from "client/components/toast/ToastService";
import TechWindow from "client/components/window/TechWindow";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import { RobotoMono, RobotoMonoBold } from "shared/asset/GameFonts";
import Item from "shared/item/Item";
import type { RepairResultTier } from "shared/item/repair";
import Items from "shared/items/Items";
import Packets from "shared/Packets";

type Phase = "idle" | "countdown" | "running" | "success" | "fail";

interface TargetData {
    id: number;
    angle: number;
    size: number;
    startTime: number; // When this target's approach circle starts closing
    approachDuration: number;
    direction: number; // Stores radius for positioning
    hit: boolean;
    hitTier?: RepairResultTier;
    goodWindow: number;
    greatWindow: number;
    perfectWindow: number;
    negativeGracePeriod: number;
}

const TARGET_FADE_IN_TWEENINFO = new TweenInfo(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out);

const BASE_APPROACH_DURATION = 2.5; // seconds for the approach ring to close (easiest)
const FASTEST_APPROACH_DURATION = 0.7; // seconds for the approach ring to close (hardest)
const BASE_GOOD_WINDOW = 0.6; // acceptable progress window (± from 0) for easiest items
const MIN_GOOD_WINDOW = 0.3; // acceptable progress window (± from 0) for hardest items
const BASE_GREAT_WINDOW = 0.25; // great progress window (± from 0) for easiest items
const MIN_GREAT_WINDOW = 0.125; // great progress window (± from 0) for hardest items
const BASE_PERFECT_WINDOW = 0.07; // perfect tier window (± from 0) for easiest items
const MIN_PERFECT_WINDOW = 0.035; // perfect tier window (± from 0) for hardest items
const BASE_TARGETS_PACING = 1; // seconds between targets for easiest items
const MIN_TARGETS_PACING = 0.3; // seconds between targets for hardest items
const MIN_TARGETS = 3;
const MAX_TARGETS = 8;
const BASE_TARGET_SIZE = 100; // Largest target size for easiest items
const MIN_TARGET_SIZE = 50; // Smallest target size for hardest items

function calculateGoodWindow(itemIndex: number, totalItems: number): number {
    const difficulty = itemIndex / math.max(totalItems - 1, 1);
    return BASE_GOOD_WINDOW - difficulty * (BASE_GOOD_WINDOW - MIN_GOOD_WINDOW);
}

function calculateGreatWindow(itemIndex: number, totalItems: number): number {
    const difficulty = itemIndex / math.max(totalItems - 1, 1);
    return BASE_GREAT_WINDOW - difficulty * (BASE_GREAT_WINDOW - MIN_GREAT_WINDOW);
}

function calculatePerfectWindow(itemIndex: number, totalItems: number): number {
    const difficulty = itemIndex / math.max(totalItems - 1, 1);
    return BASE_PERFECT_WINDOW - difficulty * (BASE_PERFECT_WINDOW - MIN_PERFECT_WINDOW);
}

function getResultTier(progress: number, perfectWindow: number, greatWindow: number): RepairResultTier {
    const absProgress = math.abs(progress);
    if (absProgress <= perfectWindow) return "Perfect";
    if (absProgress <= greatWindow) return "Great";
    return "Good";
}

function calculateTargetCount(itemIndex: number, totalItems: number): number {
    // Early items get 3 targets, later items get more
    const difficulty = itemIndex / math.max(totalItems - 1, 1);
    return math.floor(MIN_TARGETS + difficulty * (MAX_TARGETS - MIN_TARGETS));
}

function calculateTargetSize(itemIndex: number, totalItems: number): number {
    // Early items get larger targets, later items get smaller
    const difficulty = itemIndex / math.max(totalItems - 1, 1);
    return BASE_TARGET_SIZE - difficulty * (BASE_TARGET_SIZE - MIN_TARGET_SIZE);
}

function calculateApproachDuration(itemIndex: number, totalItems: number): number {
    // Early items get slower approach rate, later items get faster
    const difficulty = itemIndex / math.max(totalItems - 1, 1);
    return BASE_APPROACH_DURATION - difficulty * (BASE_APPROACH_DURATION - FASTEST_APPROACH_DURATION);
}

function calculatePacing(itemIndex: number, totalItems: number): number {
    // Early items get slower pacing, later items get faster
    const difficulty = itemIndex / math.max(totalItems - 1, 1);
    return BASE_TARGETS_PACING - difficulty * (BASE_TARGETS_PACING - MIN_TARGETS_PACING);
}

export class RepairManager {
    static model: Model | undefined;
    static modelInfo: InstanceInfo | undefined;
    static placementId: string | undefined;
    static item: Item | undefined;

    static readonly updated = new Signal();

    static setRepairing(toRepair: Model | undefined) {
        this.model = toRepair;
        this.modelInfo = toRepair ? getAllInstanceInfo(toRepair) : undefined;
        this.placementId = toRepair ? toRepair.Name : undefined;
        const itemId = this.modelInfo ? this.modelInfo.itemId : undefined;
        if (itemId) {
            this.item = Items.getItem(itemId);
        }
        if (toRepair) {
            SingleDocumentManager.open("Repair");
        } else {
            SingleDocumentManager.close("Repair");
        }
        this.updated.fire();
    }
}

export default function RepairWindow() {
    const { id, visible, closeDocument } = useSingleDocument({
        id: "Repair",
        onClose: () => {
            setPhase("idle");
            setActiveItem(undefined);
        },
    });
    const [activeItem, setActiveItem] = React.useState<Item | undefined>();
    const [phase, setPhase] = useState<Phase>("idle");
    const [countdownValue, setCountdownValue] = useState(3);
    const [attemptSeed, setAttemptSeed] = useState(0);
    const [feedback, setFeedback] = useState<string | undefined>();
    const [resultTier, setResultTier] = useState<RepairResultTier | undefined>();
    const [showResultTier, setShowResultTier] = useState(false);
    const random = useMemo(() => new Random(), []);
    const heartbeatRef = useRef<RBXScriptConnection>();
    const [sparks, setSparks] = useState<
        Array<{ id: string; angle: number; distance: number; delay: number; velocity: number; tier: RepairResultTier }>
    >([]);
    const [consecutiveFailures, setConsecutiveFailures] = useState(0);

    // Multi-target state
    const [targets, setTargets] = useState<TargetData[]>([]);
    const [currentTargetIndex, setCurrentTargetIndex] = useState(0);
    const [gameStartTime, setGameStartTime] = useState(0);
    const [currentTime, setCurrentTime] = useState(0);
    const targetsRef = useRef<TargetData[]>([]);
    const currentTargetIndexRef = useRef(0);

    targetsRef.current = targets;
    currentTargetIndexRef.current = currentTargetIndex;

    useEffect(() => {
        return () => {
            heartbeatRef.current?.Disconnect();
            heartbeatRef.current = undefined;
        };
    }, []);

    // Generate targets when starting a new attempt
    useEffect(() => {
        if (phase !== "countdown") return;

        // Calculate target count based on item difficulty
        const itemIndex = activeItem ? Items.sortedItems.indexOf(activeItem) : 0;

        // Apply difficulty reduction based on consecutive failures
        // Each failure makes the game progressively easier (up to 5 failures)
        const failureReduction = math.min(consecutiveFailures * 0.15, 0.75); // Up to 75% easier

        let targetCount = calculateTargetCount(itemIndex, Items.length);
        // Reduce target count on failures (minimum 2 targets)
        targetCount = math.max(math.floor(targetCount * (1 - failureReduction * 0.5)), 2);

        let targetSize = calculateTargetSize(itemIndex, Items.length);
        // Increase target size on failures
        targetSize = targetSize + failureReduction * 50;

        let approachDuration = calculateApproachDuration(itemIndex, Items.length);
        // Slow down approach on failures
        approachDuration = approachDuration + failureReduction * 1.5;

        let goodWindow = calculateGoodWindow(itemIndex, Items.length);
        // Widen windows on failures
        goodWindow = goodWindow + failureReduction * 0.3;

        let greatWindow = calculateGreatWindow(itemIndex, Items.length);
        greatWindow = greatWindow + failureReduction * 0.15;

        let perfectWindow = calculatePerfectWindow(itemIndex, Items.length);
        perfectWindow = perfectWindow + failureReduction * 0.05;

        const negativeGracePeriod = goodWindow + 0.05; // let the player witness their miss

        let pacing = calculatePacing(itemIndex, Items.length);
        // Increase pacing (slower) on failures
        pacing = pacing + failureReduction * 0.8;

        // Generate targets with random positions that don't overlap
        const newTargets: TargetData[] = [];
        const minDistance = 0.2; // Minimum distance between target centers (in scale units)
        const maxAttempts = 100; // Maximum attempts to find a non-overlapping position

        for (let i = 0; i < targetCount; i++) {
            // Space targets out in time
            const startTime = i * pacing;

            let angle: number;
            let radius: number;
            let attempts = 0;
            let validPosition = false;

            // Try to find a position that doesn't overlap with existing targets
            while (!validPosition && attempts < maxAttempts) {
                // Random angle and radius within playable area
                angle = random.NextNumber() * math.pi * 2;
                radius = 0.15 + random.NextNumber() * 0.25; // Between 0.15 and 0.4 from center

                // Calculate this position
                const x = 0.5 + math.cos(angle) * radius;
                const y = 0.5 + math.sin(angle) * radius;

                // Check distance to all existing targets
                validPosition = true;
                for (const existingTarget of newTargets) {
                    const existingX = 0.5 + math.cos(existingTarget.angle) * existingTarget.direction;
                    const existingY = 0.5 + math.sin(existingTarget.angle) * existingTarget.direction;

                    const distance = math.sqrt(math.pow(x - existingX, 2) + math.pow(y - existingY, 2));
                    if (distance < minDistance) {
                        validPosition = false;
                        break;
                    }
                }

                attempts++;
            }

            // If we couldn't find a valid position after max attempts, just use the last one
            newTargets.push({
                id: i,
                angle: angle!,
                size: targetSize,
                startTime: startTime,
                approachDuration: approachDuration,
                direction: radius!, // Store radius in direction field
                hit: false,
                goodWindow: goodWindow,
                greatWindow: greatWindow,
                perfectWindow: perfectWindow,
                negativeGracePeriod: negativeGracePeriod,
            });
        }

        setTargets(newTargets);
        setCurrentTargetIndex(0);
    }, [phase, attemptSeed, activeItem, random, consecutiveFailures]);

    useEffect(() => {
        if (phase !== "countdown") return;

        let active = true;
        let value = 4;

        const advance = () => {
            if (!active) return;
            value -= 1;
            if (value === 0) {
                setPhase("running");
                setGameStartTime(tick());
                return;
            }

            setCountdownValue(value);
            setFeedback(
                targets.size() > 1
                    ? `Hit ${targets.size()} targets in sequence!`
                    : "Match the pulse when the rings meet!",
            );
            playSound("repair/Start.mp3", undefined, (sound) => {
                sound.PlaybackSpeed = 0.8 + (3 - value) * 0.1;
            });

            task.delay(0.4, () => {
                if (!active) return;
                advance();
            });
        };

        advance();

        return () => {
            active = false;
        };
    }, [phase, attemptSeed, targets]);

    useEffect(() => {
        if (phase !== "running") return;

        heartbeatRef.current?.Disconnect();
        setFeedback(undefined);

        heartbeatRef.current = RunService.Heartbeat.Connect((dt) => {
            if (phase !== "running") return;

            const elapsed = tick() - gameStartTime;
            setCurrentTime(elapsed);

            // Check if we've failed any target by letting it go too negative
            const currentTargets = targetsRef.current;
            for (const target of currentTargets) {
                if (target.hit) continue;

                const targetElapsed = elapsed - target.startTime;
                if (targetElapsed > target.approachDuration + target.negativeGracePeriod) {
                    // Auto-fail if we missed a target completely
                    heartbeatRef.current?.Disconnect();
                    heartbeatRef.current = undefined;
                    setPhase("fail");
                    setFeedback("Missed a target! The mechanism jammed.");
                    playSound("Error.mp3");
                    setConsecutiveFailures((prev) => prev + 1);
                    return;
                }
            }

            // Check if all targets are hit
            const allHit = currentTargets.every((t) => t.hit);
            if (allHit) {
                heartbeatRef.current?.Disconnect();
                heartbeatRef.current = undefined;

                // Calculate overall tier based on all hits
                const tiers: RepairResultTier[] = [];
                for (const t of currentTargets) {
                    if (t.hitTier !== undefined) {
                        tiers.push(t.hitTier);
                    }
                }
                const perfectCount = tiers.filter((t) => t === "Perfect").size();
                const greatCount = tiers.filter((t) => t === "Great").size();

                let overallTier: RepairResultTier;
                if (perfectCount === tiers.size()) {
                    overallTier = "Perfect";
                } else if (perfectCount + greatCount === tiers.size()) {
                    overallTier = "Great";
                } else {
                    overallTier = "Good";
                }

                setResultTier(overallTier);
                setPhase("success");
                setFeedback(`All targets hit! ${overallTier} repair sequence!`);
            }
        });

        return () => {
            heartbeatRef.current?.Disconnect();
            heartbeatRef.current = undefined;
        };
    }, [phase, attemptSeed, gameStartTime]);

    const handleTargetActivated = (targetId: number) => {
        if (phase !== "running") return;

        const currentTargets = [...targetsRef.current];
        const target = currentTargets.find((t) => t.id === targetId);
        if (!target || target.hit) return;

        const elapsed = currentTime - target.startTime;
        const progress = 1 - elapsed / target.approachDuration;

        if (math.abs(progress) <= target.goodWindow) {
            const tier = getResultTier(progress, target.perfectWindow, target.greatWindow);
            target.hit = true;
            target.hitTier = tier;
            setTargets(currentTargets);

            // Play sound for this hit
            playSound("repair/Hit.mp3");

            // Move to next target
            if (targetId === currentTargetIndexRef.current) {
                setCurrentTargetIndex(currentTargetIndexRef.current + 1);
            }
        } else {
            // Bad hit
            heartbeatRef.current?.Disconnect();
            heartbeatRef.current = undefined;
            setPhase("fail");
            if (progress > target.goodWindow) {
                setFeedback("Too early! The calibrators rattled loose.");
            } else {
                setFeedback("Too late! The calibrators rattled loose.");
            }
            playSound("Error.mp3");
            setConsecutiveFailures((prev) => prev + 1);
        }
    };

    useEffect(() => {
        if (phase === "success" && resultTier !== undefined) {
            const tier = resultTier;
            if (tier === "Perfect") {
                playSound("repair/Perfect.mp3");
            } else if (tier === "Great") {
                playSound("repair/Great.mp3");
            } else {
                playSound("repair/Good.mp3");
            }

            // Reset failure counter on success
            setConsecutiveFailures(0);

            // Delay showing the tier text for dramatic effect
            setShowResultTier(false);
            task.delay(0.2, () => {
                setShowResultTier(true);
            });

            // Create spark effects for Great and Perfect completions
            if (tier === "Perfect" || tier === "Great") {
                const sparkCount = tier === "Perfect" ? 24 : 16;
                const sparkArray: Array<{
                    id: string;
                    angle: number;
                    distance: number;
                    delay: number;
                    velocity: number;
                    tier: RepairResultTier;
                }> = [];
                for (let i = 0; i < sparkCount; i++) {
                    sparkArray.push({
                        id: `spark_${i}_${tick()}`,
                        angle: (i / sparkCount) * math.pi * 2,
                        distance: 80 + math.random() * 40,
                        delay: math.random() * 0.3,
                        velocity: tier === "Perfect" ? 120 + math.random() * 80 : 80 + math.random() * 60,
                        tier: tier,
                    });
                }
                setSparks(sparkArray);

                // Clear sparks after animation completes
                task.delay(3, () => {
                    setSparks([]);
                });
            }

            const placementId = RepairManager.placementId;
            if (placementId === undefined) return;

            const accepted = Packets.repairItem.toServer(placementId, tier);
            if (accepted === false) {
                showErrorToast("Repair request failed. Try again.");
            }
        }
    }, [phase]);

    const startDisabled = phase === "countdown" || phase === "running";
    const showCountdown = phase === "countdown";

    const statusLabel = (() => {
        if (feedback) return feedback;

        // Show difficulty assistance message if player has failed multiple times
        const assistanceMessage =
            consecutiveFailures >= 2 ? ` (Difficulty reduced after ${consecutiveFailures} failures)` : "";

        switch (phase) {
            case "idle":
                return targets.size() > 1
                    ? `Hit ${targets.size()} targets in sequence to complete the repair.${assistanceMessage}`
                    : `Press start and time your click when the outer ring overlaps the core.${assistanceMessage}`;
            case "countdown":
                return "Steady your tools and wait for the pulse.";
            case "running": {
                const remainingTargets = targets.filter((t) => !t.hit).size();
                return remainingTargets > 0
                    ? `${remainingTargets} target${remainingTargets > 1 ? "s" : ""} remaining!`
                    : "All targets hit!";
            }
            case "success":
                if (resultTier) return `${resultTier} repair!`;
                return "Repair complete!";
            case "fail":
                return "Missed the timing—try again.";
        }
    })();

    useEffect(() => {
        if (phase === "success") return;

        const connection = Packets.itemRepairCompleted.fromServer((placementId) => {
            // Check if other clients repaired this item
            if (placementId === RepairManager.placementId) {
                playSound("repair/Complete.mp3");
                closeDocument();
            }
        });
        return () => connection.Disconnect();
    }, [phase]);

    useEffect(() => {
        const connection = RepairManager.updated.connect(() => {
            setActiveItem(RepairManager.item);
            // Reset state when a new item is selected
            resetToIdle();
            // Reset failure counter when switching items
            setConsecutiveFailures(0);
        });
        return () => connection.Disconnect();
    }, []);

    const beginRun = () => {
        heartbeatRef.current?.Disconnect();
        heartbeatRef.current = undefined;

        setResultTier(undefined);
        setShowResultTier(false);
        setAttemptSeed((attempt) => attempt + 1);
        setPhase("countdown");
    };

    const resetToIdle = () => {
        heartbeatRef.current?.Disconnect();
        heartbeatRef.current = undefined;
        setPhase("idle");
        setFeedback(undefined);
        setShowResultTier(false);
        setTargets([]);
        setCurrentTargetIndex(0);
    };

    if (!activeItem) {
        return (
            <TechWindow title="Repair Station" icon={getAsset("assets/Broken.png")} id={id} visible={visible}>
                <Fragment />
            </TechWindow>
        );
    }

    return (
        <TechWindow title="Repair Station" icon={getAsset("assets/Broken.png")} id={id} visible={visible}>
            <textlabel
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                Size={new UDim2(1, -10, 0.4, 0)}
                Text={activeItem.name.upper()}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextTransparency={0.9}
                TextScaled={true}
                ZIndex={-5}
            >
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(1, Color3.fromRGB(200, 200, 200)),
                        ])
                    }
                />
            </textlabel>
            <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
                <uilistlayout
                    Padding={new UDim(0, 12)}
                    FillDirection={Enum.FillDirection.Vertical}
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />
                <imagebutton
                    AutoButtonColor={false}
                    BackgroundColor3={Color3.fromRGB(18, 18, 18)}
                    BackgroundTransparency={0.15}
                    BorderSizePixel={0}
                    Image={getAsset("assets/Vignette.png")}
                    ImageTransparency={0.5}
                    Size={new UDim2(1, 0, 1, -82)}
                    Event={{ Activated: () => playSound("Click.mp3") }}
                >
                    <imagelabel
                        BackgroundTransparency={1}
                        Image={getAsset("assets/Grid.png")}
                        ImageTransparency={0.95}
                        ImageColor3={Color3.fromRGB(100, 100, 100)}
                        ScaleType={Enum.ScaleType.Tile}
                        Size={new UDim2(1, 0, 1, 0)}
                        TileSize={new UDim2(0, 128, 0, 128)}
                    >
                        <uicorner CornerRadius={new UDim(0, 18)} />
                    </imagelabel>
                    <uicorner CornerRadius={new UDim(0, 18)} />
                    <uistroke Color={Color3.fromRGB(255, 255, 255)} Transparency={0.7} />
                    <uiaspectratioconstraint AspectRatio={1} DominantAxis={Enum.DominantAxis.Height} />

                    <textlabel
                        AnchorPoint={new Vector2(0.5, 0.5)}
                        BackgroundTransparency={1}
                        FontFace={RobotoMono}
                        Position={new UDim2(0.5, 0, 0.5, 0)}
                        Size={new UDim2(1, 0, 0, 32)}
                        Text={showCountdown ? `${countdownValue}` : ""}
                        TextColor3={Color3.fromRGB(255, 255, 255)}
                        TextScaled={true}
                        Visible={showCountdown}
                    >
                        <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={2} />
                    </textlabel>

                    <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
                        {/* Render all targets */}
                        {targets.map((target) => {
                            const elapsed = currentTime - target.startTime;
                            const progress = 1 - elapsed / target.approachDuration;

                            // Calculate fixed position (no movement) - radius stored in direction field
                            const radius = target.direction; // Radius is stored in direction field
                            const x = 0.5 + math.cos(target.angle) * radius;
                            const y = 0.5 + math.sin(target.angle) * radius;
                            const position = new UDim2(x, 0, y, 0);

                            // Determine visibility - target appears when its approach starts
                            const isVisible = elapsed >= 0 && !target.hit;

                            // Target color based on state
                            let targetColor = Color3.fromRGB(255, 212, 120);
                            let strokeColor = Color3.fromRGB(110, 58, 0);
                            if (target.hit) {
                                if (target.hitTier === "Perfect") {
                                    targetColor = Color3.fromRGB(255, 215, 0);
                                    strokeColor = Color3.fromRGB(120, 60, 0);
                                } else if (target.hitTier === "Great") {
                                    targetColor = Color3.fromRGB(100, 200, 255);
                                    strokeColor = Color3.fromRGB(20, 60, 120);
                                } else {
                                    targetColor = Color3.fromRGB(150, 255, 150);
                                    strokeColor = Color3.fromRGB(30, 100, 30);
                                }
                            }

                            // Number label for sequence
                            const sequenceNumber = target.id + 1;

                            return (
                                <imagebutton
                                    ref={(rbx) => {
                                        if (rbx === undefined) return;
                                        if (isVisible) {
                                            TweenService.Create(rbx, TARGET_FADE_IN_TWEENINFO, {
                                                BackgroundTransparency: 0,
                                            }).Play();
                                        } else {
                                            rbx.BackgroundTransparency = 1;
                                        }
                                    }}
                                    key={`target-${target.id}`}
                                    Active={isVisible && !target.hit}
                                    AnchorPoint={new Vector2(0.5, 0.5)}
                                    AutoButtonColor={false}
                                    BackgroundTransparency={1}
                                    BackgroundColor3={targetColor}
                                    Position={position}
                                    Size={new UDim2(0, target.size, 0, target.size)}
                                    Visible={isVisible || target.hit}
                                    Event={{
                                        Activated: () => handleTargetActivated(target.id),
                                    }}
                                    ZIndex={target.hit ? 1 : 2}
                                >
                                    <uicorner CornerRadius={new UDim(1, 0)} />
                                    <uistroke Color={strokeColor} Thickness={2} />

                                    {/* Sequence number */}
                                    <textlabel
                                        AnchorPoint={new Vector2(0.5, 0.5)}
                                        BackgroundTransparency={1}
                                        FontFace={RobotoMonoBold}
                                        Position={new UDim2(0.5, 0, 0.5, 0)}
                                        Size={new UDim2(0.6, 0, 0.6, 0)}
                                        Text={`${sequenceNumber}`}
                                        TextColor3={Color3.fromRGB(50, 25, 0)}
                                        TextScaled={true}
                                        Visible={!target.hit}
                                        ZIndex={3}
                                    />

                                    {/* Approach circle */}
                                    {isVisible && !target.hit && (
                                        <frame
                                            Active={true}
                                            AnchorPoint={new Vector2(0.5, 0.5)}
                                            BackgroundTransparency={1}
                                            Position={new UDim2(0.5, 0, 0.5, 0)}
                                            Size={new UDim2(1, math.abs(progress) * 150, 1, math.abs(progress) * 150)}
                                        >
                                            <uicorner CornerRadius={new UDim(1, 0)} />
                                            <uistroke
                                                ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                                                Color={
                                                    progress >= 0
                                                        ? Color3.fromRGB(255, 148, 117)
                                                        : Color3.fromRGB(255, 100, 100)
                                                }
                                                Thickness={3}
                                                Transparency={0.2}
                                            />
                                            <uigradient
                                                Color={
                                                    progress >= 0
                                                        ? new ColorSequence([
                                                              new ColorSequenceKeypoint(0, Color3.fromRGB(44, 82, 136)),
                                                              new ColorSequenceKeypoint(
                                                                  1,
                                                                  Color3.fromRGB(117, 200, 255),
                                                              ),
                                                          ])
                                                        : new ColorSequence([
                                                              new ColorSequenceKeypoint(0, Color3.fromRGB(136, 44, 44)),
                                                              new ColorSequenceKeypoint(
                                                                  1,
                                                                  Color3.fromRGB(255, 100, 100),
                                                              ),
                                                          ])
                                                }
                                                Rotation={-45}
                                            />
                                        </frame>
                                    )}

                                    {/* Hit indicator */}
                                    {target.hit && target.hitTier && (
                                        <textlabel
                                            AnchorPoint={new Vector2(0.5, 0.5)}
                                            BackgroundTransparency={1}
                                            FontFace={RobotoMonoBold}
                                            Position={new UDim2(0.5, 0, 0.5, 0)}
                                            Size={new UDim2(0.8, 0, 0.4, 0)}
                                            Text={
                                                target.hitTier === "Perfect"
                                                    ? "!!"
                                                    : target.hitTier === "Great"
                                                      ? "!"
                                                      : "✓"
                                            }
                                            TextColor3={Color3.fromRGB(255, 255, 255)}
                                            TextScaled={true}
                                            ZIndex={3}
                                        >
                                            <uistroke Color={strokeColor} Thickness={2} />
                                        </textlabel>
                                    )}
                                </imagebutton>
                            );
                        })}

                        {phase === "success" && resultTier && showResultTier && <ResultTierDisplay tier={resultTier} />}

                        {phase === "fail" && (
                            <textlabel
                                AnchorPoint={new Vector2(0.5, 0.5)}
                                BackgroundTransparency={1}
                                FontFace={RobotoMonoBold}
                                Position={new UDim2(0.5, 0, 0.5, 0)}
                                Size={new UDim2(0.9, 0, 0.2, 0)}
                                Text="Resetting gears..."
                                TextColor3={Color3.fromRGB(255, 140, 140)}
                                TextScaled={true}
                            >
                                <uistroke Color={Color3.fromRGB(87, 16, 16)} Thickness={2} />
                            </textlabel>
                        )}
                    </frame>

                    {/* Spark effects for Great/Perfect completions */}
                    {sparks.map((spark) => (
                        <SparkEffect
                            key={spark.id}
                            angle={spark.angle}
                            distance={spark.distance}
                            delay={spark.delay}
                            velocity={spark.velocity}
                            tier={spark.tier}
                        />
                    ))}
                </imagebutton>

                <textlabel
                    BackgroundTransparency={1}
                    FontFace={RobotoMono}
                    Size={new UDim2(1, 0, 0, 22)}
                    Text={statusLabel}
                    TextColor3={Color3.fromRGB(205, 255, 255)}
                    TextScaled={true}
                    TextWrapped={true}
                />

                <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 0, 36)}>
                    <uilistlayout
                        FillDirection={Enum.FillDirection.Horizontal}
                        HorizontalAlignment={Enum.HorizontalAlignment.Center}
                        Padding={new UDim(0, 10)}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                        VerticalAlignment={Enum.VerticalAlignment.Center}
                    />

                    {phase === "idle" && (
                        <MiniGameOption
                            text="Start Repair"
                            color={Color3.fromRGB(28, 207, 150)}
                            onClick={() => {
                                playSound("CheckOn.mp3");
                                beginRun();
                            }}
                        />
                    )}

                    {phase === "fail" && (
                        <MiniGameOption
                            text="Repair Again"
                            color={Color3.fromRGB(255, 214, 135)}
                            onClick={() => {
                                playSound("CheckOn.mp3");
                                beginRun();
                            }}
                        />
                    )}

                    {(phase === "success" || phase === "fail") && (
                        <MiniGameOption
                            text="Exit"
                            color={Color3.fromRGB(156, 156, 156)}
                            onClick={() => {
                                if (phase === "success") {
                                    RepairManager.setRepairing(undefined);
                                    return;
                                }

                                playSound("CheckOff.mp3");
                                resetToIdle();
                            }}
                        />
                    )}
                </frame>
            </frame>
        </TechWindow>
    );
}

// Animated result tier display component
function ResultTierDisplay({ tier }: { tier: RepairResultTier }) {
    const textRef = useRef<TextLabel>();
    const glowRef = useRef<Frame>();

    // Different colors and sizes for each tier
    const tierConfig = useMemo(() => {
        switch (tier) {
            case "Perfect":
                return {
                    color: Color3.fromRGB(255, 215, 0), // Gold
                    glowColor: Color3.fromRGB(255, 255, 150),
                    strokeColor: Color3.fromRGB(120, 60, 0),
                    size: new UDim2(0.9, 0, 0.4, 0),
                    glowSize: 40,
                };
            case "Great":
                return {
                    color: Color3.fromRGB(100, 200, 255), // Light blue
                    glowColor: Color3.fromRGB(150, 220, 255),
                    strokeColor: Color3.fromRGB(20, 60, 120),
                    size: new UDim2(0.8, 0, 0.35, 0),
                    glowSize: 30,
                };
            case "Good":
                return {
                    color: Color3.fromRGB(150, 255, 150), // Light green
                    glowColor: Color3.fromRGB(200, 255, 200),
                    strokeColor: Color3.fromRGB(30, 100, 30),
                    size: new UDim2(0.7, 0, 0.3, 0),
                    glowSize: 20,
                };
        }
    }, [tier]);

    useEffect(() => {
        if (!textRef.current || !glowRef.current) return;

        const text = textRef.current;
        const glow = glowRef.current;

        // Initial state - small and transparent
        text.Size = new UDim2(0, 0, 0, 0);
        text.TextTransparency = 1;
        text.TextStrokeTransparency = 1;
        text.Rotation = -15;
        glow.BackgroundTransparency = 1;
        glow.Size = new UDim2(0, 0, 0, 0);

        // Animate entrance with spring-like effect
        const sizeTween = TweenService.Create(
            text,
            new TweenInfo(0.5, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
            {
                Size: tierConfig.size,
                Rotation: 0,
            },
        );

        const fadeTween = TweenService.Create(
            text,
            new TweenInfo(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
            {
                TextTransparency: 0,
                TextStrokeTransparency: 0,
            },
        );

        const glowSizeTween = TweenService.Create(
            glow,
            new TweenInfo(0.4, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
            {
                Size: new UDim2(1, tierConfig.glowSize, 1, tierConfig.glowSize),
            },
        );

        const glowFadeTween = TweenService.Create(
            glow,
            new TweenInfo(0.3, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
            {
                BackgroundTransparency: 0.6,
            },
        );

        sizeTween.Play();
        fadeTween.Play();
        glowSizeTween.Play();
        glowFadeTween.Play();

        // Pulsing glow effect for Perfect tier
        if (tier === "Perfect") {
            let pulseConnection: RBXScriptConnection | undefined;
            let time = 0;

            task.delay(0.3, () => {
                pulseConnection = RunService.Heartbeat.Connect((deltaTime) => {
                    if (!glow.Parent) {
                        pulseConnection?.Disconnect();
                        return;
                    }

                    time += deltaTime;
                    const pulse = (math.sin(time * 8) + 1) / 2; // 0 to 1
                    const transparency = 0.4 + pulse * 0.4; // 0.4 to 0.8
                    glow.BackgroundTransparency = transparency;

                    // Stop after 2 seconds
                    if (time > 2) {
                        pulseConnection?.Disconnect();
                    }
                });
            });

            return () => {
                pulseConnection?.Disconnect();
            };
        }

        // Gentle pulse for Great tier
        if (tier === "Great") {
            let pulseConnection: RBXScriptConnection | undefined;
            let time = 0;

            task.delay(0.3, () => {
                pulseConnection = RunService.Heartbeat.Connect((deltaTime) => {
                    if (!glow.Parent) {
                        pulseConnection?.Disconnect();
                        return;
                    }

                    time += deltaTime;
                    const pulse = (math.sin(time * 5) + 1) / 2; // 0 to 1
                    const transparency = 0.5 + pulse * 0.3; // 0.5 to 0.8
                    glow.BackgroundTransparency = transparency;

                    if (time > 2) {
                        pulseConnection?.Disconnect();
                    }
                });
            });

            return () => {
                pulseConnection?.Disconnect();
            };
        }
    }, [tier, tierConfig]);

    return (
        <frame
            AnchorPoint={new Vector2(0.5, 0.5)}
            BackgroundTransparency={1}
            Position={new UDim2(0.5, 0, 0.5, 0)}
            Size={new UDim2(1, -90, 1, -90)}
            ZIndex={10}
        >
            {/* Glow effect behind text */}
            <frame
                ref={glowRef}
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundColor3={tierConfig.glowColor}
                BackgroundTransparency={1}
                BorderSizePixel={0}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                Size={new UDim2(0, 0, 0, 0)}
                ZIndex={9}
            >
                <uicorner CornerRadius={new UDim(0, 16)} />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, tierConfig.glowColor),
                            new ColorSequenceKeypoint(0.5, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(1, tierConfig.glowColor),
                        ])
                    }
                    Rotation={45}
                    Transparency={
                        new NumberSequence([
                            new NumberSequenceKeypoint(0, 0.8),
                            new NumberSequenceKeypoint(0.5, 0.3),
                            new NumberSequenceKeypoint(1, 0.8),
                        ])
                    }
                />
            </frame>

            {/* Main text with shadow layers for 3D effect */}
            {/* Shadow layer */}
            <textlabel
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                Position={new UDim2(0.5, 3, 0.5, 3)}
                Size={tierConfig.size}
                Text={`${tier}!`}
                TextColor3={Color3.fromRGB(0, 0, 0)}
                TextScaled={true}
                TextTransparency={0.5}
                ZIndex={10}
            />

            {/* Main text */}
            <textlabel
                ref={textRef}
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                Size={tierConfig.size}
                Text={`${tier}!`}
                TextColor3={tierConfig.color}
                TextScaled={true}
                ZIndex={11}
            >
                <uistroke Color={tierConfig.strokeColor} Thickness={tier === "Perfect" ? 4 : 3} />
                <uigradient
                    Color={
                        new ColorSequence([
                            new ColorSequenceKeypoint(0, Color3.fromRGB(255, 255, 255)),
                            new ColorSequenceKeypoint(0.5, tierConfig.color),
                            new ColorSequenceKeypoint(1, tierConfig.color.Lerp(Color3.fromRGB(0, 0, 0), 0.3)),
                        ])
                    }
                    Rotation={-45}
                />
            </textlabel>
        </frame>
    );
}

function MiniGameOption({ text, color, onClick }: { text: string; color: Color3; onClick: () => void }) {
    return (
        <textbutton
            BackgroundColor3={color}
            BorderColor3={Color3.fromRGB(255, 255, 255)}
            BorderSizePixel={3}
            Size={new UDim2(0.4, 0, 1, 0)}
            Text={""}
            Active={true}
            Event={{ Activated: onClick }}
        >
            <uipadding
                PaddingTop={new UDim(0, 6)}
                PaddingBottom={new UDim(0, 6)}
                PaddingLeft={new UDim(0, 12)}
                PaddingRight={new UDim(0, 12)}
            />
            <textlabel
                BackgroundTransparency={1}
                FontFace={RobotoMonoBold}
                Size={new UDim2(1, 0, 1, 0)}
                Text={text}
                TextColor3={new Color3(1, 1, 1)}
                TextScaled={true}
            >
                <uistroke Color={Color3.fromRGB(0, 0, 0)} Thickness={2} />
            </textlabel>
            <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={Color3.fromRGB(0, 0, 0)} Thickness={2} />
            <uigradient
                Color={new ColorSequence(Color3.fromRGB(255, 255, 255), color.Lerp(Color3.fromRGB(255, 255, 255), 0.5))}
                Rotation={90}
            />
        </textbutton>
    );
}

function SparkEffect({
    angle,
    distance,
    delay,
    velocity,
    tier,
}: {
    angle: number;
    distance: number;
    delay: number;
    velocity: number;
    tier: RepairResultTier;
}) {
    const sparkRef = useRef<ImageLabel>();

    // Determine spark color based on tier
    const sparkColor = useMemo(() => {
        switch (tier) {
            case "Perfect":
                return Color3.fromRGB(255, 223, 62); // Golden yellow
            case "Great":
                return Color3.fromRGB(100, 200, 255); // Light blue
            case "Good":
                return Color3.fromRGB(150, 255, 150); // Light green
        }
    }, [tier]);

    useEffect(() => {
        if (!sparkRef.current) return;

        const spark = sparkRef.current;

        // Calculate initial position around the target button
        const centerX = 0.5;
        const centerY = 0.5;
        const initialX = centerX + (math.cos(angle) * distance) / 1000;
        const initialY = centerY + (math.sin(angle) * distance) / 1000;

        // Physics properties
        let positionX = initialX;
        let positionY = initialY;
        let velocityX = (math.cos(angle) * velocity) / 1000;
        let velocityY = (math.sin(angle) * velocity) / 1000 - 0.2;
        const gravity = 0.6; // Gravity acceleration (downward)
        const damping = 0.98; // Air resistance

        let time = 0;
        let animationConnection: RBXScriptConnection | undefined;

        spark.Position = new UDim2(positionX, 0, positionY, 0);
        spark.ImageTransparency = 1; // Start invisible
        spark.Size = new UDim2(0, 12, 0, 12);

        // Delayed animation start
        task.delay(delay, () => {
            if (!spark.Parent) return;

            spark.ImageTransparency = 0;

            // Start physics simulation
            animationConnection = RunService.Heartbeat.Connect((deltaTime) => {
                if (!spark.Parent) {
                    animationConnection?.Disconnect();
                    return;
                }

                time += deltaTime;

                // Apply gravity to vertical velocity
                velocityY += gravity * deltaTime;

                // Apply damping to horizontal velocity
                velocityX *= damping;

                // Update position
                positionX += velocityX * deltaTime;
                positionY += velocityY * deltaTime;

                // Update spark position
                spark.Position = new UDim2(positionX, 0, positionY, 0);

                // Rotate the spark as it falls
                spark.Rotation = time * 180;

                // Fade out over time and shrink
                const fadeTime = math.min(time / 2.5, 1); // Fade over 2.5 seconds
                spark.ImageTransparency = fadeTime;

                const scale = math.max(1 - fadeTime * 0.6, 0.4); // Shrink to 40% of original size
                spark.Size = new UDim2(0, 12 * scale, 0, 12 * scale);

                // Remove spark when it's too low or completely faded
                if (positionY > 1.5 || fadeTime >= 1) {
                    animationConnection?.Disconnect();
                    if (spark.Parent) {
                        spark.Parent = undefined;
                    }
                }
            });
        });

        return () => {
            // Cleanup when component unmounts
            animationConnection?.Disconnect();
        };
    }, [angle, distance, delay, velocity]);

    return (
        <imagelabel
            ref={sparkRef}
            AnchorPoint={new Vector2(0.5, 0.5)}
            BackgroundTransparency={1}
            Image={getAsset("assets/Spark.png")}
            ImageColor3={sparkColor}
            Size={new UDim2(0, 12, 0, 12)}
            ZIndex={5}
        />
    );
}


import React, { Fragment, useEffect, useMemo, useState } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { PLACED_ITEMS_FOLDER } from "shared/constants";
import type { RepairResultTier } from "shared/item/repair";
import Packets from "shared/Packets";

const TweenService = game.GetService("TweenService");

interface ActiveEffect {
    placementId: string;
    model: Model;
    primaryPart: BasePart;
    tier: RepairResultTier;
    startedAt: number;
    fadingOut?: boolean;
}

const EFFECT_DURATION: Record<RepairResultTier, number> = {
    Good: 3,
    Great: 4.5,
    Perfect: 8.5,
};

const FADE_OUT_DURATION = 0.8;

interface TierVisual {
    color: Color3;
    outline: Color3;
    brightness: number;
    range: number;
    fillTransparency: number;
    outlineTransparency: number;
}

const TIER_VISUALS: Record<RepairResultTier, TierVisual> = {
    Good: {
        color: Color3.fromRGB(140, 240, 180),
        outline: Color3.fromRGB(200, 255, 220),
        brightness: 1.8,
        range: 16,
        fillTransparency: 0.93,
        outlineTransparency: 0.6,
    },
    Great: {
        color: Color3.fromRGB(120, 210, 255),
        outline: Color3.fromRGB(170, 235, 255),
        brightness: 2.6,
        range: 20,
        fillTransparency: 0.85,
        outlineTransparency: 0.45,
    },
    Perfect: {
        color: Color3.fromRGB(255, 223, 62),
        outline: Color3.fromRGB(255, 255, 200),
        brightness: 4,
        range: 26,
        fillTransparency: 0.82,
        outlineTransparency: 0.35,
    },
};

function findPrimaryPart(model: Model) {
    const explicit = model.PrimaryPart;
    if (explicit && explicit.IsA("BasePart")) {
        return explicit;
    }

    const candidate = model.FindFirstChildWhichIsA("BasePart", true);
    return candidate ?? undefined;
}

export default function RepairedItemEffectRenderer() {
    const [effects, setEffects] = useState<Array<ActiveEffect>>([]);

    useEffect(() => {
        const connection = Packets.itemRepairCompleted.fromServer((placementId, tier) => {
            if (TIER_VISUALS[tier] === undefined) {
                setEffects((current) => current.filter((effect) => effect.placementId !== placementId));
                return;
            }

            const instance = PLACED_ITEMS_FOLDER.FindFirstChild(placementId);
            if (!instance || !instance.IsA("Model")) return;

            const primaryPart = findPrimaryPart(instance);
            if (!primaryPart) return;

            const startedAt = tick();
            const effect: ActiveEffect = {
                placementId,
                model: instance,
                primaryPart,
                tier,
                startedAt,
            };

            setEffects((current) => [...current.filter((existing) => existing.placementId !== placementId), effect]);

            const duration = EFFECT_DURATION[tier];
            task.delay(duration - FADE_OUT_DURATION, () => {
                setEffects((current) =>
                    current.map((existing) => {
                        if (existing.placementId === placementId && existing.startedAt === startedAt) {
                            return { ...existing, fadingOut: true };
                        }
                        return existing;
                    }),
                );
            });

            task.delay(duration, () => {
                setEffects((current) =>
                    current.filter(
                        (existing) => existing.placementId !== placementId || existing.startedAt !== startedAt,
                    ),
                );
            });
        });

        return () => {
            connection.Disconnect();
            setEffects([]);
        };
    }, []);

    const renderedEffects = useMemo(() => {
        return effects.map((effect) => {
            const visuals = TIER_VISUALS[effect.tier];
            const { color, outline, brightness, range, fillTransparency, outlineTransparency } = visuals;

            const light = ReactRoblox.createPortal(
                <pointlight
                    Brightness={brightness}
                    Range={range}
                    Color={color}
                    Shadows={true}
                    ref={(rbx) => {
                        if (rbx && effect.fadingOut) {
                            const tweenInfo = new TweenInfo(
                                FADE_OUT_DURATION,
                                Enum.EasingStyle.Quad,
                                Enum.EasingDirection.Out,
                            );
                            const tween = TweenService.Create(rbx, tweenInfo, {
                                Brightness: 0,
                            });
                            tween.Play();
                        }
                    }}
                />,
                effect.primaryPart,
            );

            return (
                <Fragment key={`${effect.placementId}-${effect.startedAt}`}>
                    {light}
                    <highlight
                        Adornee={effect.model}
                        FillColor={color}
                        FillTransparency={fillTransparency}
                        OutlineColor={outline}
                        OutlineTransparency={outlineTransparency}
                        ref={(rbx) => {
                            if (rbx && effect.fadingOut) {
                                const tweenInfo = new TweenInfo(
                                    FADE_OUT_DURATION,
                                    Enum.EasingStyle.Quad,
                                    Enum.EasingDirection.Out,
                                );
                                const tween = TweenService.Create(rbx, tweenInfo, {
                                    FillTransparency: 1,
                                    OutlineTransparency: 1,
                                });
                                tween.Play();
                            }
                        }}
                    />
                </Fragment>
            );
        });
    }, [effects]);

    return <Fragment>{renderedEffects}</Fragment>;
}


/**
 * @fileoverview Empty state component for the inventory
 *
 * Displays a message when the player has no items in their inventory.
 */

import React from "@rbxts/react";
import { RobotoSlab, RobotoSlabHeavy } from "shared/asset/GameFonts";

interface InventoryEmptyStateProps {
    /** Whether the empty state should be visible */
    visible: boolean;
}

/**
 * Empty state component shown when inventory has no items
 */
export default function InventoryEmptyState({ visible }: InventoryEmptyStateProps) {
    return (
        <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)} Visible={visible}>
            <textlabel
                Active={true}
                AnchorPoint={new Vector2(0.5, 0.5)}
                AutomaticSize={Enum.AutomaticSize.XY}
                BackgroundTransparency={1}
                FontFace={RobotoSlabHeavy}
                Position={new UDim2(0.5, 0, 0.3, 0)}
                Text="You don't have any items!"
                TextColor3={Color3.fromRGB(206, 206, 206)}
                TextSize={30}
                TextWrapped={true}
            >
                <uistroke Thickness={2} />
            </textlabel>

            <textlabel
                Active={true}
                AnchorPoint={new Vector2(0.5, 0.5)}
                AutomaticSize={Enum.AutomaticSize.XY}
                BackgroundTransparency={1}
                FontFace={RobotoSlab}
                LayoutOrder={1}
                Position={new UDim2(0.5, 0, 0.34, 20)}
                Text="Buy some stuff in the shop to get started."
                TextColor3={Color3.fromRGB(129, 129, 129)}
                TextSize={25}
                TextWrapped={true}
            >
                <uistroke Thickness={2} />
            </textlabel>

            <uilistlayout
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
        </frame>
    );
}


import { FuzzySearch } from "@rbxts/fuzzy-search";
import React, { forwardRef, useCallback, useMemo, useRef, useState } from "@rbxts/react";
import { useMessageTooltip } from "client/components/tooltip/TooltipManager";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import Item from "shared/item/Item";
import Items from "shared/items/Items";

declare global {
    type TraitFilterId = (typeof TRAIT_OPTIONS)[number]["id"];
}

export const TRAIT_OPTIONS = [
    {
        id: "Dropper",
        image: getAsset("assets/indexing/Dropper.png"),
        color: Color3.fromRGB(255, 92, 92),
    },
    {
        id: "Furnace",
        image: getAsset("assets/indexing/Furnace.png"),
        color: Color3.fromRGB(255, 155, 74),
    },
    {
        id: "Upgrader",
        image: getAsset("assets/indexing/Upgrader.png"),
        color: Color3.fromRGB(245, 255, 58),
    },
    {
        id: "Conveyor",
        image: getAsset("assets/indexing/Conveyor.png"),
        color: Color3.fromRGB(131, 255, 78),
    },
    {
        id: "Generator",
        image: getAsset("assets/indexing/Generator.png"),
        color: Color3.fromRGB(60, 171, 255),
    },
    {
        id: "Charger",
        image: getAsset("assets/indexing/Charger.png"),
        color: Color3.fromRGB(255, 170, 255),
    },
    {
        id: "Miscellaneous",
        image: getAsset("assets/indexing/Miscellaneous.png"),
        color: Color3.fromRGB(170, 85, 255),
    },
] as const;

/**
 * Checks if the item has any of the whitelisted traits enabled.
 * If the Miscellaneous trait is enabled, it will return true if no other traits are found.
 *
 * @param item The item to check.
 * @param whitelistedTraits The traits to check for.
 * @returns Whether the item is whitelisted.
 */
export function isWhitelisted(item: Item, whitelistedTraits: Set<TraitFilterId>) {
    if (whitelistedTraits.isEmpty()) return true;
    let isMisc = true;
    for (const traitOption of TRAIT_OPTIONS) {
        const traitId = traitOption.id;
        if (item.isA(traitId as keyof ItemTraits)) {
            isMisc = false;
            if (whitelistedTraits.has(traitId)) return true;
        }
    }
    return isMisc && whitelistedTraits.has("Miscellaneous");
}

export type ItemFilterData = {
    layoutOrder: number;
    visible: boolean;
};

/**
 * Filters items based on search query and trait filters.
 * @param searchQuery The search query string.
 * @param traitFilters The selected trait filters.
 * @returns A map of item IDs to their layout order and visibility.
 */
export function filterItems(items: Set<Item>, searchQuery: string, traitFilters: Set<TraitFilterId>) {
    const processedItems = new Set<string>();
    const dataPerItem = new Map<string, ItemFilterData>();

    if (searchQuery !== "") {
        const terms = new Array<string>();
        for (const item of items) {
            if (!isWhitelisted(item, traitFilters)) continue;
            terms.push(item.name);
        }
        const sorted = FuzzySearch.Sorting.FuzzyScore(terms, searchQuery);
        for (const [index, name] of sorted) {
            const item = Items.itemsPerName.get(name);
            if (item === undefined) continue; // Skip if item not found
            if (processedItems.has(item.id)) continue; // Skip duplicates
            processedItems.add(item.id);
            dataPerItem.set(item.id, {
                layoutOrder: index,
                visible: index > 0,
            });
        }
    } else {
        for (const item of items) {
            if (!isWhitelisted(item, traitFilters)) continue;
            dataPerItem.set(item.id, {
                layoutOrder: item.layoutOrder,
                visible: true,
            });
        }
    }
    return dataPerItem;
}

/**
 * Inventory filter component with search and trait filtering
 */
const InventoryFilter = forwardRef<
    Frame,
    {
        /** Border color for the search box */
        color?: Color3;
        /** Currently selected trait filters */
        traitFilters: Set<TraitFilterId>;
        /** Callback when search query changes */
        onSearchChange: (query: string) => void;
        /** Callback when trait filter is toggled */
        onTraitToggle: (traitId: TraitFilterId) => void;
        /** Callback when clear button is pressed */
        onClear: () => void;
        /** Optional ref for search textbox */
        searchBoxRef?: React.Ref<TextBox>;
        /** Optional ref for filter frame */
        filterFrameRef?: React.Ref<Frame>;
        /** Optional refs for individual filter buttons */
        filterButtonRefs?: React.MutableRefObject<Map<string, ImageButton>>;
    }
>(
    (
        {
            color = Color3.fromRGB(255, 186, 125),
            traitFilters,
            onSearchChange,
            onTraitToggle,
            onClear,
            searchBoxRef,
            filterFrameRef,
            filterButtonRefs,
        },
        ref,
    ) => {
        const textBoxRef = useRef<TextBox>();
        const [previousText, setPreviousText] = useState("");

        const handleSearchChange = useCallback(
            (rbx: TextBox) => {
                const text = rbx.Text;
                onSearchChange(text);
                if (text.size() > previousText.size()) {
                    switch (math.random(1, 4)) {
                        case 1:
                            playSound("KeyPress1.mp3");
                            break;
                        case 2:
                            playSound("KeyPress2.mp3");
                            break;
                        case 3:
                            playSound("KeyPress3.mp3");
                            break;
                        case 4:
                            playSound("KeyPress4.mp3");
                            break;
                    }
                } else {
                    playSound("KeyDelete.mp3");
                }
                setPreviousText(text);
            },
            [onSearchChange],
        );

        // Generate tooltip props for each trait at the top level
        const tooltipPropsArray = TRAIT_OPTIONS.map((trait) => ({
            id: trait.id,
            props: useMessageTooltip(trait.id),
        }));

        const tooltipPropsPerTrait = useMemo(() => {
            const tooltipProps = new Map<string, UseHoverReturn>();
            for (const { id, props } of tooltipPropsArray) {
                tooltipProps.set(id, props);
            }
            return tooltipProps;
        }, [tooltipPropsArray]);

        return (
            <frame ref={ref} BackgroundTransparency={1} LayoutOrder={-1} Size={new UDim2(1, 0, 0.025, 20)}>
                <uilistlayout
                    FillDirection={Enum.FillDirection.Horizontal}
                    HorizontalAlignment={Enum.HorizontalAlignment.Left}
                    Padding={new UDim(0, 15)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                    VerticalAlignment={Enum.VerticalAlignment.Center}
                />
                <uipadding PaddingLeft={new UDim(0, 10)} />

                {/* Search textbox wrapper */}
                <frame BackgroundTransparency={1} LayoutOrder={2} Size={new UDim2(0.4, 0, 1, 0)}>
                    <textbox
                        ref={searchBoxRef}
                        AnchorPoint={new Vector2(0.5, 0.5)}
                        BackgroundColor3={Color3.fromRGB(255, 255, 255)}
                        BorderSizePixel={0}
                        ClearTextOnFocus={false}
                        Font={Enum.Font.RobotoMono}
                        PlaceholderText="Search..."
                        Position={new UDim2(0.5, 0, 0.5, 0)}
                        Size={new UDim2(1, 0, 1, 0)}
                        Text={""}
                        TextColor3={Color3.fromRGB(0, 0, 0)}
                        TextScaled={true}
                        TextSize={25}
                        TextWrapped={true}
                        TextXAlignment={Enum.TextXAlignment.Left}
                        ZIndex={100}
                        Change={{
                            Text: handleSearchChange,
                        }}
                    >
                        {/* Search box padding */}
                        <uipadding
                            PaddingBottom={new UDim(0, 5)}
                            PaddingLeft={new UDim(0, 10)}
                            PaddingRight={new UDim(0, 5)}
                            PaddingTop={new UDim(0, 5)}
                        />

                        {/* Search icon */}
                        <imagebutton
                            Active={true}
                            AnchorPoint={new Vector2(1, 0.5)}
                            BackgroundTransparency={1}
                            Event={{
                                Activated: () => {
                                    setPreviousText("");
                                    if (searchBoxRef && "current" in searchBoxRef && searchBoxRef.current) {
                                        searchBoxRef.current.Text = "";
                                    } else if (textBoxRef.current) {
                                        textBoxRef.current.Text = "";
                                    }
                                },
                            }}
                            Image={
                                previousText === ""
                                    ? getAsset("assets/indexing/Search.png")
                                    : getAsset("assets/indexing/Clear.png")
                            }
                            ImageColor3={Color3.fromRGB(143, 143, 143)}
                            ImageTransparency={0.5}
                            Position={new UDim2(1, 0, 0.5, 0)}
                            ScaleType={Enum.ScaleType.Fit}
                            Selectable={false}
                            Size={new UDim2(1, 0, 0.7, 0)}
                            SizeConstraint={Enum.SizeConstraint.RelativeYY}
                        />

                        {/* Search box border */}
                        <uistroke ApplyStrokeMode={Enum.ApplyStrokeMode.Border} Color={color} Thickness={2} />
                    </textbox>
                </frame>

                {/* Trait filter options wrapper */}
                <frame BackgroundTransparency={1} LayoutOrder={3} Size={new UDim2(0.6, 0, 1, 0)}>
                    <frame
                        ref={filterFrameRef}
                        AnchorPoint={new Vector2(0.5, 0.5)}
                        BackgroundTransparency={1}
                        Position={new UDim2(0.5, 0, 0.5, 0)}
                        Size={new UDim2(1, 0, 1, 0)}
                    >
                        <uilistlayout
                            FillDirection={Enum.FillDirection.Horizontal}
                            Padding={new UDim(0, 6)}
                            SortOrder={Enum.SortOrder.LayoutOrder}
                            VerticalAlignment={Enum.VerticalAlignment.Center}
                        />

                        {/* Render trait filter buttons */}
                        {TRAIT_OPTIONS.map((trait, index) => (
                            <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
                                <imagebutton
                                    key={trait.id}
                                    ref={(node) => {
                                        if (filterButtonRefs && node) {
                                            filterButtonRefs.current.set(trait.id, node);
                                        }
                                    }}
                                    AnchorPoint={new Vector2(0.5, 0.5)}
                                    BackgroundTransparency={1}
                                    Image={trait.image}
                                    ImageColor3={trait.color}
                                    ImageTransparency={traitFilters.has(trait.id) ? 0 : 0.6}
                                    LayoutOrder={index + 1}
                                    Position={new UDim2(0.5, 0, 0.5, 0)}
                                    ScaleType={Enum.ScaleType.Fit}
                                    Size={new UDim2(1, 0, 1, 0)}
                                    Event={{
                                        Activated: () => onTraitToggle(trait.id),
                                        ...tooltipPropsPerTrait.get(trait.id)!.events,
                                    }}
                                >
                                    <uipadding
                                        PaddingBottom={new UDim(0, 5)}
                                        PaddingLeft={new UDim(0, 5)}
                                        PaddingRight={new UDim(0, 5)}
                                        PaddingTop={new UDim(0, 5)}
                                    />
                                </imagebutton>
                                <uiaspectratioconstraint />
                            </frame>
                        ))}

                        {/* Clear button */}
                        <textbutton
                            AutomaticSize={Enum.AutomaticSize.X}
                            BackgroundTransparency={1}
                            Font={Enum.Font.SourceSans}
                            LayoutOrder={99}
                            Size={new UDim2(0, 0, 0.5, 0)}
                            Text="Clear"
                            TextColor3={Color3.fromRGB(255, 255, 255)}
                            TextScaled={true}
                            TextSize={14}
                            TextTransparency={0.5}
                            TextWrapped={true}
                            Event={{
                                Activated: onClear,
                            }}
                        />
                    </frame>
                </frame>
            </frame>
        );
    },
);

export default InventoryFilter;

export function useBasicInventoryFilter() {
    const [searchQuery, setSearchQuery] = useState("");
    const [traitFilters, setTraitFilters] = useState<Set<TraitFilterId>>(new Set());

    // Handle search change
    const onSearchChange = useCallback((query: string) => {
        setSearchQuery(query);
    }, []);

    // Handle trait filter toggle
    const onTraitToggle = useCallback((traitId: TraitFilterId) => {
        setTraitFilters((prev) => {
            const newFilters = table.clone(prev);
            if (newFilters.has(traitId)) {
                playSound("CheckOff.mp3");
                newFilters.delete(traitId);
            } else {
                playSound("CheckOn.mp3");
                newFilters.add(traitId);
            }
            return newFilters;
        });
    }, []);

    // Handle filter clear
    const onClear = useCallback(() => {
        setTraitFilters(new Set());
    }, []);

    return {
        searchQuery,
        props: {
            traitFilters,
            onSearchChange,
            onTraitToggle,
            onClear,
        },
    };
}


import React, { StrictMode } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { CreateReactStory } from "@rbxts/ui-labs";
import InventoryWindow from "client/components/item/inventory/InventoryWindow";
import StoryMocking from "client/components/StoryMocking";
import TooltipWindow from "client/components/tooltip/TooltipWindow";
import { useSingleDocumentVisibility } from "client/hooks/useVisibility";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
        controls: {
            visible: true,
        },
    },
    (props) => {
        StoryMocking.mockData();

        useSingleDocumentVisibility("Inventory", props.controls.visible);

        return (
            <StrictMode>
                <InventoryWindow />
                <TooltipWindow />
            </StrictMode>
        );
    },
);


/**
 * @fileoverview Main inventory window React component
 *
 * Modern React implementation of the inventory window using BasicWindow.
 * Follows the same pattern as QuestWindow for consistency.
 */

import React, { useCallback, useEffect, useMemo, useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import BuildManager from "client/components/build/BuildManager";
import InventoryEmptyState from "client/components/item/inventory/InventoryEmptyState";
import InventoryFilter, {
    filterItems,
    ItemFilterData,
    TRAIT_OPTIONS,
    useBasicInventoryFilter,
} from "client/components/item/inventory/InventoryFilter";
import {
    createInventorySlot,
    updateInventorySlot,
    type InventorySlotHandle,
} from "client/components/item/inventory/InventorySlot";
import useSingleDocument from "client/components/sidebar/useSingleDocumentWindow";
import { showErrorToast } from "client/components/toast/ToastService";
import BasicWindow from "client/components/window/BasicWindow";
import useProperty from "client/hooks/useProperty";
import { getAsset } from "shared/asset/AssetMap";
import { playSound } from "shared/asset/GameAssets";
import simpleProfile from "shared/hamster/simpleProfile";
import type Item from "shared/item/Item";
import Items from "shared/items/Items";
import Packets from "shared/Packets";

/**
 * Calculate optimal cell count for the inventory grid.
 */
function calculateOptimalCellCount(containerX: number): number {
    return math.max(math.round((containerX - 50) / 65), 3);
}

type InventorySlotData = ItemFilterData & {
    amount?: number;
    bonusAmount?: number;
    uuid?: string;
};

export function getBestUniqueInstances(uniqueInstances: Map<string, UniqueItemInstance>) {
    // Find the best unique item instance for the given base item ID
    const bestUuidPerItem = new Map<string, string>();
    const bestInstancePerItem = new Map<string, UniqueItemInstance>();
    const lastBestPotsPerItem = new Map<string, number>();
    for (const [uuid, instance] of uniqueInstances) {
        if (instance.placed) continue; // Skip placed instances
        let thisPots = 0;
        for (const [_, potValue] of instance.pots) {
            thisPots += potValue;
        }

        const lastBestPots = lastBestPotsPerItem.get(instance.baseItemId);
        if (lastBestPots === undefined || thisPots > lastBestPots) {
            lastBestPotsPerItem.set(instance.baseItemId, thisPots);
            bestUuidPerItem.set(instance.baseItemId, uuid);
            bestInstancePerItem.set(instance.baseItemId, instance);
        }
    }
    return bestUuidPerItem;
}

/**
 * Handle item activation for placement in the game world.
 * This method encapsulates the business logic for item placement.
 *
 * @param item The item to activate/place
 * @returns True if the item was successfully activated, false otherwise
 */
export function activateItem(item: Item): boolean {
    const isPlaceable = item.placeableAreas.size() > 0 || item.bounds !== undefined;
    const level = Packets.level.get();

    // Check restrictions
    if (BuildManager.getRestricted() === true || BuildManager.hasSelection() === true || isPlaceable === false) {
        playSound("Error.mp3");
        showErrorToast("Can't place that item right now.");
        return false;
    }

    if (item.levelReq !== undefined && item.levelReq > level) {
        playSound("Error.mp3");
        showErrorToast(`You need to be Lv. ${item.levelReq} to place that item.`);
        return false;
    }

    playSound("MenuClick.mp3");

    // Find best unique instance if applicable
    let bestUuid: string | undefined;
    if (Items.uniqueItems.has(item)) {
        bestUuid = getBestUniqueInstances(Packets.uniqueInstances.get() ?? new Map()).get(item.id);
    }

    // Add placing model and select it
    BuildManager.mainSelect(
        BuildManager.addPlacingModel({
            item: item.id,
            uniqueItemId: bestUuid,
            posX: 0,
            posY: 0,
            posZ: 0,
            rotX: 0,
            rotY: 0,
            rotZ: 0,
        }),
    );

    return true;
}

const NON_GEAR_ITEMS = Items.sortedItems.filter((item) => !item.findTrait("Gear"));
const NON_GEAR_ITEMS_SET = new Set(NON_GEAR_ITEMS);

/**
 * Main inventory window component following the QuestWindow pattern
 */
export default function InventoryWindow() {
    const { id, visible, closeDocument } = useSingleDocument({ id: "Inventory" });
    const { searchQuery, props: filterProps } = useBasicInventoryFilter();
    const [cellSize, setCellSize] = useState(new UDim2(0, 65, 0, 65));
    const cellSizeRef = useRef(cellSize);
    useEffect(() => {
        cellSizeRef.current = cellSize;
    }, [cellSize]);

    // Refs for filter animation
    const searchBoxRef = useRef<TextBox>();
    const filterFrameRef = useRef<Frame>();
    const filterButtonRefs = useRef(new Map<string, ImageButton>());
    const [previousVisible, setPreviousVisible] = useState(visible);

    // Observe inventory data from packets
    const inventory = useProperty(Packets.inventory) ?? new Map<string, number>();
    const researching = useProperty(Packets.researching) ?? new Map<string, number>();
    const uniqueInstances = useProperty(Packets.uniqueInstances) ?? new Map<string, UniqueItemInstance>();
    const challengeItemBonuses = useProperty(Packets.challengeItemBonuses) ?? new Map<string, number>();

    const scrollingFrameRef = useRef<ScrollingFrame>();
    const itemSlotsRef = useRef(new Map<string, InventorySlotHandle>());

    // Cascading animation for filter elements
    useEffect(() => {
        const action = visible && !previousVisible ? "open" : !visible && previousVisible ? "close" : undefined;
        if (action) {
            const searchBox = searchBoxRef.current;
            const filterButtons = filterButtonRefs.current;

            if (action === "open") {
                // Animate search box first
                if (searchBox) {
                    searchBox.Rotation = -3;
                    const searchTween = TweenService.Create(
                        searchBox,
                        new TweenInfo(0.4, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
                        { Rotation: 0 },
                    );
                    searchTween.Play();
                }

                // Animate each filter button individually with cascading delays
                task.spawn(() => {
                    for (let index = 0; index < TRAIT_OPTIONS.size(); index++) {
                        const trait = TRAIT_OPTIONS[index];
                        const button = filterButtons.get(trait.id);
                        if (button) {
                            button.Position = new UDim2(0.5, 0, 0.5, -8);
                            const buttonTween = TweenService.Create(
                                button,
                                new TweenInfo(0.35 + index * 0.04, Enum.EasingStyle.Back, Enum.EasingDirection.Out),
                                { Position: new UDim2(0.5, 0, 0.5, 0) },
                            );
                            buttonTween.Play();
                        }
                    }
                });
            } else {
                // Reset rotation on close
                if (searchBox) {
                    const searchTween = TweenService.Create(searchBox, new TweenInfo(0.1, Enum.EasingStyle.Linear), {
                        Rotation: -3,
                    });
                    searchTween.Play();
                }
            }
        }
        setPreviousVisible(visible);
    }, [visible]);

    // Handle item activation
    const handleItemActivated = useCallback(
        (item: Item) => {
            const success = activateItem(item);
            if (success) {
                closeDocument();
            }
        },
        [closeDocument],
    );
    const handleItemActivatedRef = useRef(handleItemActivated);
    useEffect(() => {
        handleItemActivatedRef.current = handleItemActivated;
    }, [handleItemActivated]);

    // Calculate cell size based on container width
    const updateCellSize = useCallback((width: number) => {
        const optimalCellCount = calculateOptimalCellCount(width);
        const newCellSize = new UDim2(1 / optimalCellCount, -12, 1, 0);
        setCellSize(newCellSize);
    }, []);

    // Monitor size changes
    useEffect(() => {
        const frame = scrollingFrameRef.current;
        if (frame) {
            const updateSize = () => {
                updateCellSize(frame.AbsoluteSize.X);
            };

            // Initial size update
            updateSize();

            // Listen for size changes
            const connection = frame.GetPropertyChangedSignal("AbsoluteSize").Connect(updateSize);
            return () => connection.Disconnect();
        }
    }, [updateCellSize]);

    const { dataPerItem, hasOwnedItems, hasFilteredItems } = useMemo(() => {
        const amountsPerItem = new Map<string, number>();
        for (const [itemId, amount] of inventory) {
            amountsPerItem.set(itemId, amount);
        }
        for (const [, uniqueInstance] of uniqueInstances) {
            const itemId = uniqueInstance.baseItemId;
            if (uniqueInstance.placed) continue; // Skip placed instances
            amountsPerItem.set(itemId, (amountsPerItem.get(itemId) ?? 0) + 1);
        }

        let ownsAnyItems = false;
        for (const [, amount] of amountsPerItem) {
            if (amount > 0) {
                ownsAnyItems = true;
                break;
            }
        }

        const dataPerItem: Map<string, InventorySlotData> = filterItems(
            NON_GEAR_ITEMS_SET,
            searchQuery,
            filterProps.traitFilters,
        );

        const bestInstancePerItem = getBestUniqueInstances(uniqueInstances);
        let anyFilteredVisible = false;
        for (const [id, data] of dataPerItem) {
            const reserved = researching.get(id) ?? 0;
            const amount = math.max((amountsPerItem.get(id) ?? 0) - reserved, 0);
            const bonusAmount = challengeItemBonuses.get(id) ?? 0;
            data.amount = amount;
            data.bonusAmount = bonusAmount;
            if (amount + bonusAmount <= 0) {
                data.visible = false;
            }

            if (data.visible === true) {
                anyFilteredVisible = true;
            }

            const bestInstance = bestInstancePerItem.get(id);
            if (bestInstance !== undefined) {
                data.uuid = bestInstance;
            }
        }
        return { dataPerItem, hasOwnedItems: ownsAnyItems, hasFilteredItems: anyFilteredVisible };
    }, [inventory, uniqueInstances, researching, challengeItemBonuses, searchQuery, filterProps.traitFilters]);

    useEffect(() => {
        const frame = scrollingFrameRef.current;
        if (!frame) return;

        const slots = itemSlotsRef.current;
        for (const item of NON_GEAR_ITEMS) {
            if (slots.has(item.id)) continue;

            const slot = createInventorySlot(item, {
                parent: frame,
                size: cellSizeRef.current,
                layoutOrder: item.layoutOrder,
                visible: false,
                onActivated: () => handleItemActivatedRef.current(item),
            });
            slots.set(item.id, slot);
        }

        return () => {
            const slots = itemSlotsRef.current;
            for (const [, slot] of slots) {
                slot.destroy();
            }
            slots.clear();
        };
    }, []);

    useEffect(() => {
        const slots = itemSlotsRef.current;
        for (const [, slot] of slots) {
            updateInventorySlot(slot, { size: cellSize });
        }
    }, [cellSize]);

    useEffect(() => {
        const slots = itemSlotsRef.current;

        for (const item of NON_GEAR_ITEMS) {
            const slot = slots.get(item.id);
            if (slot === undefined) continue;

            const slotData = dataPerItem.get(item.id);
            const layoutOrder = slotData === undefined ? item.layoutOrder : -slotData.layoutOrder;
            updateInventorySlot(slot, {
                layoutOrder,
                visible: slotData?.visible === true,
                amount: slotData?.amount ?? 0,
                bonusAmount: slotData?.bonusAmount ?? 0,
                uuid: slotData?.uuid,
            });
        }
    }, [dataPerItem]);

    const isEmpty = !hasOwnedItems; // Only show empty state if user has no items at all

    return (
        <BasicWindow
            icon={getAsset("assets/Inventory.png")}
            id={id}
            backgroundColor={
                new ColorSequence([
                    new ColorSequenceKeypoint(0, Color3.fromRGB(69, 51, 36)),
                    new ColorSequenceKeypoint(1, Color3.fromRGB(56, 28, 28)),
                ])
            }
            strokeColor={
                new ColorSequence([
                    new ColorSequenceKeypoint(0, Color3.fromRGB(255, 186, 125)),
                    new ColorSequenceKeypoint(1, Color3.fromRGB(255, 123, 123)),
                ])
            }
            visible={visible}
        >
            {/* Empty state */}
            <InventoryEmptyState visible={isEmpty} />

            {/* Main inventory content */}
            <frame BackgroundTransparency={1} Size={new UDim2(1, 0, 1, 0)}>
                {/* Filter options */}
                <InventoryFilter
                    {...filterProps}
                    searchBoxRef={searchBoxRef}
                    filterFrameRef={filterFrameRef}
                    filterButtonRefs={filterButtonRefs}
                />

                {/* Item list container */}
                <scrollingframe
                    ref={scrollingFrameRef}
                    AnchorPoint={new Vector2(0.5, 0)}
                    AutomaticCanvasSize={Enum.AutomaticSize.Y}
                    BackgroundTransparency={1}
                    BorderSizePixel={0}
                    CanvasSize={new UDim2(0, 0, 0, 0)}
                    LayoutOrder={1}
                    Position={new UDim2(0.5, 0, 0, 0)}
                    ScrollBarThickness={12}
                    Selectable={false}
                    Size={new UDim2(1, 0, 0.975, -20)}
                    Visible={hasFilteredItems}
                >
                    <uipadding
                        PaddingBottom={new UDim(0, 5)}
                        PaddingLeft={new UDim(0, 10)}
                        PaddingRight={new UDim(0, 10)}
                        PaddingTop={new UDim(0, 5)}
                    />

                    <uigridlayout
                        CellPadding={new UDim2(0, 12, 0, 12)}
                        CellSize={cellSize}
                        SortOrder={Enum.SortOrder.LayoutOrder}
                    >
                        <uiaspectratioconstraint />
                    </uigridlayout>
                    {/* Item slots are managed imperatively via createInventorySlot */}
                </scrollingframe>

                {/* Layout for filter and item list */}
                <uilistlayout
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    Padding={new UDim(0, 8)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />
            </frame>
        </BasicWindow>
    );
}


import React, { useRef, useState } from "@rbxts/react";
import { TweenService } from "@rbxts/services";
import { RobotoSlabBold } from "shared/asset/GameFonts";

export default function ActionButton({
    text,
    backgroundColor,
    layoutOrder,
    onClick,
}: {
    text: string;
    backgroundColor: Color3;
    layoutOrder?: number;
    onClick?: () => void;
}) {
    const buttonRef = useRef<TextButton>();
    const [isAnimating, setIsAnimating] = useState(false);
    const [isHovered, setIsHovered] = useState(false);

    const handleHoverEnter = () => {
        if (isAnimating || !buttonRef.current) return;

        setIsHovered(true);

        // Smooth scale up and glow effect on hover
        const hoverTween = TweenService.Create(
            buttonRef.current,
            new TweenInfo(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
            {
                Size: new UDim2(0.22, 0, 0.85, 0),
            },
        );

        hoverTween.Play();
    };

    const handleHoverLeave = () => {
        if (isAnimating || !buttonRef.current) return;

        setIsHovered(false);

        // Smooth scale back down
        const unhoverTween = TweenService.Create(
            buttonRef.current,
            new TweenInfo(0.2, Enum.EasingStyle.Quad, Enum.EasingDirection.Out),
            {
                Size: new UDim2(0.2, 0, 0.8, 0),
            },
        );

        unhoverTween.Play();
    };

    const handleClick = () => {
        if (isAnimating || !buttonRef.current) return;

        setIsAnimating(true);

        const tweenInfo = new TweenInfo(0.15, Enum.EasingStyle.Back, Enum.EasingDirection.Out, 0, false, 0);

        // Scale down first
        const scaleDown = TweenService.Create(buttonRef.current, new TweenInfo(0.1), {
            Size: isHovered ? new UDim2(0.2, 0, 0.77, 0) : new UDim2(0.18, 0, 0.72, 0),
        });

        // Then scale back up with bounce
        const scaleUp = TweenService.Create(buttonRef.current, tweenInfo, {
            Size: isHovered ? new UDim2(0.22, 0, 0.85, 0) : new UDim2(0.2, 0, 0.8, 0),
        });

        scaleDown.Play();
        scaleDown.Completed.Connect(() => {
            scaleUp.Play();
            scaleUp.Completed.Connect(() => {
                setIsAnimating(false);
            });
        });

        // Call the original onClick after a small delay
        if (onClick) {
            onClick();
        }
    };

    return (
        <textbutton
            ref={buttonRef}
            AutoButtonColor={false}
            BackgroundColor3={isHovered ? backgroundColor.Lerp(Color3.fromRGB(255, 255, 255), 0.15) : backgroundColor}
            BorderSizePixel={0}
            LayoutOrder={layoutOrder}
            Size={new UDim2(0.2, 0, 0.8, 0)}
            Text={""}
            TextColor3={Color3.fromRGB(0, 0, 0)}
            TextSize={14}
            Event={{
                Activated: handleClick,
                MouseEnter: handleHoverEnter,
                MouseLeave: handleHoverLeave,
            }}
        >
            <uistroke
                ApplyStrokeMode={Enum.ApplyStrokeMode.Border}
                Thickness={2}
                Color={isHovered ? Color3.fromRGB(255, 255, 255) : Color3.fromRGB(0, 0, 0)}
                Transparency={0}
            />
            <textlabel
                AnchorPoint={new Vector2(0.5, 0.5)}
                BackgroundTransparency={1}
                FontFace={RobotoSlabBold}
                Position={new UDim2(0.5, 0, 0.5, 0)}
                Size={new UDim2(0.9, 0, 0.4, 0)}
                Text={text}
                TextColor3={Color3.fromRGB(255, 255, 255)}
                TextScaled={true}
                TextSize={14}
                TextWrapped={true}
            >
                <uistroke Thickness={1.5} />
            </textlabel>
        </textbutton>
    );
}


import React, { JSX } from "@rbxts/react";
import { OnoeNum } from "@rbxts/serikanum";
import SetupOption from "client/components/item/printer/SetupOption";
import TemplateSetup from "client/components/item/printer/TemplateSetup";
import useProperty from "client/hooks/useProperty";
import CurrencyBundle from "shared/currency/CurrencyBundle";
import Packets from "shared/Packets";

export default function PrinterGui({ adornee, areaId }: { adornee: Part; areaId: AreaId }) {
    const printedSetups = useProperty(Packets.printedSetups);

    const setups = new Array<JSX.Element>();
    for (const setup of printedSetups) {
        if (setup.area !== areaId) continue;
        const currencyBundle = new CurrencyBundle();
        for (const [currency, amount] of setup.calculatedPrice) {
            currencyBundle.set(currency, new OnoeNum(amount));
        }
        setups.push(<SetupOption setupName={setup.name} cost={currencyBundle.toString()} />);
    }

    // Generate default name for template: "Setup X" where X is the next number
    const defaultSetupName = `Setup ${printedSetups.size() + 1}`;

    return (
        <surfacegui
            Adornee={adornee}
            ClipsDescendants={false}
            LightInfluence={0.5}
            MaxDistance={200}
            SizingMode={Enum.SurfaceGuiSizingMode.PixelsPerStud}
            ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
            ResetOnSpawn={false}
            Face={Enum.NormalId.Front}
            PixelsPerStud={50}
        >
            <uilistlayout
                FillDirection={Enum.FillDirection.Horizontal}
                HorizontalAlignment={Enum.HorizontalAlignment.Center}
                Padding={new UDim(0.05, 0)}
                SortOrder={Enum.SortOrder.LayoutOrder}
                VerticalAlignment={Enum.VerticalAlignment.Center}
            />
            <uipadding
                PaddingBottom={new UDim(0, 5)}
                PaddingLeft={new UDim(0, 5)}
                PaddingRight={new UDim(0, 5)}
                PaddingTop={new UDim(0, 5)}
            />
            <scrollingframe
                Active={false}
                AutomaticCanvasSize={Enum.AutomaticSize.Y}
                BackgroundTransparency={1}
                Size={new UDim2(1, 0, 1, 0)}
            >
                <uistroke Color={Color3.fromRGB(255, 255, 255)} Thickness={3} />
                <uilistlayout
                    HorizontalAlignment={Enum.HorizontalAlignment.Center}
                    Padding={new UDim(0, 4)}
                    SortOrder={Enum.SortOrder.LayoutOrder}
                />
                {setups}
                <TemplateSetup placementId={adornee.Name} defaultSetupName={defaultSetupName} />
            </scrollingframe>
        </surfacegui>
    );
}


/**
 * @fileoverview UI Labs story for UpgradeBoard components.
 * Provides interactive controls for testing upgrade selection and purchase functionality.
 */

import { OnoeNum } from "@rbxts/serikanum";
import React, { useEffect } from "@rbxts/react";
import ReactRoblox from "@rbxts/react-roblox";
import { Workspace } from "@rbxts/services";
import { CreateReactStory } from "@rbxts/ui-labs";
import PrinterRenderer from "client/components/item/printer/PrinterRenderer";
import Item from "shared/item/Item";
import Items from "shared/items/Items";
import Packets from "shared/Packets";

export = CreateReactStory(
    {
        react: React,
        reactRoblox: ReactRoblox,
    },
    () => {
        useEffect(() => {
            Packets.printedSetups.set([
                {
                    name: "Basic Setup",
                    area: "BarrenIslands",
                    autoloads: false,
                    calculatedPrice: new Map([["Funds", new OnoeNum(5000)]]),
                    items: [],
                    alerted: false,
                },
            ]);
        }, []);

        useEffect(() => {
            const testModels = new Array<Instance>();

            const createTestModel = (item: Item) => {
                const testModel = item.createModel({
                    item: item.id,
                    posX: -testModels.size() * 50,
                    posY: 30,
                    posZ: 0,
                    rotX: 0,
                    rotY: 0,
                    rotZ: 0,
                });
                if (!testModel) return;
                testModel.AddTag("Printer");
                testModel.Parent = Workspace;
                testModels.push(testModel);
            };
            for (const printer of Items.sortedItems) {
                if (printer.findTrait("Printer")) {
                    createTestModel(printer);
                }
            }

            return () => {
                testModels.forEach((model) => model.Destroy());
            };
        }, []);

        return <PrinterRenderer />;
    },
);