--!strict

local EPSILON = 1e-3
local GJK_MAX_ITRS = 20
local MARGIN = 0.05

local Hull = require("../Hull")

local clipHullFaces = require("./clipHullFaces")

--[[
	The incremental strategy here is to just keep on
	holding onto the current normal until we can't clip anymore.
	From testing, this just straight up yields better performance
	than just GJK + clipping
]]

export type Hull = Hull.Hull
export type Face = Hull.Face
export type Support = Hull.Support

export type Contact = {
	sep: number,
	axis: Vector3,

	contactA: { Vector3 },
	contactB: { Vector3 },
}

local function getFaceInDirection(hull: Hull.MeshHull, direction: Vector3): Face
	-- Gets a face pointing most in the specified
	-- direction. Works only for convex stuff
	local bestFace: Face
	local bestDot = -math.huge

	for i, face in hull.faces do
		local dot = face.normal:Dot(direction)
		if bestDot < dot then
			bestFace = face
			bestDot = dot
		end
	end

	return bestFace
end

local function draw(simplex: { Vector3 })
	local length = #simplex
	for i = 1, length do
		local j = (i % length) + 1
		workspace.Wireframe:AddLine(
			simplex[i],
			simplex[j]
		)
	end
end

return function(
	hullA: Hull.MeshHull,
	hullB: Hull.MeshHull,
	lastNormal: Vector3
): Contact?
	debug.profilebegin("narrowphase::stubborn")

	local faceB = getFaceInDirection(hullB, lastNormal)
	local vertexB = hullB.verticies[faceB.verticies[1]]
	local vertexA = hullA.support(-lastNormal)

	--draw({
	--	hullB.verticies[faceB.verticies[1]],
	--	hullB.verticies[faceB.verticies[2]],
	--	hullB.verticies[faceB.verticies[3]],
	--	hullB.verticies[faceB.verticies[4]]
	--})

	local distance = (vertexB - vertexA):Dot(faceB.normal)
	local contact = clipHullFaces(hullB, hullA, faceB, 1e-2)

	debug.profileend()

	if -MARGIN > distance then
		return nil
	elseif 1 < #contact then
		return {
			sep = distance,
			axis = faceB.normal,

			contactA = contact,
			contactB = contact,
		}
	else
		return nil
	end
end


--!native
--!strict

local EPSILON = 1e-2

local getContactPersistent = require("../collision/getContactPersistent")
local getContactManifold = require("../collision/getContactManifold")
local areHullsColliding = require("../collision/areHullsColliding")

local MeshHull = require("./MeshHull")
local Contact = require("../Contact")
local Body = require("../../Body")

export type MeshHull = MeshHull.MeshHull
export type Contact = Contact.Contact
export type Body = Body.Body

local MeshArbiter = {}
MeshArbiter.__index = MeshArbiter

local function hashvec(vec: Vector3): Vector3
	return (vec * 1e3):Floor()
end

export type MeshArbiter = typeof(setmetatable({} :: {
	shapeA: Body,
	shapeB: Body,

	rotA: CFrame,
	rotB: CFrame,

	normal: Vector3,
	contacts: { [Vector3]: Contact },
}, MeshArbiter))

local function getClosestPoint(set: { Vector3 }, to: Vector3): Vector3
	local dotB = math.huge
	local pointB: Vector3
	for j, other in set do
		-- Loop through the set and calculate
		-- the difference. The one with the least
		-- distance is the closest point
		local what = other - to
		local dot = what:Dot(what)
		if dot < dotB then
			-- set closest point
			pointB = other
			dotB = dot
		end
	end

	return pointB
end

function MeshArbiter.new(shapeA: Body, shapeB: Body): MeshArbiter?
	-- Add contacts for A->B
	local originA = shapeA.hull.center
	local originB = shapeB.hull.center

	if not areHullsColliding(
		shapeA.hull.support,
		shapeB.hull.support,
		originB - originA
	) then
		return nil
	end

	local manifold = getContactManifold(
		shapeA.hull :: MeshHull,
		shapeB.hull :: MeshHull
	)

	if not manifold then
		return nil
	end

	local contacts = {}
	for i, pointA in manifold.contactA do
		-- Get the closest point to apply the impulse
		-- towards
		local pointB = getClosestPoint(manifold.contactB, pointA)
		local rA = pointA - originA
		local rB = pointB - originB

		-- We do this for warm starting, I arbritrarily
		-- chose rA as the hash
		local relative = shapeA.cframe:PointToObjectSpace(pointA)
		contacts[hashvec(relative)] = Contact.new(
			shapeA, shapeB,
			-manifold.axis, -- normal
			rA, -- ra
			rB, -- rb
			manifold.sep -- depth
		)
	end

	return setmetatable({
		shapeA = shapeA,
		shapeB = shapeB,

		normal = -manifold.axis,

		rotA = shapeA.cframe.Rotation:Inverse(),
		rotB = shapeB.cframe.Rotation:Inverse(),

		contacts = contacts
	}, MeshArbiter)
end

function MeshArbiter.updateFromManifold(self: MeshArbiter, manifold: getContactManifold.Contact)
	local shapeA = self.shapeA
	local shapeB = self.shapeB

	-- add contacts for A->B
	local originA = shapeA.hull.center
	local originB = shapeB.hull.center

	local contacts = {}
	for i, pointA in manifold.contactA do
		-- Get the closest point to apply the impulse
		-- towards
		local pointB = getClosestPoint(manifold.contactB, pointA)
		local rA = pointA - originA
		local rB = pointB - originB

		-- We do this for warm starting, I chose rA
		-- as the hash
		local relative = shapeA.cframe:PointToObjectSpace(pointA)
		local id = hashvec(relative)
		local contact = self.contacts[id]
		if contact then
			-- Then, warm start.
			contact:update(rA, rB, -manifold.axis, manifold.sep)
			contacts[id] = contact
			continue
		end

		-- Generate a new contact
		local contact = Contact.new(
			shapeA, shapeB,
			-manifold.axis, -- normal
			rA, -- ra
			rB, -- rb
			manifold.sep -- depth
		)

		contacts[id] = contact
	end

	self.contacts = contacts
end

function MeshArbiter.update(self: MeshArbiter, dt: number): MeshArbiter?
	local shapeA = self.shapeA
	local shapeB = self.shapeB

	local contact = getContactPersistent(
		shapeA.hull :: MeshHull,
		shapeB.hull :: MeshHull,
		-self.normal
	)
	if not contact then
		-- The face contact failed.
		-- So, try doing the brute force.
		local bruteForce = getContactManifold(
			shapeA.hull :: MeshHull,
			shapeB.hull :: MeshHull
		)

		if not bruteForce then
			return nil
		end

		self.normal = -bruteForce.axis
		MeshArbiter.updateFromManifold(self, bruteForce)
	else
		self.normal = -contact.axis
		MeshArbiter.updateFromManifold(self, contact)
	end

	return self
end

function MeshArbiter.solve(self: MeshArbiter, dt: number)
	-- Solve our contacts!
	for i, contact in self.contacts do
		contact:solve(dt)
	end
end

return MeshArbiter


--!native
--!strict

--[[
	Hull based off of a wavefront
	During collision, this uses the SAT
	method for stable contact manifolds
]]

local EPSILON = 1e-3

local Mat3 = require("../../Mat3")

local MeshHull = {}
MeshHull.__index = MeshHull

type Mat3 = Mat3.Mat3

-- canonical inertia tensor
local INERTIA_CANONICAL = Mat3.new(
	2.0 / 120.0, 1.0 / 120.0, 1.0 / 120.0,
	1.0 / 120.0, 2.0 / 120.0, 1.0 / 120.0,
	1.0 / 120.0, 1.0 / 120.0, 2.0 / 120.0
)

export type Support = (Vector3) -> Vector3
export type Face = {
	verticies: { number },
	normal: Vector3,
}

export type Obj = {
	-- used for querying gjk/sat
	support: Support,

	-- center of mass
	center: Vector3,

	-- list of verticies
	verticies: { Vector3 },

	-- center of mass
	centroid: Vector3,

	-- inertia tensor
	inertia: Vector3,

	-- mass
	mass: number,

	-- edge and normal
	normals: { [number]: number },

	-- edges are defined are 2 byte numbers
	edges: { number },

	-- list of faces, `w` = p.n
	faces: { Face },
}

export type MeshHull = typeof(setmetatable({} :: {
	-- used for querying gjk/sat
	support: Support,

	-- center of mass
	center: Vector3,

	-- list of verticies
	verticies: { Vector3 },

	-- edge and normal
	normals: { [number]: number },

	-- edges are defined are 2 byte numbers
	edges: { number },

	-- list of faces, `w` = p.n
	faces: { Face },

	-- base shape
	ref: Obj,
}, MeshHull))

local function getInertiaTensor(
	verticies: { Vector3 },
	faces: { Face },
	centroid: Vector3,
	mass: number
): Vector3
	local inertia = Mat3.fromVector(Vector3.zero)
	for i, face in faces do
		local faceVerts = face.verticies
		local v1 = verticies[faceVerts[1]] - centroid
		local v2 = verticies[faceVerts[2]] - centroid

		for j = 3, #faceVerts do
			local v3 = verticies[faceVerts[j]] - centroid

			-- Create a tetrahedron matrix
			local tetrahedron = Mat3.fromMatrix(v1, v2, v3)
			local det = Mat3.det(tetrahedron)

			local covar = Mat3.matMul(tetrahedron, Mat3.matMul(INERTIA_CANONICAL, Mat3.transpose(tetrahedron)))
			inertia = Mat3.matAdd(inertia, covar)

			-- Make sure that v2 = verts[j - 1]
			v2 = v3
		end
	end

	-- Construct our final inertia tensor
	return Vector3.new(
		inertia[1][1] - INERTIA_CANONICAL[1][1],
		inertia[2][2] - INERTIA_CANONICAL[2][2],
		inertia[3][3] - INERTIA_CANONICAL[3][3]
	)
end

local function getCenterVolume(
	verticies: { Vector3 },
	faces: { Face }
): (Vector3, number)
	-- First, calculate the center point
	-- of the entire shape
	local centroid = Vector3.zero
	for i, face in faces do
		local faceVerts = face.verticies
		local center = Vector3.zero

		for j, vertex in faceVerts do
			center += verticies[vertex]
		end

		centroid += center / #faceVerts
	end

	-- Gathered our centroid!
	centroid /= #faces

	-- Go through the triangle and
	-- create a tetrahedron from the center
	-- and the triangles of our mesh
	local totCenter = Vector3.zero
	local totVolume = 0.0

	for i, face in faces do
		local faceVerts = face.verticies
		local v1 = verticies[faceVerts[1]]
		local v2 = verticies[faceVerts[2]]

		for j = 3, #faceVerts do
			local v3 = verticies[faceVerts[j]]

			-- Get the verticies and then we
			-- calculate the volume of the tetrahedron
			local volume = (v1 - centroid):Dot((v2 - centroid):Cross(v3 - centroid))
			local center = (v1 + v2 + v3 + centroid) * 0.25

			totVolume += volume
			totCenter += center * volume

			-- Make sure that v2 = verts[j - 1]
			v2 = v3
		end
	end

	return totCenter / totVolume, totVolume / 6.0
end

local function empty(v: Vector3): Vector3
	-- Empty support vector for
	-- optimization purposes
	return Vector3.zero
end

local function raycastTriangle(
	tri: { Vector3 },
	origin: Vector3,
	direction: Vector3
): (number, Vector3, Vector3)
	-- https://en.wikipedia.org/wiki/Möller–Trumbore_intersection_algorithm
	local a = tri[1]
	local b = tri[2]
	local c = tri[3]

	local ab = b - a
	local ac = c - a

	local rayAC = direction:Cross(ac)
	local det = ab:Dot(rayAC)
	if math.abs(det) < EPSILON then
		-- The ray is parallel to the triangle
		return 1, Vector3.zero, Vector3.zero
	end

	local invDet = 1.0 / det
	local s = origin - a
	local u = invDet * s:Dot(rayAC)

	if (u > 1 and math.abs(u - 1) > EPSILON)
	or (u < 0 and math.abs(u) > EPSILON) then
		-- First case, u is out of bounds
		return 1, Vector3.zero, Vector3.zero
	end

	local se = s:Cross(ab)
	local v = invDet*direction:Dot(se)
	if (u + v > 1 and math.abs(u + v - 1) > EPSILON)
	or (v < 0 and math.abs(v) > EPSILON) then
		-- Second case, v is out of bounds
		return 1, Vector3.zero, Vector3.zero
	end

	local t = invDet*ac:Dot(se)
	if t > EPSILON then
		-- The ray hits the triangle
		return t,
			origin+direction*t,
			ab:Cross(ac).Unit
	else
		-- the ray does not hit the triangle
		return 1, Vector3.zero, Vector3.zero
	end
end

local function createCentroid(verticies: { Vector3 }): Vector3
	-- Calculate center from point cloud
	local centroid = Vector3.zero
	for i, vertex in verticies do
		centroid += vertex
	end

	return centroid / #verticies
end

local function createSupportQuery(verticies: { Vector3 }): Support
	return function(direction: Vector3): Vector3
		-- get the vertex with the highest dot
		-- product with the direction
		local bestDot = -math.huge
		local bestItr = 0

		for i, point in verticies do
			local dot = point:Dot(direction)
			if bestDot < dot then
				bestDot = dot
				bestItr = i
			end
		end

		return verticies[bestItr]
	end
end

function MeshHull.queryEdge(hullA: MeshHull, edge: number): (Vector3, Vector3)
	-- Query edges of the verticies
	return hullA.verticies[bit32.band(edge, 0xFFFF)],
		hullA.verticies[bit32.rshift(edge, 16)]
end

function MeshHull.update(
	self: MeshHull,
	cframe: CFrame,
	size: Vector3
)
	-- Clones a base hull, good for parts
	local ref = self.ref

	local centroid = Vector3.zero
	local verticies = self.verticies
	local faces = self.faces

	for i, vertex in ref.verticies do
		-- Create our own vertex since it changed
		local point = cframe:PointToWorldSpace(vertex * size)
		centroid += point
		verticies[i] = point
	end

	local invSize = Vector3.one / size
	for i, face in ref.faces do
		-- Make a new normal but preserve
		-- verticies table
		local normal = cframe:VectorToWorldSpace(face.normal * invSize)
		faces[i].normal = normal.Unit
	end

	self.center = cframe.Position
end

function MeshHull.raycast(
	self: MeshHull,
	origin: Vector3,
	direction: Vector3
): (number, Vector3, Vector3)
	local verticies = self.verticies
	local temporary = table.create(3, Vector3.zero)

	local bestNormal = Vector3.zero
	local bestT = 1.0

	for i, face in self.faces do
		local verts = face.verticies
		local numVerts = #verts

		for j = 1, numVerts - 1, 2 do
			-- We assume that the convex face creates
			-- a triangle fan, so we just keep on testing
			-- if our ray hits the triangle.
			temporary[3] = verticies[verts[((j + 1) % numVerts) + 1]]
			temporary[2] = verticies[verts[(j % numVerts) + 1]]
			temporary[1] = verticies[verts[j]]

			local t, position, normal = raycastTriangle(
				temporary,
				origin,
				direction
			)

			if t < bestT then
				bestNormal = normal
				bestT = t
			end
		end
	end

	return bestT, origin + direction * bestT, bestNormal
end

function MeshHull.new(
	ref: Obj,
	cframe: CFrame,
	size: Vector3
): MeshHull
	local faces = {}
	local refFaces = ref.faces
	for i = 1, #ref.faces do
		faces[i] = {
			verticies = refFaces[i].verticies,
			normal = Vector3.zero,
		}
	end

	local verticies = table.create(#ref.verticies, Vector3.zero) :: { Vector3 }
	local self = setmetatable({
		ref = ref,
		support = empty,
		center = cframe.Position,
		verticies = verticies,
		normals = ref.normals,
		edges = ref.edges,
		faces = faces :: { Face },
	}, MeshHull)

	function self.support(direction: Vector3): Vector3
		-- Get the vertex with the highest dot
		-- product with the direction.
		local bestDot = -math.huge
		local bestItr = 0

		for i, point in verticies do
			local dot = point:Dot(direction)
			if bestDot < dot then
				bestDot = dot
				bestItr = i
			end
		end

		return verticies[bestItr]
	end

	self:update(cframe, size)
	return self
end

function MeshHull.fromObj(obj: string): Obj
	-- Create a hull from an OBJ
	local edgeNormals = {} :: { [number]: number }
	local verticies = {} :: { Vector3 }
	local normals = {} :: { Vector3 }
	local edges = {} :: { number }
	local faces = {} :: { Face }

	for i, line in obj:split('\n') do
		local token = line:gmatch("[^%s/]+")
		local command = token()

		if command == 'f' then
			-- face
			local normal = Vector3.zero
			local verts = {} :: { number }

			repeat
				local vertex = token()
				if not vertex then
					-- early break incase
					break
				end

				local _uv = tonumber(token())
				normal += normals[tonumber(token()) :: number]
				table.insert(verts, tonumber(vertex) :: number)
			until not vertex

			local faceId = #faces + 1
			local length = #verts
			normal = (normal / length).Unit

			local k = length
			for j = 1, length do
				-- add an edge
				local edge = bit32.lshift(verts[k], 16) + verts[j]
				edgeNormals[edge] = faceId

				table.insert(edges, edge)
				k = j
			end

			table.insert(faces, {
				verticies = verts,
				normal = normal,
			})
		elseif command == 'v' then
			-- vertex
			table.insert(verticies, Vector3.new(
				tonumber(token()) :: number,
				tonumber(token()) :: number,
				tonumber(token()) :: number
			))
		elseif command == 'vn' then
			-- vertex normal
			table.insert(normals, Vector3.new(
				tonumber(token()) :: number,
				tonumber(token()) :: number,
				tonumber(token()) :: number
			))
		end
	end

	local centroid, mass = getCenterVolume(verticies, faces)
	local inertia = getInertiaTensor(verticies, faces, centroid, mass)

	return table.freeze({
		support = createSupportQuery(verticies),
		center = createCentroid(verticies),
		normals = edgeNormals,

		centroid = centroid,
		inertia = inertia,
		mass = mass,

		verticies = verticies,
		edges = edges,
		faces = faces,
	}) :: Obj
end

return MeshHull


--!native
--!strict

local getContactGjk = require("../collision/getContactGjk")
local getContactEpa = require("../collision/getContactEpa")

local Contact = require("../Contact")
local Body = require("../../Body")

export type Contact = Contact.Contact
export type Body = Body.Body

local SupportArbiter = {}
SupportArbiter.__index = SupportArbiter

export type SupportArbiter = typeof(setmetatable({} :: {
	shapeA: Body,
	shapeB: Body,

	rotA: CFrame,
	rotB: CFrame,

	normal: Vector3,
	contact: Contact,
}, SupportArbiter))

function SupportArbiter.new(shapeA: Body, shapeB: Body): SupportArbiter?
	local manifold = getContactGjk(shapeA.hull, shapeB.hull)
	if not manifold then
		-- No contact possible, so do not
		-- use this.
		return nil
	end

	-- add contacts for A->B
	local originA = shapeA.hull.center
	local originB = shapeB.hull.center

	local pointB = manifold.contactB[1]
	local pointA = manifold.contactA[1]

	local rA = pointA - originA
	local rB = pointB - originB

	local contact = Contact.new(
		shapeA, shapeB,
		-manifold.axis, -- normal
		rA, -- ra
		rB, -- rb
		manifold.sep -- depth
	)

	return setmetatable({
		shapeA = shapeA,
		shapeB = shapeB,

		normal = -manifold.axis,

		rotA = shapeA.cframe.Rotation:Inverse(),
		rotB = shapeB.cframe.Rotation:Inverse(),

		contact = contact,
	}, SupportArbiter)
end

function SupportArbiter.update(self: SupportArbiter, dt: number): SupportArbiter?
	local shapeA = self.shapeA
	local shapeB = self.shapeB

	local manifold = getContactGjk(shapeA.hull, shapeB.hull)
	if not manifold then
		-- No contact possible, so do not
		-- use this.
		return nil
	end

	-- add contacts for A->B
	local originA = shapeA.hull.center
	local originB = shapeB.hull.center

	local pointB = manifold.contactB[1]
	local pointA = manifold.contactA[1]

	local rA = pointA - originA
	local rB = pointB - originB

	self.contact:update(rA, rB, -manifold.axis, manifold.sep)
	return self
end

function SupportArbiter.solve(self: SupportArbiter, dt: number)
	-- Solve our contacts!
	self.contact:solve(dt)
end

return SupportArbiter


--!native
--!strict

--[[
	Hull that uses support points instead of mesh
	stuff. Uses EPA/GJK internally.

	TODO: adapt the inertia tensor and volume calculations
	algorithms through support points! Could possibly sample
	the convex hull of the support points, then construct from there.
]]

local Mpr = require("../Mpr")

export type Support = (Vector3) -> Vector3

local SupportHull = {}
SupportHull.__index = SupportHull

export type SupportHull = typeof(setmetatable({} :: {
	ref: (CFrame, Vector3) -> (Support),
	center: Vector3,
	support: Support,
}, SupportHull))

local function rayPlane(p: Vector3, v: Vector3, s: Vector3, n: Vector3)
	local r = p - s;
	local t = -r:Dot(n) / v:Dot(n)
	return p + t * v, t
end

local function createBallSupport(cframe: CFrame, halfSize: Vector3)
	local position = cframe.Position
	local radius = math.min(halfSize.X, halfSize.Y, halfSize.Z)

	return function(direction: Vector3): Vector3
		return direction.Unit * radius + position
	end
end

local function createBlockSupport(cframe: CFrame, halfSize: Vector3)
	return function(direction: Vector3): Vector3
		local delta = cframe:VectorToObjectSpace(direction)
		return cframe * (delta:Sign() * halfSize)
	end
end

local function createWedgeSupport(cframe: CFrame, halfSize: Vector3)
	return function(direction: Vector3)
		local delta = cframe:VectorToObjectSpace(direction)
		local sign = delta:Sign()
		if sign.Z < 1.0 then
			sign *= Vector3.new(1, -math.sign(sign.Y), 1)
		end

		return cframe * (sign * halfSize)
	end
end

local function createCylinderSupport(cframe: CFrame, halfSize: Vector3)
	local radius = math.min(halfSize.Y, halfSize.Z)
	return function(direction: Vector3)
		direction = cframe:VectorToObjectSpace(direction)

		local dotT, cPoint = direction.X, Vector3.new(halfSize.X, 0, 0)
		local h, t, final

		if dotT == 0 then
			final = direction.Unit * radius
		else
			cPoint = dotT > 0 and cPoint or -cPoint
			h, t = rayPlane(Vector3.zero, direction, cPoint, Vector3.xAxis)
			final = cPoint + (h - cPoint).Unit * radius
		end

		return cframe:PointToWorldSpace(final)
	end
end

local PartTypes = table.freeze({
	[Enum.PartType.Ball] = createBallSupport,
	[Enum.PartType.Wedge] = createWedgeSupport,
	[Enum.PartType.Cylinder] = createCylinderSupport,
})

function SupportHull.new(
	partType: Enum.PartType,
	cframe: CFrame,
	size: Vector3
): SupportHull
	return setmetatable({
		support = PartTypes[partType](cframe, size * 0.5),
		center = cframe.Position,
		ref = PartTypes[partType],
	}, SupportHull)
end

function SupportHull.update(
	self: SupportHull,
	cframe: CFrame,
	size: Vector3
)
	self.support = self.ref(cframe, size * 0.5)
	self.center = cframe.Position
end

function SupportHull.raycast(
	self: SupportHull,
	origin: Vector3,
	direction: Vector3
): (number, Vector3, Vector3)
	local hitT, hitNormal = Mpr(
		origin,
		self.support,
		direction
	)

	return hitT, origin + direction * hitT, hitNormal
end

return SupportHull


-- Stoway Server Operation Template
-- Copy this file to src/server/StowayServerV1_2/Operations/{{OperationName}}.luau
-- See docs/guides/add-server-features.html for complete instructions

--// ═══════════════════════════════════════════════════════════════════════════
--// {{OperationName}}.luau
--// Description: [Add description of what this operation does]
--// ═══════════════════════════════════════════════════════════════════════════

local {{OperationName}} = {}

--// Dependencies
local SlotManager = require(script.Parent.Parent.Core.SlotManager)
local LimitChecker = require(script.Parent.Parent.Core.LimitChecker)
local UUID = require(script.Parent.Parent.Utils.UUID)

--// Types
export type {{OperationName}}Result = {
	success: boolean,
	reason: string?,
	-- Add your result fields here:
	-- field1: type1?,
	-- field2: type2?,
}

--// ═══════════════════════════════════════════════════════════════════════════
--// Main Execute Function
--// ═══════════════════════════════════════════════════════════════════════════

function {{OperationName}}.Execute(
	state: InventoryStateType,
	-- Add your parameters here:
	param1: string,    -- Replace with actual parameter type
	param2: number     -- Replace with actual parameter type
): {{OperationName}}Result
	--// 1. Validate Inputs
	--------------------------------
	if not param1 then
		return { success = false, reason = "INVALID_PARAM1" }
	end

	--// 2. Perform Operation
	--------------------------------
	-- TODO: Add your operation logic here
	-- Examples of common operations:

	-- Find a slot:
	-- local slotType, slotIndex = SlotManager.FindSlotByUUID(state, uuid)

	-- Find empty hotbar slot:
	-- local emptySlot = SlotManager.FindEmptyHotbarSlot(state)

	-- Check weight limit:
	-- if LimitChecker.IsEnabled(state) then
	--     local remaining = LimitChecker.GetRemainingCapacity(state)
	--     if remaining <= 0 then
	--         return { success = false, reason = "FULL" }
	--     end
	-- end

	--// 3. Modify State
	--------------------------------
	-- TODO: Update state.Items, state.Hotbar, state.Storage as needed
	-- Example: state.Items[uuid] = { UUID = uuid, Id = "item", Amount = 1 }

	--// 4. Return Result
	--------------------------------
	return {
		success = true,
		-- Add your result fields here:
		-- field1 = value1,
		-- field2 = value2,
	}
end

return {{OperationName}}

--// ═══════════════════════════════════════════════════════════════════════════
--// QUICK REFERENCE: Common State Operations
--// ═══════════════════════════════════════════════════════════════════════════

--[[
-- Access Items by UUID (O(1)):
local item = state.Items[uuid]

-- Access Items by ID (for stacking):
local uuids = state.ItemsByID["sword"]  -- returns {uuid1, uuid2, ...}

-- Get hotbar slot:
local uuid = state.Hotbar[slotIndex]

-- Get storage slot:
local uuid = state.Storage[index]

-- Set hotbar slot:
SlotManager.SetHotbarSlot(state, slotIndex, uuid)

-- Append to storage:
SlotManager.AppendStorage(state, uuid)

-- Update weight:
LimitChecker.AddWeight(state, amount)
LimitChecker.RemoveWeight(state, amount)

-- Check if item is equipped:
local isEquipped = state.EquippedItemUUID == uuid
--]]

--// ═══════════════════════════════════════════════════════════════════════════
--// WIRING INSTRUCTIONS (Copy to docs/guides/add-server-features.html reference)
--// ═══════════════════════════════════════════════════════════════════════════

--[[
STEP 1: Add to init.luau imports
-------------------------------
local {{OperationName}} = require(script.Operations.{{OperationName}})

STEP 2: Add API method to InventoryService
-----------------------------------------
function InventoryService.{{OperationName}}(player, param1, param2, REPLICATE)
    local state = PlayerInventories[player]
    if not state then return { success = false, reason = "NO_INVENTORY" } end

    local result = {{OperationName}}.Execute(state, param1, param2)

    if result.success and REPLICATE then
        -- Add replication if needed:
        -- Replicator.SendSomething(player, ...)
    end

    return result
end

STEP 3: Wire to network protocol
--------------------------------
-- In Operations table:
[Operations.{{OPERATION_NAME}}] = "{{OperationName}}"

-- In OperationHandlers table:
[Operations.{{OPERATION_NAME}}] = function(player, args)
    return InventoryService.{{OperationName}}(player, args.param1, args.param2, false)
end
--]]


-- src/client/Inventory/InventoryStore.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Fusion = require(ReplicatedStorage.Packages.fusion)
local Settings = require(ReplicatedStorage.Shared.Settings)
local Types = require(ReplicatedStorage.Shared.Types)

local Scope = Fusion.scoped(Fusion)
local peek = Fusion.peek

--// 1. Define the Atoms (The raw data)
local Store = {
    Items = Scope:Value({}),        -- {[uuid] = ItemData}
    Hotbar = Scope:Value({}),       -- {[slotIndex] = uuid}
    Storage = Scope:Value({}),      -- {[slotIndex] = uuid} (Array or Dict)
    EquippedUUID = Scope:Value(nil),
    Weight = Scope:Value(0),
    Settings = Scope:Value({}),     -- Static settings from server

    -- Expose Scope for cleanup if needed
    Scope = Scope
}

local function ComputeWeightFromItems(items: { [string]: any }): number
    local total = 0
    for _, item in pairs(items) do
        local amount = item and item.Amount
        if type(amount) == "number" then
            total += amount
        end
    end
    return total
end

--// 2. Initialize Function (Called once by Main)
function Store.Init(initialData)
    if not initialData then return end
    local items = initialData.Items or {}

    Store.Items:set(items)
    Store.Hotbar:set(initialData.Hotbar or {})
    Store.Storage:set(initialData.Storage or {})
    Store.EquippedUUID:set(initialData.EquippedUUID or initialData.EquippedItemUUID)

    local weight = initialData.Weight
    if type(weight) ~= "number" then
        weight = ComputeWeightFromItems(items)
    end
    Store.Weight:set(weight)

    Store.Settings:set(initialData.Settings or {})
end

function Store:GetState()
    return {
        Hotbar = peek(Store.Hotbar),
        Storage = peek(Store.Storage),
        Items = peek(Store.Items),
        Equipped = peek(Store.EquippedUUID)
    }
end

function Store:GetHotbar()
    return peek(Store.Hotbar)
end

function Store:GetStorage()
    return peek(Store.Storage)
end

function Store:GetEquippedUUID()
    return peek(Store.EquippedUUID)
end

function Store:GetUUidFromIndex(index:number, slottype:string?): nil | string
    local hotbarcheck
    local storagecheck

    if not slottype then
        hotbarcheck = peek(Store.Hotbar)[index]
        storagecheck = peek(Store.Storage)[index]

        if hotbarcheck then return hotbarcheck end
        if storagecheck then return storagecheck end

        return nil
    end

    if slottype == "Hotbar" then
        hotbarcheck = peek(Store.Hotbar)[index]
        return hotbarcheck
    end

    if slottype == "Storage" then
        storagecheck = peek(Store.Storage)[index]
        return storagecheck
    end

    return nil
end

function Store:DoesItemExist(uuid:string): Types.ItemData?
    if not uuid then return nil end
    local check = peek(Store.Items)[uuid]
    return check
end

function Store:GetWeightOfUuid(uuid:string) : number | nil
    local item = Store:DoesItemExist(uuid)
    if item then
        return item.Amount
    end

    return nil
end

function Store:GetDroppableStatus(uuid:string) : boolean | nil
    local item = Store:DoesItemExist(uuid)
    if item then
        return item.Metadata.Droppable
    end

    return nil
end

function Store:FindSlotByUUID(uuid: string): (string?, number?)
    if not uuid then return nil, nil end

    -- Check Hotbar first
    local hotbar = peek(Store.Hotbar)
    for slotIndex, slotUUID in pairs(hotbar) do
        if slotUUID == uuid then
            return "Hotbar", slotIndex
        end
    end

    -- Check Storage
    local storage = peek(Store.Storage)
    for slotIndex, slotUUID in ipairs(storage) do
        if slotUUID == uuid then
            return "Storage", slotIndex
        end
    end

    return nil, nil
end

-- Get item data from a specific slot
function Store:GetItemFromSlot(slotType: string, slotIndex: number): Types.ItemData?
    local uuid = Store:GetUUidFromIndex(slotIndex, slotType)
    if not uuid then return nil end
    return peek(Store.Items)[uuid]
end

-- Get max hotbar slots from settings
function Store:GetMaxHotbarSlots(): number
    local settings = peek(Store.Settings)
    return (settings and settings.MaxHotbarSlots) or 10
end

function Store.CreateSlotFrameName(slotnumber:number): string
    local numberstring = tostring(slotnumber) :: string
    local slotframeName = ""
    local len = numberstring:len()

    if len > 1 then
        for _ = 1, (len - 1) do
            slotframeName ..= "_"
        end
    end

    slotframeName ..= numberstring
    return slotframeName
end

-- Build a packed view of occupied hotbar slots without mutating server-auth slot indices.
function Store:GetVisibleHotbarSlotMap(): ({ [number]: number }, { [number]: number })
    local hotbar = peek(Store.Hotbar)
    local items = peek(Store.Items)
    local maxSlots = Store:GetMaxHotbarSlots()

    local visibleToReal = {}
    local realToVisible = {}
    local visibleIndex = 0

    for realSlot = 1, maxSlots do
        local uuid = hotbar[realSlot]
        if uuid and items[uuid] then
            visibleIndex += 1
            visibleToReal[visibleIndex] = realSlot
            realToVisible[realSlot] = visibleIndex
        end
    end

    return visibleToReal, realToVisible
end

function Store:GetHotbarSlotFromVisibleIndex(visibleIndex: number): number?
    local visibleToReal = Store:GetVisibleHotbarSlotMap()
    return visibleToReal[visibleIndex]
end

function Store:GetVisibleHotbarIndexFromSlot(realSlot: number): number?
    local _visibleToReal, realToVisible = Store:GetVisibleHotbarSlotMap()
    return realToVisible[realSlot]
end

function Store:GetUUidFromVisibleHotbarIndex(visibleIndex: number): string?
    local realSlot = Store:GetHotbarSlotFromVisibleIndex(visibleIndex)
    if not realSlot then return nil end
    return peek(Store.Hotbar)[realSlot]
end

function Store:GetFirstOccupiedHotbarButton(hotbarFrame: Instance?): GuiObject?
    if not hotbarFrame then return nil end

    local firstSlotIndex = nil
    if Settings.Hotbar.Type == "Dynamic" then
        -- Dynamic hotbar is packed; slot 1 is first occupied when any item exists.
        if Store:GetUUidFromIndex(1, "Hotbar") then
            firstSlotIndex = 1
        else
            -- Safety fallback if packed invariant is broken by external mutation.
            firstSlotIndex = Store:GetHotbarSlotFromVisibleIndex(1)
        end
    else
        local maxSlots = Store:GetMaxHotbarSlots()
        for i = 1, maxSlots do
            if Store:GetUUidFromIndex(i, "Hotbar") then
                firstSlotIndex = i
                break
            end
        end
    end

    if not firstSlotIndex then return nil end

    local slotFrame = hotbarFrame:FindFirstChild(Store.CreateSlotFrameName(firstSlotIndex))
    if not slotFrame then return nil end

    local slotButton = slotFrame:FindFirstChild("slotbutton")
    if slotButton and slotButton:IsA("GuiObject") and slotButton.Visible then
        return slotButton
    end

    return nil
end

-- Find first empty hotbar slot
function Store:FindFirstEmptyHotbarSlot(): number?
    local hotbar = peek(Store.Hotbar)
    local items = peek(Store.Items)
    local maxSlots = Store:GetMaxHotbarSlots()

    for i = 1, maxSlots do
        local uuid = hotbar[i]
        if not uuid or not items[uuid] then
            return i
        end
    end

    return nil -- Hotbar full
end

-- Find first available storage slot (next index)
function Store:FindFirstEmptyStorageSlot(): number
    local storage = peek(Store.Storage)
    return #storage + 1
end

return Store


local UserInputService = game:GetService("UserInputService")
local GuiService = game:GetService("GuiService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local InventoryController = require(script.Parent.Parent.Operations.InventoryController)
local InventoryStore = require(script.Parent.Parent.CoreState.InventoryStore)
local fusion = require(ReplicatedStorage.Packages.fusion)
local Settings = require(ReplicatedStorage.Shared.Settings)

local peek = fusion.peek

local DragHandler = {}
local CloneForGhost = nil

local DragState = {
	isDragging = false,
	enabled = false,
	sourceType = nil,
	sourceSlot = nil,
	originalFrame = nil,
	ghostFrame = nil,
}

local DragThreshold = 5

local dragConnection = nil
local releaseConnection = nil
local checkConnection = nil

local DraggingGui = nil
local HotbarFrame = nil
local BackpackFrame = nil
local hookManager = nil

--// Utility: GUI-space mouse position (FIXES OFFSET)
local function GetGuiMousePosition(): Vector2
	local mousePos = UserInputService:GetMouseLocation()
	local inset = GuiService:GetGuiInset()
	return Vector2.new(mousePos.X - inset.X, mousePos.Y - inset.Y)
end

--// Initialize
function DragHandler.Init(draggingGui: ScreenGui, hotbar: Frame, backpack: Frame, hooks: any?, _CloneForGhost)
	DraggingGui = draggingGui
	HotbarFrame = hotbar
	BackpackFrame = backpack
	hookManager = hooks
	CloneForGhost = _CloneForGhost
end

function DragHandler.GetDragState() : {isDragging : boolean,sourceType :any ,sourceSlot : any,originalFrame : any, ghostFrame : any}
	return DragState
end

function DragHandler.SetEnabled(state: boolean)
	DragState.enabled = state
	if not state and DragState.isDragging then
		DragHandler.Destroy()
	end
end

--// Start drag
function DragHandler.StartDrag(slotType: string, slotIndex: number, originalFrame: Frame, FullData--[[, Scope]])
    if not DragState.enabled then return end
    if DragState.isDragging then return end

	-- Validate slot has item
	local uuid
	if slotType == "Hotbar" then
		uuid = peek(FullData).Hotbar[slotIndex]
	else
		uuid = peek(FullData).Storage[slotIndex]
	end

	if not uuid then return end

	local items = peek(FullData).Items
	if not items[uuid] then return end

    DragState.originalFrame = originalFrame
    DragState.sourceType = slotType
    DragState.sourceSlot = slotIndex

    -- Store the starting position to check threshold
    local startPosition = GetGuiMousePosition()

    -- Clone ghost immediately but don't parent yet
    local ghost = CloneForGhost(originalFrame)

    checkConnection = UserInputService.InputChanged:Connect(function(input)
        if input.UserInputType ~= Enum.UserInputType.MouseMovement and input.UserInputType ~= Enum.UserInputType.Touch then return end

        local currentPosition = GetGuiMousePosition()
        local distance = (currentPosition - startPosition).Magnitude

        if distance >= DragThreshold then
            if checkConnection then
                checkConnection:Disconnect()
                checkConnection = nil
            end
            -- Pass the current position to init the ghost
            DragHandler.CreateGhost(originalFrame, currentPosition, ghost)
        end
    end)

    releaseConnection = UserInputService.InputEnded:Connect(function(input)
        if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
             DragHandler.EndDrag(--[[, Scope]])
        end
    end)
end

--// Create ghost frame for dragging
function DragHandler.CreateGhost(originalFrame: Frame, currentMousePos: Vector2, ghost: Frame)
    if not DraggingGui then return end
    if DragState.ghostFrame then return end

    DragState.isDragging = true

    local screenSize = DraggingGui.AbsoluteSize
    local frameSize = originalFrame.AbsoluteSize
    local framePos = originalFrame.AbsolutePosition

    -- 1. CALCULATE OFFSET: How far is the mouse from the top-left of the button?
    -- We use currentMousePos (where the mouse is NOW) vs the Frame's static position
    DragState.mouseOffset = currentMousePos - framePos

    -- 2. SETUP GHOST SIZE
    ghost.Size = UDim2.fromScale(frameSize.X / screenSize.X, frameSize.Y / screenSize.Y)
    ghost.AnchorPoint = Vector2.new(0, 0) -- Ensure AnchorPoint is TopLeft for this math to work

    -- 3. INITIAL POSITION
    -- Target Pixel X = Mouse X - Offset X
    local targetX = currentMousePos.X - DragState.mouseOffset.X
    local targetY = currentMousePos.Y - DragState.mouseOffset.Y

    ghost.Position = UDim2.fromScale(targetX / screenSize.X, targetY / screenSize.Y)
    ghost.Parent = DraggingGui

    DragState.ghostFrame = ghost

    DragHandler.HideOriginalVisuals()

    if hookManager then
        hookManager:Fire("OnDragStart", DragState.sourceType, DragState.sourceSlot)
    end

    -- 4. UPDATE LOOP (RunService is smoother than InputChanged for dragging)
    dragConnection = RunService.RenderStepped:Connect(function()
        if not DragState.ghostFrame then
            DragHandler.EndDrag()
            return
        end

        local mousePos = GetGuiMousePosition()
        local scrSize = DraggingGui.AbsoluteSize

        -- Apply the offset we calculated earlier
        local newX = mousePos.X - DragState.mouseOffset.X
        local newY = mousePos.Y - DragState.mouseOffset.Y

        DragState.ghostFrame.Position = UDim2.fromScale(
            newX / scrSize.X,
            newY / scrSize.Y
        )
    end)
end

--// Hide visuals
function DragHandler.HideOriginalVisuals()
	local frame = DragState.originalFrame
	if not frame then return end

	-- if Settings.Hotbar.Type == "Dynamic" then frame.Visible = false end -- Need access to settings for this?
	-- For now keeping it simple as before
	frame.BackgroundTransparency = 0.95

	local function fade(obj, prop, value)
		if obj then obj[prop] = value end
	end

	if DragState.originalFrame:IsA("ImageLabel") then
		fade(frame:FindFirstChild("slotbutton"), "ImageTransparency", 1)
		fade(frame:FindFirstChild("slotbutton"), "BackgroundTransparency", 1)
		fade(frame:FindFirstChild("stackcount"), "TextTransparency", 0.7)
		fade(frame:FindFirstChild("selecticon"), "ImageTransparency", 1)
		fade(frame:FindFirstChild("rarityframe"), "ImageTransparency", 1)
	end

	fade(frame:FindFirstChild("slotbutton"), "ImageTransparency", 1)
	fade(frame:FindFirstChild("slotbutton"), "BackgroundTransparency", 1)
	fade(frame:FindFirstChild("stackcount"), "TextTransparency", 0.7)
	fade(frame:FindFirstChild("selecticon"), "ImageTransparency", 1)
	fade(frame:FindFirstChild("rarityframe"), "BackgroundTransparency", 1)
end

--// Restore visuals
function DragHandler.RestoreOriginalVisuals()
	local frame = DragState.originalFrame
	if not frame then return end

	-- if Settings.Hotbar.Type == "Dynamic" then frame.Visible = true end
	frame.BackgroundTransparency = 0.8

	local function restore(obj, prop, value)
		if obj then obj[prop] = value end
	end

	if DragState.originalFrame:IsA("Frame") then
		restore(frame:FindFirstChild("slotbutton"), "ImageTransparency", 0)
		restore(frame:FindFirstChild("slotbutton"), "BackgroundTransparency", 0)
		restore(frame:FindFirstChild("stackcount"), "TextTransparency", 0)
		restore(frame:FindFirstChild("selecticon"), "ImageTransparency", 0)
		restore(frame:FindFirstChild("rarityframe"), "BackgroundTransparency", 1)
	end

	if DragState.originalFrame:IsA("ImageLabel") then
		restore(frame:FindFirstChild("slotbutton"), "ImageTransparency", 0)
		restore(frame:FindFirstChild("stackcount"), "TextTransparency", 0)
		restore(frame:FindFirstChild("selecticon"), "ImageTransparency", 0)
		restore(frame:FindFirstChild("rarityframe"), "BackgroundTransparency", 1)
	end
end

--// Hit detection
local function IsMouseInside(gui1, gui2): boolean
    local gui1_topLeft,gui1_bottomRight = gui1.AbsolutePosition, gui1.AbsolutePosition + gui1.AbsoluteSize
	local gui2_topLeft,gui2_bottomRight = gui2.AbsolutePosition, gui2.AbsolutePosition + gui2.AbsoluteSize
	return ((gui1_topLeft.x < gui2_bottomRight.x and gui1_bottomRight.x > gui2_topLeft.x) and (gui1_topLeft.y < gui2_bottomRight.y and gui1_bottomRight.y > gui2_topLeft.y))
end

local function GetHotbarSlotUnderMouse()
	for _, child in ipairs(HotbarFrame:GetChildren()) do
		if child:IsA("Frame") and child.Visible and IsMouseInside(DragState.ghostFrame, child) then
			local slotindex = child.Name:gsub("_", "")
			return child, tonumber(slotindex)
		end

		if child:IsA("ImageLabel") and child.Visible and IsMouseInside(DragState.ghostFrame, child) then
			local slotindex = child.Name:gsub("_", "")
			return child, tonumber(slotindex)
		end
	end

	return nil, nil
end

local function GetBackpackSlotUnderMouse()
	local scrolling = BackpackFrame:FindFirstChild("ScrollingFrame")
	if not scrolling then return end

	for _, child in ipairs(scrolling:GetChildren()) do
		if child:IsA("Frame") and child.Visible and IsMouseInside(DragState.ghostFrame, child) then
            local slotIndex = child.Name:gsub("_", "")
			return child, tonumber(slotIndex)
		end

		if child:IsA("ImageLabel") and child.Visible and IsMouseInside(DragState.ghostFrame, child) then
            local slotIndex = child.Name:gsub("_", "")
			return child, tonumber(slotIndex)
		end
	end

	return nil, nil
end

--// End drag
function DragHandler.EndDrag(--[[, Scope]])
	if checkConnection then checkConnection:Disconnect() end
	if dragConnection then dragConnection:Disconnect() end
	if releaseConnection then releaseConnection:Disconnect() end

	if not DragState.isDragging then
		DragHandler.Reset()
		return
	end

	local hotbarSlot, hotbarIndex = GetHotbarSlotUnderMouse()
	local backpackSlot, backpackIndex = GetBackpackSlotUnderMouse()
	local OverBackpack = IsMouseInside(DragState.ghostFrame, BackpackFrame)
	local OverHotbar = IsMouseInside(DragState.ghostFrame, HotbarFrame)

	-- Logic Execution
	local targetType
	local targetSlot
	local FromType =  DragState.originalFrame.Parent.Name == "Hotbar" and "Hotbar" or "Storage"
	local FromSlot: Frame = DragState.originalFrame
	local index
	-- local index2

	if FromSlot then
        local slotIndex = FromSlot.Name:gsub("_", "")
        index = tonumber(slotIndex)
    end
	-- if targetSlot then targetSlot.Name:gsub("_", ""); index2 = tonumber(targetSlot.Name) end

	if hotbarSlot and hotbarIndex and hotbarSlot ~= FromSlot then
		--swap
		targetType = "Hotbar"
		targetSlot = hotbarSlot

		if FromType == "Hotbar" and FromSlot then
			warn("from hotbar to hotbar swapping ".. FromSlot.Name .. " with " .. hotbarIndex)
			InventoryController.Swap(FromType, index, targetType, hotbarIndex, true)
		else
			warn("from backpack to hotbar swapping ".. FromSlot.Name .. " with " .. hotbarIndex)
			InventoryController.Swap(FromType, index, targetType, hotbarIndex, true)
		end
	end

	if backpackSlot and backpackIndex and backpackSlot ~= FromSlot then
		--swap
		targetType = "Storage"
		targetSlot = backpackSlot

		if FromType == "Hotbar" then
			warn("from hotbar to backpack swapping ".. FromSlot.Name .. " with " .. backpackIndex)
			InventoryController.Swap(FromType, index, targetType, backpackIndex, true)
		else
			warn("from backpack to backpack swapping ".. FromSlot.Name .. " with " .. backpackIndex)
			InventoryController.Swap(FromType, index, targetType, backpackIndex, true)
		end
	end

    -- Dynamic hotbar UX: dropping a storage item over hotbar (not over a visible slot)
    -- inserts into the first empty real hotbar slot.
    if OverHotbar and not hotbarSlot and FromType == "Storage" and Settings.Hotbar.Type == "Dynamic" then
        local firstEmptyHotbar = InventoryStore:FindFirstEmptyHotbarSlot()
        if firstEmptyHotbar then
            InventoryController.Swap(FromType, index, "Hotbar", firstEmptyHotbar, true)
        end
    end

	if OverBackpack and not backpackSlot and FromType == "Hotbar" then
		if  BackpackFrame.Visible then
			warn("append to backpack")
			InventoryController.AddToBackpack(FromType, index, true)
		else
			InventoryController.Drop(FromType, index, nil, true)
			warn("drop")
		end
	end

	if not OverHotbar and not OverBackpack then
		InventoryController.Drop(FromType, index, nil, true)
		warn("drop")
	end

	if hookManager then
		hookManager:Fire(
			"OnDragEnd",
			DragState.sourceType,
			DragState.sourceSlot,
			targetType,
			targetSlot
		)
	end

	DragHandler.Reset()
end

--// Reset
function DragHandler.Reset()
	DragHandler.RestoreOriginalVisuals()

	if DragState.ghostFrame then
		DragState.ghostFrame:Destroy()
	end

    local currentEnabled = DragState.enabled

	DragState = {
		isDragging = false,
		enabled = currentEnabled,
		sourceType = nil,
		sourceSlot = nil,
		originalFrame = nil,
		ghostFrame = nil,
	}
end

--// Cleanup
function DragHandler.Destroy()
	DragHandler.Reset()
	if dragConnection then dragConnection:Disconnect() end
	if releaseConnection then releaseConnection:Disconnect() end
	if checkConnection then checkConnection:Disconnect() end
end

return DragHandler


-- src/client/StowayClientV1_2/Hooks/HookManager.luau
-- UI-specific hook system - each UI skin provides its own hooks

local HookManager = {}
HookManager.__index = HookManager

--// Available hook types
local HOOK_TYPES = {
	OnSlotClick = "OnSlotClick",
	OnSlotHover = "OnSlotHover",
	OnDragStart = "OnDragStart",
	OnDragEnd = "OnDragEnd",
	OnEquip = "OnEquip",
	OnUnequip = "OnUnequip",
	OnItemAdd = "OnItemAdd",
	OnItemRemove = "OnItemRemove",
	OnSwap = "OnSwap",
	OnSelect = "OnSelect",
	OnDeselect = "OnDeselect",
}

--// Create a new HookManager instance for a UI skin
function HookManager.new(): HookManager
	local self = setmetatable({}, HookManager)

	self._hooks = {}

	for _, hookName in pairs(HOOK_TYPES) do
		self._hooks[hookName] = {}
	end

	return self
end

--// Register a hook callback
function HookManager:Register(hookName: string, callback: (...any) -> ())
	if not self._hooks[hookName] then
		warn("[HookManager] Unknown hook:", hookName)
		return
	end
	table.insert(self._hooks[hookName], callback)
end

--// Unregister a specific callback
function HookManager:Unregister(hookName: string, callback: (...any) -> ())
	local list = self._hooks[hookName]
	if not list then return end

	for i, cb in ipairs(list) do
		if cb == callback then
			table.remove(list, i)
			return
		end
	end
end

--// Clear all hooks for a specific type
function HookManager:Clear(hookName: string)
	if self._hooks[hookName] then
		self._hooks[hookName] = {}
	end
end

--// Clear all hooks
function HookManager:ClearAll()
	for hookName in pairs(self._hooks) do
		self._hooks[hookName] = {}
	end
end

--// Fire a hook with arguments
function HookManager:Fire(hookName: string, ...: any)
	local list = self._hooks[hookName]
	if not list then return end

	for _, callback in ipairs(list) do
		task.spawn(callback, ...)
	end
end

--// Get available hook types
function HookManager.GetHookTypes(): { string }
	return table.clone(HOOK_TYPES)
end

--// Create a hook table preset for a UI skin
--// Returns a table of hook functions that can be registered at once
function HookManager:LoadPreset(preset: { [string]: (...any) -> () })
	for hookName, callback in pairs(preset) do
		if self._hooks[hookName] then
			self:Register(hookName, callback)
		else
			warn("[HookManager] Preset contains unknown hook:", hookName)
		end
	end
end

--// Export type
export type HookManager = typeof(HookManager.new())

return HookManager


local Players = game:GetService("Players")
local GuiService = game:GetService("GuiService")

local ConsoleDropUI = {}
local container = nil
local currentAmount = 1
local maxAmount = 1
local dropMenuTemplate = nil

-- References
local numberIndicator = nil
local titleLabel = nil
local decreaseButton = nil
local increaseButton = nil

-- Callbacks (set by SelectionManager)
ConsoleDropUI.OnDrop = nil     -- function(amount)
ConsoleDropUI.OnDropAll = nil  -- function()
ConsoleDropUI.OnCancel = nil   -- function()

function ConsoleDropUI.SetTemplate(template)
    dropMenuTemplate = template
end

function ConsoleDropUI.Create()
    if container then return container end

    local gui = Instance.new("ScreenGui")
    gui.Name = "StowayDropUI"
    gui.ResetOnSpawn = false
    gui.Parent = Players.LocalPlayer:WaitForChild("PlayerGui")
    gui.Enabled = false

    local DropMenu
    local Drop, Dropall, Cancel, Decrease, Increase, NumberIndicator, Title

    if dropMenuTemplate then
        DropMenu = dropMenuTemplate:Clone()
        DropMenu.Parent = gui

        -- Find children in template (assuming names match)
        NumberIndicator = DropMenu:FindFirstChild("NumberIndicator")
        Drop = DropMenu:FindFirstChild("Drop")
        Dropall = DropMenu:FindFirstChild("Dropall")
        Cancel = DropMenu:FindFirstChild("Cancel")
        Decrease = DropMenu:FindFirstChild("Decrease")
        Increase = DropMenu:FindFirstChild("Increase")
        Title = DropMenu:FindFirstChild("Title")
    else
        DropMenu = Instance.new("Frame")
        DropMenu.Name = "DropMenu"
        DropMenu.Size = UDim2.new(0.37, 0, 0.49, 0)
        DropMenu.BorderColor3 = Color3.new(0.00, 0.00, 0.00)
        DropMenu.Position = UDim2.new(0.32, 0, 0.25, 0)
        DropMenu.BorderSizePixel = 0
        DropMenu.BackgroundColor3 = Color3.new(1.00, 1.00, 1.00)
        DropMenu.SelectionGroup = true
        DropMenu.SelectionBehaviorDown = Enum.SelectionBehavior.Stop
        DropMenu.SelectionBehaviorUp = Enum.SelectionBehavior.Stop
        DropMenu.SelectionBehaviorLeft = Enum.SelectionBehavior.Stop
        DropMenu.SelectionBehaviorRight = Enum.SelectionBehavior.Stop
        DropMenu.Parent = gui

        local UICorner = Instance.new("UICorner")
        UICorner.Parent = DropMenu

        local UIGradient = Instance.new("UIGradient")
        UIGradient.Color = ColorSequence.new({
            ColorSequenceKeypoint.new(0.00, Color3.new(0.10, 0.31, 1.00)),
            ColorSequenceKeypoint.new(0.56, Color3.new(0.33, 1.00, 1.00)),
            ColorSequenceKeypoint.new(1.00, Color3.new(0.08, 0.14, 1.00))
        })
        UIGradient.Parent = DropMenu

        NumberIndicator = Instance.new("TextLabel")
        NumberIndicator.Name = "NumberIndicator"
        NumberIndicator.TextWrapped = true
        NumberIndicator.BorderSizePixel = 0
        NumberIndicator.TextScaled = true
        NumberIndicator.BackgroundColor3 = Color3.new(0.19, 0.19, 0.19)
        NumberIndicator.FontFace = Font.new("rbxasset://fonts/families/Arial.json", Enum.FontWeight.Regular, Enum.FontStyle.Normal)
        NumberIndicator.TextSize = 14
        NumberIndicator.Size = UDim2.new(0.42, 0.00, 0.26, 0.00)
        NumberIndicator.BorderColor3 = Color3.new(0.00, 0.00, 0.00)
        NumberIndicator.Text = "1"
        NumberIndicator.TextColor3 = Color3.new(1.00, 1.00, 1.00)
        NumberIndicator.Position = UDim2.new(0.06, 0.00, 0.43, 0.00)
        NumberIndicator.Parent = DropMenu

        local UICorner_1 = Instance.new("UICorner")
        UICorner_1.Parent = NumberIndicator

        Drop = Instance.new("TextButton")
        Drop.Name = "Drop"
        Drop.TextWrapped = true
        Drop.BorderSizePixel = 0
        Drop.BackgroundColor3 = Color3.new(0.16, 0.16, 0.16)
        Drop.FontFace = Font.new("rbxasset://fonts/families/Balthazar.json", Enum.FontWeight.Regular, Enum.FontStyle.Normal)
        Drop.TextSize = 25
        Drop.Size = UDim2.new(0.30, 0.00, 0.26, 0.00)
        Drop.TextColor3 = Color3.new(1.00, 1.00, 1.00)
        Drop.BorderColor3 = Color3.new(0.00, 0.00, 0.00)
        Drop.Text = "Drop"
        Drop.Position = UDim2.new(0.64, 0.00, 0.17, 0.00)
        Drop.Parent = DropMenu

        local UICorner_2 = Instance.new("UICorner")
        UICorner_2.Parent = Drop

        Dropall = Instance.new("TextButton")
        Dropall.Name = "Dropall"
        Dropall.TextWrapped = true
        Dropall.BorderSizePixel = 0
        Dropall.BackgroundColor3 = Color3.new(0.16, 0.16, 0.16)
        Dropall.FontFace = Font.new("rbxasset://fonts/families/Balthazar.json", Enum.FontWeight.Regular, Enum.FontStyle.Normal)
        Dropall.TextSize = 25
        Dropall.Size = UDim2.new(0.30, 0.00, 0.26, 0.00)
        Dropall.TextColor3 = Color3.new(1.00, 1.00, 1.00)
        Dropall.BorderColor3 = Color3.new(0.00, 0.00, 0.00)
        Dropall.Text = "DropAll"
        Dropall.Position = UDim2.new(0.64, 0.00, 0.43, 0.00)
        Dropall.Parent = DropMenu

        local UICorner_3 = Instance.new("UICorner")
        UICorner_3.Parent = Dropall

        Cancel = Instance.new("TextButton")
        Cancel.Name = "Cancel"
        Cancel.TextWrapped = true
        Cancel.BorderSizePixel = 0
        Cancel.BackgroundColor3 = Color3.new(0.16, 0.16, 0.16)
        Cancel.FontFace = Font.new("rbxasset://fonts/families/Balthazar.json", Enum.FontWeight.Regular, Enum.FontStyle.Normal)
        Cancel.TextSize = 25
        Cancel.Size = UDim2.new(0.30, 0.00, 0.26, 0.00)
        Cancel.TextColor3 = Color3.new(1.00, 1.00, 1.00)
        Cancel.BorderColor3 = Color3.new(0.00, 0.00, 0.00)
        Cancel.Text = "Cancel"
        Cancel.Position = UDim2.new(0.64, 0.00, 0.70, 0.00)
        Cancel.Parent = DropMenu

        local UICorner_4 = Instance.new("UICorner")
        UICorner_4.Parent = Cancel

        Decrease = Instance.new("ImageButton")
        Decrease.Name = "Decrease"
        Decrease.BorderSizePixel = 0
        Decrease.BackgroundColor3 = Color3.new(0.16, 0.16, 0.16)
        Decrease.Image = "rbxassetid://878102417"
        Decrease.Size = UDim2.new(0.17, 0.00, 0.21, 0.00)
        Decrease.BorderColor3 = Color3.new(0.00, 0.00, 0.00)
        Decrease.Position = UDim2.new(0.18, 0.00, 0.72, 0.00)
        Decrease.Parent = DropMenu

        local UICorner_5 = Instance.new("UICorner")
        UICorner_5.Parent = Decrease

        Increase = Instance.new("ImageButton")
        Increase.Name = "Increase"
        Increase.BorderSizePixel = 0
        Increase.BackgroundColor3 = Color3.new(0.16, 0.16, 0.16)
        Increase.Image = "rbxassetid://16844851226"
        Increase.Size = UDim2.new(0.17, 0.00, 0.21, 0.00)
        Increase.BorderColor3 = Color3.new(0.00, 0.00, 0.00)
        Increase.Position = UDim2.new(0.18, 0.00, 0.19, 0.00)
        Increase.Parent = DropMenu

        local UICorner_6 = Instance.new("UICorner")
        UICorner_6.Parent = Increase

        Title = Instance.new("TextLabel")
        Title.Name = "Title"
        Title.TextWrapped = true
        Title.BorderSizePixel = 0
        Title.BackgroundColor3 = Color3.new(0.26, 0.26, 0.26)
        Title.FontFace = Font.new("rbxasset://fonts/families/Arial.json", Enum.FontWeight.Regular, Enum.FontStyle.Normal)
        Title.TextSize = 20
        Title.Size = UDim2.new(1.00, 0.00, 0.14, 0.00)
        Title.BorderColor3 = Color3.new(0.00, 0.00, 0.00)
        Title.Text = "Destroying Item:"
        Title.TextColor3 = Color3.new(1.00, 1.00, 1.00)
        Title.BackgroundTransparency = 0.5
        Title.Parent = DropMenu

        local UIStroke = Instance.new("UIStroke")
        UIStroke.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
        UIStroke.LineJoinMode = Enum.LineJoinMode.Miter
        UIStroke.Thickness = 2
        UIStroke.Parent = Title

        local UIStroke_1 = Instance.new("UIStroke")
        UIStroke_1.ApplyStrokeMode = Enum.ApplyStrokeMode.Border
        UIStroke_1.Thickness = 2
        UIStroke_1.Parent = DropMenu
    end

    -- Assign references
    numberIndicator = NumberIndicator
    titleLabel = Title
    increaseButton = Increase
    decreaseButton = Decrease

    -- Button Click Connections
    if Drop then
        Drop.Activated:Connect(function()
            if ConsoleDropUI.OnDrop then
                ConsoleDropUI.OnDrop(currentAmount)
            end
        end)
    end

    if Dropall then
        Dropall.Activated:Connect(function()
            if ConsoleDropUI.OnDropAll then
                ConsoleDropUI.OnDropAll()
            end
        end)
    end

    if Cancel then
        Cancel.Activated:Connect(function()
            if ConsoleDropUI.OnCancel then
                ConsoleDropUI.OnCancel()
            end
        end)
    end

    if Increase then
        Increase.Activated:Connect(function()
            ConsoleDropUI.ChangeAmount(1)
        end)
    end

    if Decrease then
        Decrease.Activated:Connect(function()
            ConsoleDropUI.ChangeAmount(-1)
        end)
    end

    container = gui
    return gui
end

function ConsoleDropUI.UpdateAmount(amount)
    currentAmount = math.clamp(amount, 1, maxAmount)
    if numberIndicator then
        numberIndicator.Text = tostring(currentAmount)
    end
end

function ConsoleDropUI.Show(itemData, startAmount)
    local gui = ConsoleDropUI.Create()
    gui.Enabled = true

    maxAmount = itemData.Amount or 1
    currentAmount = math.clamp(startAmount or 1, 1, maxAmount)

    if titleLabel then
        titleLabel.Text = "Destroying Item: " .. (itemData.Id or "Unknown")
    end

    ConsoleDropUI.UpdateAmount(currentAmount)

    -- Set gamepad focus to Increase button, then release after 1 second
    -- so D-Pad controls amount instead of navigation
    if increaseButton then
        GuiService.SelectedObject = increaseButton
        task.delay(1, function()
            if container and container.Enabled then
                GuiService.SelectedObject = nil
            end
        end)
    end
end

function ConsoleDropUI.Hide()
    if container then
        container.Enabled = false
    end
end

function ConsoleDropUI.ChangeAmount(delta)
    ConsoleDropUI.UpdateAmount(currentAmount + delta)
    return currentAmount
end

function ConsoleDropUI.GetAmount()
    return currentAmount
end

function ConsoleDropUI.GetIncreaseButton()
    return increaseButton
end

function ConsoleDropUI.GetDecreaseButton()
    return decreaseButton
end

function ConsoleDropUI.Destroy()
    if container then
        container:Destroy()
        container = nil
    end

    -- Clear all references
    numberIndicator = nil
    titleLabel = nil
    increaseButton = nil
    decreaseButton = nil

    -- Clear callbacks
    ConsoleDropUI.OnDrop = nil
    ConsoleDropUI.OnDropAll = nil
    ConsoleDropUI.OnCancel = nil

    -- Reset state
    currentAmount = 1
    maxAmount = 1
end

return ConsoleDropUI


local GuiService = game:GetService("GuiService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local SelectionManager = require(script.Parent.SelectionManager)
local InputManager = require(script.Parent.InputManager)
local IllusionIAS = require(ReplicatedStorage.Packages.IllusionIAS)
local Binds = require(ReplicatedStorage.Shared.Binds)

local ConsoleModeManager = {}
local CurrentMode = "" -- "Combat" | "Inventory"

-- Long Press Configuration
local LONG_PRESS_DURATION = 0.5
local isPressed = false
local switchAction = nil
local _backpackAction = nil

local modeIndicatorGui = nil
local modeTextLabel = nil

function ConsoleModeManager.Init()
    if switchAction then return end -- Prevent double-init

    switchAction = IllusionIAS.new("SwitchModeAction")
    _backpackAction = InputManager.BackpackAction

    local switchBinds = Binds.Global.SwitchMode
    if switchBinds then
        if switchBinds.Console then
             for _, b in ipairs(switchBinds.Console) do
                switchAction:AddBind(b.Main, unpack(b.Modifiers or {}))
             end
        end
        if switchBinds.PC then
             for _, b in ipairs(switchBinds.PC) do
                switchAction:AddBind(b.Main, unpack(b.Modifiers or {}))
             end
        end
    end

    IllusionIAS.addContext(InputManager.ContextGlobal, switchAction)

    CreateModeIndicator()

    local longPressTask = nil

    switchAction.Activated:Connect(function(active, pressed)
        if pressed then
            isPressed = true

            -- Schedule the toggle action
            longPressTask = task.delay(LONG_PRESS_DURATION, function()
                if isPressed then
                    ConsoleModeManager.ToggleMode()
                    isPressed = false -- Reset state after successful toggle
                    longPressTask = nil
                end
            end)
        else
            isPressed = false
            -- Cancel the scheduled toggle if button is released early
            if longPressTask then
                task.cancel(longPressTask)
                longPressTask = nil
            end
        end
    end)
end

function CreateModeIndicator()
    if modeIndicatorGui then return end

    local player = game:GetService("Players").LocalPlayer
    modeIndicatorGui = Instance.new("ScreenGui")
    modeIndicatorGui.Name = "StowayConsoleHUD"
    modeIndicatorGui.ResetOnSpawn = false
    modeIndicatorGui.Parent = player:WaitForChild("PlayerGui")

    local bg = Instance.new("Frame")
    bg.Name = "ModeBadge"
    bg.Size = UDim2.fromOffset(150, 40)
    bg.Position = UDim2.new(0.95, -160, 0.85, 0) -- Bottom Right
    bg.BackgroundColor3 = Color3.fromRGB(30, 30, 30)
    bg.BackgroundTransparency = 0.2
    bg.BorderSizePixel = 0
    bg.Parent = modeIndicatorGui

    local uiCorner = Instance.new("UICorner")
    uiCorner.CornerRadius = UDim.new(0, 8)
    uiCorner.Parent = bg

    modeTextLabel = Instance.new("TextLabel")
    modeTextLabel.Name = "ModeText"
    modeTextLabel.Size = UDim2.fromScale(1, 1)
    modeTextLabel.BackgroundTransparency = 1
    modeTextLabel.Font = Enum.Font.GothamBold
    modeTextLabel.TextSize = 18
    modeTextLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
    modeTextLabel.Text = "COMBAT"
    modeTextLabel.Parent = bg
end

function ConsoleModeManager.SetMode(modeName)
    if CurrentMode == modeName then return end
    CurrentMode = modeName

    print(`[ConsoleModeManager] Switching to {modeName} Mode`)

    if modeName == "Combat" then
        InputManager.SetCombatEnabled(true)
        InputManager.SetInventoryEnabled(false)
        SelectionManager.SetEnabled(false)
        GuiService.GuiNavigationEnabled = false
        GuiService.SelectedObject = nil

        if modeTextLabel then
             modeTextLabel.Text = "⚔ COMBAT"
             modeTextLabel.TextColor3 = Color3.fromRGB(255, 100, 100) -- Reddish
        end

    elseif modeName == "Inventory" then
        InputManager.SetCombatEnabled(false)
        InputManager.SetInventoryEnabled(true)
        SelectionManager.SetEnabled(true)
        GuiService.GuiNavigationEnabled = true

        if modeTextLabel then
             local initialDesc = SelectionManager.GetStateDescription and SelectionManager.GetStateDescription() or "SELECTING"
             modeTextLabel.Text = "🎒 " .. initialDesc
             modeTextLabel.TextColor3 = Color3.fromRGB(100, 255, 150) -- Greenish
        end

        -- Connect to State Changes to update UI
        SelectionManager.OnStateChanged = function(newDesc)
            if modeTextLabel then
                modeTextLabel.Text = "🎒 " .. newDesc
            end
        end
    end
end

function ConsoleModeManager.ToggleMode()
    if CurrentMode == "Combat" then
        ConsoleModeManager.SetMode("Inventory")
    else
        ConsoleModeManager.SetMode("Combat")
    end
end

function ConsoleModeManager.Destroy()
    isPressed = false
    if switchAction then
        switchAction:Destroy()
        switchAction = nil
    end

    if modeIndicatorGui then
        modeIndicatorGui:Destroy()
        modeIndicatorGui = nil
        modeTextLabel = nil
    end

    -- Reset to default state
    CurrentMode = ""
    InputManager.SetCombatEnabled(true)
    InputManager.SetInventoryEnabled(false)
    SelectionManager.SetEnabled(false)
    SelectionManager.OnStateChanged = nil
end

return ConsoleModeManager


-- src/client/StowayClientv1_2/Input/InputManager.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local IllusionIAS = require(ReplicatedStorage.Packages.IllusionIAS)
local Binds = require(ReplicatedStorage.Shared.Binds)
local Settings = require(ReplicatedStorage.Shared.Settings)

local InputManager = {}
InputManager.ContextGlobal = "StowayGlobal"
InputManager.ContextCombat = "StowayCombat"
InputManager.ContextInventory = "StowayInventory"
InputManager.ContextDrop = "StowayDrop"

InputManager.HotbarActions = {} -- { [1] = IASObject, ... }
InputManager.BackpackAction = nil

function InputManager.Init()
    -- Initialize Contexts
    IllusionIAS.newContext(InputManager.ContextGlobal)
    IllusionIAS.newContext(InputManager.ContextCombat)
    IllusionIAS.newContext(InputManager.ContextInventory)
    IllusionIAS.newContext(InputManager.ContextDrop)

    -- Initialize Backpack Action
    local bpAction = IllusionIAS.new("BackpackAction")
    InputManager.BackpackAction = bpAction

    -- Initialize Hotbar Actions
    local slotCount = #Binds.HotbarSlots
    if slotCount < 10 then slotCount = 10 end

    for i = 1, slotCount do
        if not InputManager.HotbarActions[i] then
            local action = IllusionIAS.new("HotbarSlot" .. i)
            table.insert(InputManager.HotbarActions, action)
        end
    end

    -- Add to contexts
    IllusionIAS.addContext(InputManager.ContextGlobal, bpAction)
    IllusionIAS.addContext(InputManager.ContextCombat, unpack(InputManager.HotbarActions))

    -- Apply Binds
    InputManager.UpdateBinds()

    -- Enable Contexts
    IllusionIAS.enableContext(InputManager.ContextGlobal, true)
    IllusionIAS.enableContext(InputManager.ContextCombat, true)
    IllusionIAS.enableContext(InputManager.ContextInventory, false) -- Default Off
    IllusionIAS.enableContext(InputManager.ContextDrop, false)
end

function InputManager.SetCombatEnabled(enabled: boolean)
    IllusionIAS.enableContext(InputManager.ContextCombat, enabled)
end

function InputManager.SetInventoryEnabled(enabled: boolean)
    IllusionIAS.enableContext(InputManager.ContextInventory, enabled)
end

function InputManager.SetDropEnabled(enabled: boolean)
    IllusionIAS.enableContext(InputManager.ContextDrop, enabled)
end

function InputManager.UpdateBinds()
    if InputManager.BackpackAction then
        InputManager.BackpackAction:ClearBinds()

        if Settings.Storage.BackpackEnabled then
            local binds = Binds.Global.Backpack
            if binds then
                if binds.PC then
                    for _, b in ipairs(binds.PC) do
                        InputManager.BackpackAction:AddBind(b.Main, unpack(b.Modifiers or {}))
                    end
                end
                if binds.Console then
                    for _, b in ipairs(binds.Console) do
                        InputManager.BackpackAction:AddBind(b.Main, unpack(b.Modifiers or {}))
                    end
                end
            end
        end
    end

    for i, action in ipairs(InputManager.HotbarActions) do
        local bindData = Binds.HotbarSlots[i]
        if bindData then
            action:ClearBinds()
            if bindData.PC then
                for _, b in ipairs(bindData.PC) do
                    action:AddBind(b.Main, unpack(b.Modifiers or {}))
                end
            end
            if bindData.Console then
                for _, b in ipairs(bindData.Console) do
                     action:AddBind(b.Main, unpack(b.Modifiers or {}))
                end
            end
        end
    end
end

function InputManager.Destroy()
    -- Cleanup Actions
    if InputManager.BackpackAction then
        InputManager.BackpackAction:Destroy()
        InputManager.BackpackAction = nil
    end

    for _, action in ipairs(InputManager.HotbarActions) do
        action:Destroy()
    end
    table.clear(InputManager.HotbarActions)

    -- Cleanup Contexts
    IllusionIAS.removeContext(InputManager.ContextGlobal)
    IllusionIAS.removeContext(InputManager.ContextCombat)
    IllusionIAS.removeContext(InputManager.ContextInventory)
    IllusionIAS.removeContext(InputManager.ContextDrop)
end

return InputManager


local UserInputService = game:GetService("UserInputService")
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local InventoryStore = require(script.Parent.Parent.CoreState.InventoryStore)
local InventoryController = require(script.Parent.Parent.Operations.InventoryController)
local Settings = require(ReplicatedStorage.Shared.Settings)

local InteractionManager = {}
local DragHandlerRef = nil
local HotbarFrameRef = nil

local ConsoleModeManager = require(script.Parent.ConsoleModeManager)

local connection = nil
local scrollConnection = nil
local hotbarEnterConnection = nil
local hotbarLeaveConnection = nil

--// Helper: Zoom Management
local player = Players.LocalPlayer
local DefaultMinZoom = player.CameraMinZoomDistance
local DefaultMaxZoom = player.CameraMaxZoomDistance
local IsZoomLocked = false

local function getCameraDistance()
	local camera = workspace.CurrentCamera
	local character = player.Character; if not character then return nil end
	local CamPos = camera.CFrame.Position
	local CharPos = character:GetPivot().Position

	--this is the best I can do there is still some zooming in/out ;((
	return math.floor((CamPos - CharPos).Magnitude)
end

local function lockZoomAtCurrentDistance(): boolean
	if IsZoomLocked then return true end

	local currentDist = getCameraDistance()
	if not currentDist then return false end

	-- Snapshot current defaults so external changes are restored correctly.
	DefaultMinZoom = player.CameraMinZoomDistance
	DefaultMaxZoom = player.CameraMaxZoomDistance

	-- Lock to current distance
	player.CameraMinZoomDistance = currentDist
	player.CameraMaxZoomDistance = currentDist
	IsZoomLocked = true

	return true
end

local function unlockZoom()
	if not IsZoomLocked then return end
	player.CameraMinZoomDistance = DefaultMinZoom
	player.CameraMaxZoomDistance = DefaultMaxZoom
	IsZoomLocked = false
end

local function disconnectHotbarHoverConnections()
	if hotbarEnterConnection then
		hotbarEnterConnection:Disconnect()
		hotbarEnterConnection = nil
	end
	if hotbarLeaveConnection then
		hotbarLeaveConnection:Disconnect()
		hotbarLeaveConnection = nil
	end
end

local function hotbarHasItems(): boolean
	local hotbar = InventoryStore:GetHotbar()
	for _, uuid in pairs(hotbar) do
		if uuid then
			return true
		end
	end
	return false
end

local function connectHotbarHoverConnections()
	disconnectHotbarHoverConnections()
	if not HotbarFrameRef then return end

	hotbarEnterConnection = HotbarFrameRef.MouseEnter:Connect(function()
		if hotbarHasItems() then
			lockZoomAtCurrentDistance()
		end
	end)

	hotbarLeaveConnection = HotbarFrameRef.MouseLeave:Connect(function()
		unlockZoom()
	end)
end

--// Helper: Hotbar Cycling
local function handleScroll(input)
    -- 1. Check if Mouse is over Hotbar (User Request)
    if not IsZoomLocked then return end

    -- 2. Check if Hotbar has items
    local hotbar = InventoryStore:GetHotbar()
    local hasItems = false
    for _, uuid in pairs(hotbar) do
        if uuid then hasItems = true; break end
    end

    if not hasItems then return end

    -- 3. Ensure zoom is locked while cycling.
    lockZoomAtCurrentDistance()

    -- 4. Determine Direction & Current State
    local direction = (input.Position.Z > 0) and 1 or -1 -- 1 = Up (Ascending), -1 = Down (Descending)
    local currentEquipped = InventoryStore:GetEquippedUUID()
    local currentSlotIndex = nil
	local maxSlots = InventoryStore:GetMaxHotbarSlots()

    if currentEquipped then
        local slotType, slot = InventoryStore:FindSlotByUUID(currentEquipped)
        if slotType == "Hotbar" then
            currentSlotIndex = slot
        end
    end

    -- 5. Find Next Slot
    local nextSlot = nil

    if not currentSlotIndex then
        -- Nothing equipped: Find first occupied slot
        if direction == 1 then
             for i = 1, maxSlots do
                if hotbar[i] then nextSlot = i; break end
             end
        else
            for i = maxSlots, 1, -1 do
                if hotbar[i] then nextSlot = i; break end
            end
        end
    else
        -- Something equipped: Find next occupied slot in direction
        local checkSlot = currentSlotIndex
        for _ = 1, maxSlots do
            checkSlot = checkSlot + (direction == 1 and 1 or -1)

            -- Wrap around
            if checkSlot > maxSlots then checkSlot = 1 end
            if checkSlot < 1 then checkSlot = maxSlots end

            if hotbar[checkSlot] then
                nextSlot = checkSlot
                break
            end
        end
    end

    -- 6. Equip
    if nextSlot then
        InventoryController.HandleEquipped(hotbar[nextSlot], true) -- Replicate = true
    end
end

local function preferredInputChanged()
    local preferredInput = UserInputService.PreferredInput
    if not DragHandlerRef then return end

    local isGamepad = preferredInput == Enum.PreferredInput.Gamepad
    local isMouseKeyboard = preferredInput == Enum.PreferredInput.KeyboardAndMouse

    if isGamepad then
        warn("Gamepad detected, disabling DragHandler")
        DragHandlerRef.SetEnabled(false)

        ConsoleModeManager.Init()
        ConsoleModeManager.SetMode("Combat")

        -- Disable Scroll on Console
        if scrollConnection then
            scrollConnection:Disconnect()
            scrollConnection = nil
        end
		disconnectHotbarHoverConnections()
		unlockZoom()
    else
        warn("Mouse/Touch detected, enabling DragHandler")
        DragHandlerRef.SetEnabled(true)

        ConsoleModeManager.Destroy()

        -- Enable mouse-wheel behavior only on mouse+keyboard.
        if isMouseKeyboard and Settings.Gameplay.MouseScroll and not scrollConnection then
             scrollConnection = UserInputService.InputChanged:Connect(function(input, gameProcessed)
                if input.UserInputType == Enum.UserInputType.MouseWheel then
                    handleScroll(input)
                end
            end)
        end

        if (not isMouseKeyboard) and scrollConnection then
            scrollConnection:Disconnect()
            scrollConnection = nil
        end

        -- Hover-based zoom lock only applies to mouse+keyboard.
        if isMouseKeyboard then
		    connectHotbarHoverConnections()
        else
            disconnectHotbarHoverConnections()
            unlockZoom()
        end
    end
end

function InteractionManager.Init(dragHandler, hotbarFrame)
    DragHandlerRef = dragHandler
    HotbarFrameRef = hotbarFrame
	DefaultMinZoom = player.CameraMinZoomDistance
	DefaultMaxZoom = player.CameraMaxZoomDistance
	IsZoomLocked = false

    preferredInputChanged()
    connection = UserInputService:GetPropertyChangedSignal("PreferredInput"):Connect(preferredInputChanged)
end

function InteractionManager.Destroy()
    if connection then
        connection:Disconnect()
        connection = nil
    end

	if scrollConnection then
		scrollConnection:Disconnect()
		scrollConnection = nil
	end
	disconnectHotbarHoverConnections()
	unlockZoom()

	ConsoleModeManager.Destroy()
	DragHandlerRef = nil
    HotbarFrameRef = nil
end

return InteractionManager


local GuiService = game:GetService("GuiService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local InputManager = require(script.Parent.InputManager)
local InventoryController = require(script.Parent.Parent.Operations.InventoryController)
local InventoryStore = require(script.Parent.Parent.CoreState.InventoryStore)
local IllusionIAS = require(ReplicatedStorage.Packages.IllusionIAS)

local SelectionManager = {}
local SelectionState = {
    enabled = false,
    step = 0,  -- 0: idle, 1: source selected
    sourceType = nil,
    sourceSlot = nil,
    sourceFrame = nil,
}

local HotbarFrame = nil
local BackpackFrame = nil

local Binds = require(ReplicatedStorage.Shared.Binds)

local ConsoleDropUI = require(script.Parent.ConsoleDropUI)

local swapAction = nil
local dropAction = nil
local quickMoveAction = nil
local cancelAction = nil

-- Dynamic Drop Actions Container
local activeDropActions = {}
local dropIncreaseAction = nil
local dropDecreaseAction = nil
local HookManager = nil

SelectionManager.OnStateChanged = nil -- Call this function instead of firing a BindableEvent

function SelectionManager.Init(hotbarFrame, backpackFrame, hookManager)
    HotbarFrame = hotbarFrame
    BackpackFrame = backpackFrame
    HookManager = hookManager

    if HotbarFrame then HotbarFrame.SelectionGroup = true end
    if BackpackFrame then BackpackFrame.SelectionGroup = true end

    local inventoryActions = Binds.Global.InventoryActions

    -- 1. Swap Bind (Y)
    swapAction = IllusionIAS.new("ConsoleSwapAction")
    Binds.ApplyToAction(swapAction, inventoryActions.Swap, "Console")
    swapAction.Activated:Connect(function(active, pressed)
        if not SelectionState.enabled or not pressed then return end
        SelectionManager.HandleSwap()
    end)

    -- 2. Drop Bind (X)
    dropAction = IllusionIAS.new("ConsoleDropAction")
    Binds.ApplyToAction(dropAction, inventoryActions.Drop, "Console")
    dropAction.Activated:Connect(function(active, pressed)
        if not SelectionState.enabled or not pressed then return end
        SelectionManager.HandleDrop()
    end)

    -- 3. Quick Move Bind (A)
    quickMoveAction = IllusionIAS.new("ConsoleQuickMoveAction")
    Binds.ApplyToAction(quickMoveAction, inventoryActions.QuickMove, "Console")
    quickMoveAction.Activated:Connect(function(active, pressed)
        if not SelectionState.enabled or not pressed then return end
        SelectionManager.HandleQuickMove()
    end)

    -- 4. Cancel Bind (B)
    cancelAction = IllusionIAS.new("ConsoleCancelAction")
    Binds.ApplyToAction(cancelAction, inventoryActions.Cancel, "Console")
    cancelAction.Activated:Connect(function(active, pressed)
        if not SelectionState.enabled or not pressed then return end
        SelectionManager.CancelSelection()
    end)

    IllusionIAS.addContext(InputManager.ContextInventory, swapAction, dropAction, quickMoveAction, cancelAction)
end

function SelectionManager.LoadDropBinds()
    if next(activeDropActions) then return end -- Already loaded

    local dropActions = Binds.Global.DropActions

    dropIncreaseAction = IllusionIAS.new("DropIncrease")
    Binds.ApplyToAction(dropIncreaseAction, dropActions.Increase, "Console")

    local increaseButton = ConsoleDropUI.GetIncreaseButton()
    if increaseButton then
        dropIncreaseAction:SetUIButton(increaseButton)
    end

    dropDecreaseAction = IllusionIAS.new("DropDecrease")
    Binds.ApplyToAction(dropDecreaseAction, dropActions.Decrease, "Console")

    local decreaseButton = ConsoleDropUI.GetDecreaseButton()
    if decreaseButton then
        dropDecreaseAction:SetUIButton(decreaseButton)
    end

    dropIncreaseAction.Activated:Connect(function(active, pressed) if pressed then ConsoleDropUI.ChangeAmount(1) end end)
    dropDecreaseAction.Activated:Connect(function(active, pressed) if pressed then ConsoleDropUI.ChangeAmount(-1) end end)

    IllusionIAS.addContext(InputManager.ContextDrop, dropIncreaseAction, dropDecreaseAction)

    table.insert(activeDropActions, dropIncreaseAction)
    table.insert(activeDropActions, dropDecreaseAction)
end

function SelectionManager.UnloadDropBinds()
    for _, action in ipairs(activeDropActions) do
        action:Destroy()
    end
    table.clear(activeDropActions)
    dropIncreaseAction = nil
    dropDecreaseAction = nil
end

function SelectionManager.GetSelectedSlotInfo()
    local selected = GuiService.SelectedObject
    if not selected then return nil end

    local slotFrame = selected.Parent
    if not slotFrame then return nil end

    local slotType
    if slotFrame.Parent and slotFrame.Parent.Name == "Hotbar" then
        slotType = "Hotbar"
    elseif slotFrame.Parent and slotFrame.Parent.Name == "ScrollingFrame" and slotFrame.Parent.Parent.Name == "Backpack" then
        slotType = "Storage"
    elseif slotFrame.Parent and slotFrame.Parent.Name == "Storage" then
         slotType = "Storage"
    end

    if not slotType then return nil end

    local slotName = slotFrame.Name:gsub("_", "")
    local slotIndex = tonumber(slotName)
    if not slotIndex then return nil end

    return {
        Type = slotType,
        Index = slotIndex,
        Frame = slotFrame
    }
end

function SelectionManager.GetStateDescription()
    if SelectionState.step == 2 then
        return "🗑 DROPPING ITEM"
    elseif SelectionState.step == 1 then
        return "🔄 SWAPPING - SELECT TARGET"
    else
        return "👋 SELECTING"
    end
end

function SelectionManager.NotifyStateChanged()
    if SelectionManager.OnStateChanged then
         SelectionManager.OnStateChanged(SelectionManager.GetStateDescription())
    end
end

function SelectionManager.HandleSwap()
    local selected = SelectionManager.GetSelectedSlotInfo()
    if not selected then return end

    if SelectionState.step == 2 then return end -- Block Swap while Dropping

    if SelectionState.step == 0 then
        -- Step 1: Select source
        SelectionState.step = 1
        SelectionState.sourceType = selected.Type
        SelectionState.sourceSlot = selected.Index
        SelectionState.sourceFrame = selected.Frame

        SelectionManager.ApplySelectionHighlight(selected.Frame)
        SelectionManager.NotifyStateChanged()

    elseif SelectionState.step == 1 then
        -- Step 2: Select destination
        if SelectionState.sourceType == selected.Type and SelectionState.sourceSlot == selected.Index then
             -- Toggling same slot deselects
             SelectionManager.CancelSelection()
             return
        end

        InventoryController.Swap(
            SelectionState.sourceType,
            SelectionState.sourceSlot,
            selected.Type,
            selected.Index,
            true
        )

        SelectionManager.CancelSelection()
    end
end

function SelectionManager.HandleDrop()
    local selected = SelectionManager.GetSelectedSlotInfo()
    if not selected then return end

    if SelectionState.step == 1 then return end -- Block Drop while Swapping

    -- Fetch Item Data using new helper
    local item = InventoryStore:GetItemFromSlot(selected.Type, selected.Index)

    if not item then return end

    SelectionState.step = 2 -- Dropping
    SelectionManager.NotifyStateChanged()

    InputManager.SetInventoryEnabled(false)
    InputManager.SetDropEnabled(true)

    SelectionManager.LoadDropBinds()

    -- Set up callbacks for button clicks
    ConsoleDropUI.OnDrop = function(amount)
        InventoryController.Drop(selected.Type, selected.Index, amount, true)
        SelectionManager.CloseDropUI()
    end

    ConsoleDropUI.OnDropAll = function()
        InventoryController.Drop(selected.Type, selected.Index, item.Amount, true)
        SelectionManager.CloseDropUI()
    end

    ConsoleDropUI.OnCancel = function()
        SelectionManager.CloseDropUI()
    end

    ConsoleDropUI.Show(item, 1)
end

function SelectionManager.CloseDropUI()
    ConsoleDropUI.Hide()
    InputManager.SetDropEnabled(false)
    InputManager.SetInventoryEnabled(true)

    SelectionManager.UnloadDropBinds()

    -- Clear callbacks
    ConsoleDropUI.OnDrop = nil
    ConsoleDropUI.OnDropAll = nil
    ConsoleDropUI.OnCancel = nil

    SelectionState.step = 0
    SelectionManager.NotifyStateChanged()
end

function SelectionManager.HandleQuickMove()
    local selected = SelectionManager.GetSelectedSlotInfo()
    if not selected then return end

    -- If in middle of swap, cancel it
    if SelectionState.step == 1 then
        SelectionManager.CancelSelection()
    end

    if SelectionState.step == 2 then return end -- Don't quick move while dropping

    -- Determine target type and find empty slot using helpers
    local targetType = (selected.Type == "Hotbar") and "Storage" or "Hotbar"
    local emptySlotIndex

    if targetType == "Hotbar" then
        emptySlotIndex = InventoryStore:FindFirstEmptyHotbarSlot()
    else
        emptySlotIndex = InventoryStore:FindFirstEmptyStorageSlot()
    end

    if emptySlotIndex then
        InventoryController.Swap(
            selected.Type,
            selected.Index,
            targetType,
            emptySlotIndex,
            true
        )
    end
end

function SelectionManager.CancelSelection()
    if SelectionState.step == 2 then
        SelectionManager.CloseDropUI()
        return
    end

    if SelectionState.sourceFrame then
        SelectionManager.ClearSelectionHighlight(SelectionState.sourceFrame)
    end
    SelectionState.step = 0
    SelectionState.sourceType = nil
    SelectionState.sourceSlot = nil
    SelectionState.sourceFrame = nil

    SelectionManager.NotifyStateChanged()
end

function SelectionManager.SetEnabled(state: boolean)
    SelectionState.enabled = state
    GuiService.GuiNavigationEnabled = state

    if state then
        local firstHotbarButton = InventoryStore:GetFirstOccupiedHotbarButton(HotbarFrame)
        if firstHotbarButton then
            GuiService.SelectedObject = firstHotbarButton
        end
        return
    end

    if not state then
        SelectionManager.CancelSelection()
        SelectionManager.UnloadDropBinds() -- Cleanup drop binds if forced disabled
        GuiService.SelectedObject = nil
    end
end

function SelectionManager.ApplySelectionHighlight(frame)
    if HookManager then
        HookManager:Fire("OnSelect", frame)
    end
end

function SelectionManager.ClearSelectionHighlight(frame)
    if HookManager then
        HookManager:Fire("OnDeselect", frame)
    end
end

function SelectionManager.Destroy()
    SelectionManager.CancelSelection()
    SelectionManager.UnloadDropBinds()

    -- Destroy Drop UI completely
    ConsoleDropUI.Destroy()

    if swapAction then swapAction:Destroy(); swapAction = nil end
    if dropAction then dropAction:Destroy(); dropAction = nil end
    if quickMoveAction then quickMoveAction:Destroy(); quickMoveAction = nil end
    if cancelAction then cancelAction:Destroy(); cancelAction = nil end

    SelectionState.enabled = false
    GuiService.GuiNavigationEnabled = false
    HotbarFrame = nil
    BackpackFrame = nil

    SelectionManager.OnStateChanged = nil
end

return SelectionManager


-- src/client/StowayClientV1_2/NetworkFunctions/NetworkHandler.luau
-- Centralized Network Gateway for Inventory System V1.2
-- This module handles all server communication (incoming & outgoing)

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local NetworkHandler = {}

-- Remotes
local Remotes = ReplicatedStorage:WaitForChild("Remotes")
local InventoryUpdate = Remotes:WaitForChild("InventoryUpdate") :: RemoteEvent
local InventoryAction = Remotes:WaitForChild("InventoryAction") :: RemoteEvent

-- Connection storage for cleanup
local _updateConnection = nil

function NetworkHandler.Send(action: string, args: {[string]: any})
    InventoryAction:FireServer(action, args)
end

function NetworkHandler.Init(handlers: {[string]: (any) -> ()})
    if _updateConnection then
        _updateConnection:Disconnect()
    end

    _updateConnection = InventoryUpdate.OnClientEvent:Connect(function(action: string, payload: any)
        local handler = handlers[action]
        if handler then
            handler(payload)
        else
            warn("[NetworkHandler] Unknown action received:", action)
        end
    end)
end

function NetworkHandler.Destroy()
    if _updateConnection then
        _updateConnection:Disconnect()
        _updateConnection = nil
    end
end

return NetworkHandler