local TAG = "SitAny_"
local PLUGIN = PLUGIN
SitAnywhere = SitAnywhere or {}

local NextUse = setmetatable({}, {__mode = 'k', __index = function() return 0 end})

-- Конфигурация через ix.config
ix.config.Add("sitOnPlayers", true, "Разрешить сидеть на игроках в SitAnywhere", nil, {
	category = "Sit Anywhere"
})

ix.config.Add("sitOnPlayerEntities", true, "Разрешить сидеть на сущностях игроков", nil, {
	category = "Sit Anywhere"
})

ix.config.Add("sitPlayerDamage", false, "Разрешить урон сидящим игрокам", nil, {
	category = "Sit Anywhere"
})

ix.config.Add("sitAllowWeapons", false, "Разрешить использование оружия при сидении", nil, {
	category = "Sit Anywhere"
})

ix.config.Add("sitAdminOnly", false, "Только админы могут сидеть", nil, {
	category = "Sit Anywhere"
})

ix.config.Add("sitAntiPropSurf", true, "Запретить физган на объектах с сидящими игроками", nil, {
	category = "Sit Anywhere"
})

ix.config.Add("sitAntiToolAbuse", true, "Запретить тулган на объектах с сидящими игроками", nil, {
	category = "Sit Anywhere"
})

ix.config.Add("sitAllowTightPlaces", false, "Разрешить сидеть в тесных местах", nil, {
	category = "Sit Anywhere"
})

ix.config.Add("sitForceNoWalk", false, "Отключить необходимость шага для сидения", nil, {
	category = "Sit Anywhere"
})

local META = FindMetaTable("Player")

util.AddNetworkString("SitAnywhere")

net.Receive("SitAnywhere", function(len, ply)
	local netID = net.ReadInt(4)
	if netID == SitAnywhere.NET.SitWantedAng then
		local wantedAng, traceStart, traceNormal = net.ReadFloat(), net.ReadVector(), net.ReadVector()

		if traceStart:Distance(ply:EyePos()) > 10 then return end
		local trace = util.TraceLine({
			start = traceStart,
			endpos = traceStart + traceNormal * 12000,
			filter = player.GetAll()
		})

		ply:Sit(trace, nil, nil, nil, nil, nil, wantedAng)
	elseif netID == SitAnywhere.NET.SitRequestExit then
		ply:ExitSit()
	end
end)

local function Sit(ply, pos, ang, parent, parentbone, func, exit)
	if IsValid(ply:GetVehicle()) then
		local veh = ply:GetVehicle()
		if veh:GetClass() == "prop_vehicle_prisoner_pod" and IsValid(veh.holder) then
			SafeRemoveEntity(veh.holder)
		end
		ply:ExitVehicle()
	end

	local vehicle = ents.Create("prop_vehicle_prisoner_pod")
	local t = hook.Run("OnPlayerSit", ply, pos, ang, parent or NULL, parentbone, vehicle)

	if t == false then
		SafeRemoveEntity(vehicle)
		return false
	end

	vehicle:SetAngles(ang)
	pos = pos + vehicle:GetUp() * 18
	vehicle:SetPos(pos)

	vehicle.playerdynseat = true
	vehicle:SetNWBool("playerdynseat", true)
	vehicle.sittingPly = ply
	vehicle.oldpos = vehicle:WorldToLocal(ply:GetPos())
	vehicle.wasCrouching = ply:Crouching()

	vehicle:SetModel("models/nova/airboat_seat.mdl")
	vehicle:SetKeyValue("vehiclescript", "scripts/vehicles/prisoner_pod.txt")
	vehicle:SetKeyValue("limitview", "0")
	vehicle:Spawn()
	vehicle:Activate()

	if not IsValid(vehicle) or not IsValid(vehicle:GetPhysicsObject()) then
		SafeRemoveEntity(vehicle)
		return false
	end

	local phys = vehicle:GetPhysicsObject()
	vehicle:SetMoveType(MOVETYPE_PUSH)
	phys:Sleep()
	vehicle:SetCollisionGroup(COLLISION_GROUP_WORLD)
	vehicle:SetNotSolid(true)
	phys:Sleep()
	phys:EnableGravity(false)
	phys:EnableMotion(false)
	phys:EnableCollisions(false)
	phys:SetMass(1)
	vehicle:CollisionRulesChanged()

	vehicle:DrawShadow(false)
	vehicle:SetColor(Color(0,0,0,0))
	vehicle:SetRenderMode(RENDERMODE_TRANSALPHA)
	vehicle:SetNoDraw(true)

	vehicle.VehicleName = "Airboat Seat"
	vehicle.ClassOverride = "prop_vehicle_prisoner_pod"
	vehicle.PhysgunDisabled = true
	vehicle.m_tblToolsAllowed = {}
	vehicle.customCheck = function() return false end

	if IsValid(parent) then
		local r = math.rad(ang.yaw + 90)
		vehicle.plyposhack = vehicle:WorldToLocal(pos + Vector(math.cos(r) * 2, math.sin(r) * 2, 2))
		vehicle:SetParent(parent)
		vehicle.parent = parent
	else
		vehicle.OnWorld = true
	end

	local prev = ply:GetAllowWeaponsInVehicle()
	if prev then
		ply.sitting_allowswep = nil
	elseif ix.config.Get("sitAllowWeapons", false) then
		ply.sitting_allowswep = prev
		ply:SetAllowWeaponsInVehicle(true)
	end

	ply:EnterVehicle(vehicle)

	if ix.config.Get("sitPlayerDamage", false) then
		ply:SetCollisionGroup(COLLISION_GROUP_WEAPON)
		ply:CollisionRulesChanged()
	end

	if func then
		func(ply)
	end

	ply.seatExit = exit
	ply:SetEyeAngles(Angle(0,90,0))

	return vehicle
end

local d = function(a,b) return math.abs(a-b) end

local SittingOnPlayerPoses = {
	{
		Pos = Vector(-33, 13, 7),
		Ang = Angle(0, 90, 90),
		FindAng = 90,
	},
	{
		Pos = Vector(33, 13, 7),
		Ang = Angle(0, 270, 90),
		Func = function(ply)
			if not ply:LookupBone("ValveBiped.Bip01_R_Thigh") then return end
			ply:ManipulateBoneAngles(ply:LookupBone("ValveBiped.Bip01_R_Thigh"), Angle(0,90,0))
			ply:ManipulateBoneAngles(ply:LookupBone("ValveBiped.Bip01_L_Thigh"), Angle(0,90,0))
		end,
		OnExitFunc = function(ply)
			if not ply:LookupBone("ValveBiped.Bip01_R_Thigh") then return end
			ply:ManipulateBoneAngles(ply:LookupBone("ValveBiped.Bip01_R_Thigh"), Angle(0,0,0))
			ply:ManipulateBoneAngles(ply:LookupBone("ValveBiped.Bip01_L_Thigh"), Angle(0,0,0))
		end,
		FindAng = 270,
	},
	{
		Pos = Vector(0, 16, -15),
		Ang = Angle(0, 180, 0),
		Func = function(ply)
			if not ply:LookupBone("ValveBiped.Bip01_R_Thigh") then return end
			ply:ManipulateBoneAngles(ply:LookupBone("ValveBiped.Bip01_R_Thigh"), Angle(45,0,0))
			ply:ManipulateBoneAngles(ply:LookupBone("ValveBiped.Bip01_L_Thigh"), Angle(-45,0,0))
		end,
		OnExitFunc = function(ply)
			if not ply:LookupBone("ValveBiped.Bip01_R_Thigh") then return end
			ply:ManipulateBoneAngles(ply:LookupBone("ValveBiped.Bip01_R_Thigh"), Angle(0,0,0))
			ply:ManipulateBoneAngles(ply:LookupBone("ValveBiped.Bip01_L_Thigh"), Angle(0,0,0))
		end,
		FindAng = 0,
	},
	{
		Pos = Vector(0, 8, -18),
		Ang = Angle(0, 0, 0),
		FindAng = 180,
	},
}

local lookup = {}
for k,v in pairs(SittingOnPlayerPoses) do
	table.insert(lookup, {v.FindAng, v})
	table.insert(lookup, {v.FindAng + 360, v})
	table.insert(lookup, {v.FindAng - 360, v})
end

local function FindPose(this,me)
	local avec = me:GetAimVector()
	avec.z = 0
	avec:Normalize()
	local evec = this:GetRight()
	evec.z = 0
	evec:Normalize()
	local derp = avec:Dot(evec)

	local avec2 = me:GetAimVector()
	avec2.z = 0
	avec2:Normalize()
	local evec2 = this:GetForward()
	evec2.z = 0
	evec2:Normalize()
	local herp = avec2:Dot(evec2)
	local v = Vector(derp,herp,0)
	local a = v:Angle()

	local ang = a.y
	ang = ang + 90 + 180
	ang = ang % 360

	table.sort(lookup,function(aa,bb)
		return d(ang,aa[1]) < d(ang,bb[1])
	end)
	return lookup[1][2]
end

function META.Sit(ply, EyeTrace, ang, parent, parentbone, func, exit, wantedAng)
	if EyeTrace == nil then
		EyeTrace = ply:GetEyeTrace()
	elseif type(EyeTrace) == "Vector" then
		return Sit(ply, EyeTrace, ang or Angle(0,0,0), parent, parentbone or 0, func, exit)
	end

	local valid, ent = SitAnywhere.ValidSitTrace(ply, EyeTrace)
	if not valid then return end
	local surfaceAng = EyeTrace.HitNormal:Angle() + Angle(-270, 0, 0)
	ang = surfaceAng

	if wantedAng and math.abs(surfaceAng.pitch) <= 15 then
		ent = EyeTrace.Entity
		if EyeTrace.HitWorld or not ent:IsPlayer() then
			if SitAnywhere.CheckValidAngForSit(EyeTrace.HitPos, EyeTrace.HitNormal:Angle(), wantedAng) then
				ang.yaw = wantedAng + 90
			else
				return
			end
		end
		return Sit(ply, EyeTrace.HitPos - Vector(0, 0, 23), ang, ent, EyeTrace.PhysicsBone or 0)
	end

	if ix.config.Get("sitOnPlayers", true) then
		local veh
		if not EyeTrace.HitWorld and IsValid(EyeTrace.Entity) and EyeTrace.Entity:IsPlayer() and IsValid(EyeTrace.Entity:GetVehicle()) and EyeTrace.Entity:GetVehicle().playerdynseat then
			local safe = 256
			veh = EyeTrace.Entity:GetVehicle()
			while IsValid(veh.SittingOnMe) and IsValid(veh.SittingOnMe:GetDriver()) and safe > 0 do
				safe = safe - 1
				veh = veh.SittingOnMe
			end
		else
			for k, v in pairs(ents.FindInSphere(EyeTrace.HitPos, 12)) do
				local safe = 256
				veh = v
				while IsValid(veh.SittingOnMe) and IsValid(veh.SittingOnMe:GetDriver()) and safe > 0 do
					safe = safe - 1
					veh = veh.SittingOnMe
				end
			end
		end

		if IsValid(veh) and veh:GetClass() == "prop_vehicle_prisoner_pod" and veh:GetModel() ~= "models/vehicles/prisoner_pod_inner.mdl" and veh:GetDriver() and veh:GetDriver():IsValid() and not veh.PlayerSitOnPlayer then
			if IsValid(veh.holder) and veh.holder.GetTargetPlayer and veh.holder:GetTargetPlayer() == ply then return end
			
			if veh:GetDriver():GetInfoNum("sitting_allow_on_me", 1) == 0 then
				ply:ChatPrint(("%s отключил возможность сидеть на нём!"):format(veh:GetDriver():Name()))
				return false
			end

			local pose = FindPose(veh, ply)
			local pos = veh:GetDriver():GetPos()

			if veh.plyposhack then
				pos = veh:LocalToWorld(veh.plyposhack)
			end

			local vec, ang2 = LocalToWorld(pose.Pos, pose.Ang, pos, veh:GetAngles())

			if veh:GetParent() == ply then return false end

			ent = Sit(ply, vec, ang2, veh, 0, pose.Func, pose.OnExitFunc)

			if ent and IsValid(ent) then
				ent.PlayerOnPlayer = true
				veh.SittingOnMe = ent
			end

			return true, ent
		end
	else
		for k, v in pairs(ents.FindInSphere(EyeTrace.HitPos, 5)) do
			if v.playerdynseat then
				return false
			end
		end
	end

	local shouldSitOnPlayer = (ply.IsFlying and ply:IsFlying()) or EyeTrace.Entity == ply:GetGroundEntity() or ply:GetMoveType() == MOVETYPE_NOCLIP
	if IsValid(EyeTrace.Entity) and EyeTrace.Entity:IsPlayer() and ix.config.Get("sitOnPlayerEntities", true) and shouldSitOnPlayer then
		ent = EyeTrace.Entity
		if IsValid(ent:GetVehicle()) then return end

		local min, max = ent:GetCollisionBounds()
		local zadjust = math.abs(min.z) + math.abs(max.z)
		local seatPos = ent:GetPos() + Vector(0, 0, 10 + zadjust / 2)

		local vehicle = Sit(ply, seatPos, ply:GetAngles(), ent, EyeTrace.PhysicsBone or 0)
		return vehicle
	end

	if math.abs(surfaceAng.pitch) <= 15 then
		ang = Angle()
		local sampleResolution = 24
		local dists, distsang, ang_smallest_hori, smallest_hori = SitAnywhere.GetAreaProfile(EyeTrace.HitPos, sampleResolution, false)
		local infront = ((ang_smallest_hori or 0) + 180) % 360

		local cancelSit, seat = hook.Run("HandleSit", ply, dists, EyeTrace)
		if cancelSit then
			return seat
		end

		if ang_smallest_hori and distsang[infront].Hit and distsang[infront].Distance > 14 and smallest_hori <= 16 then
			local hori = distsang[ang_smallest_hori].HorizontalTrace
			ang.yaw = (hori.HitNormal:Angle().yaw - 90)
			return Sit(ply, EyeTrace.HitPos - Vector(0, 0, 23), ang, (not EyeTrace.HitWorld) and EyeTrace.Entity, EyeTrace.PhysicsBone or 0)
		else
			table.sort(dists, function(a,b) return b.Distance < a.Distance end)
			local wants = {}
			local eyeang = ply:EyeAngles() + Angle(0, 180, 0)
			for I = 1, #dists do
				local trace = dists[I]
				local behind = distsang[(trace.ang + 180) % 360]
				if behind.Distance2 > 3 then
					table.insert(wants, {
						cost = math.abs(eyeang.yaw - trace.ang),
						ang = trace.ang,
					})
				end
			end

			table.sort(wants,function(a,b) return b.cost > a.cost end)
			if #wants == 0 then return end
			ang.yaw = (wants[1].ang - 90)
			return Sit(ply, EyeTrace.HitPos - Vector(0, 0, 23), ang, (not EyeTrace.HitWorld) and EyeTrace.Entity, EyeTrace.PhysicsBone or 0)
		end
	end
end

local function checkAllowSit(ply)
	local allowSit = hook.Run("ShouldAllowSit", ply)

	local bottom, top = ply:GetHull()
	local diff = top.Z - bottom.Z
	local trace = util.QuickTrace(ply:GetPos(), Vector(0, 0, diff), player.GetAll())

	if not ix.config.Get("sitAllowTightPlaces", false) and trace.HitWorld then return false end
	if allowSit == false or allowSit == true then
		return allowSit
	end

	if ix.config.Get("sitAdminOnly", false) and not ply:IsAdmin() then
		return false
	end

	return true
end

local function sitcmd(ply)
	if not IsValid(ply) then return end
	if ply:InVehicle() then return end
	if not checkAllowSit(ply) then return end

	local now = CurTime()
	if NextUse[ply] > now then return end

	if ply:Sit() then
		NextUse[ply] = now + 1
	else
		NextUse[ply] = now + 0.1
	end
end

concommand.Add("sit", function(ply, cmd, args)
	sitcmd(ply)
end)

local function UndoSitting(ply)
	if not IsValid(ply) then return end

	local prev = ply.sitting_allowswep
	if prev ~= nil then
		ply.sitting_allowswep = nil
		ply:SetAllowWeaponsInVehicle(prev)
	end

	if ix.config.Get("sitPlayerDamage", false) then
		ply:SetCollisionGroup(COLLISION_GROUP_PLAYER)
		ply:CollisionRulesChanged()
	end

	if ply.seatExit then
		ply.seatExit(ply)
		ply.seatExit = nil
	end
end

-- Хуки
function PLUGIN:CanExitVehicle(seat, ply)
	if not IsValid(seat) or not IsValid(ply) then return end
	if not seat.playerdynseat then return end
	if CurTime() < NextUse[ply] then return false end
end

function PLUGIN:PlayerLeaveVehicle(ply, seat)
	if not IsValid(seat) or not IsValid(ply) then return end
	if not seat.playerdynseat then return end

	local oldpos = seat:LocalToWorld(seat.oldpos)
	ply:SetPos(oldpos)
	if ply.UnStuck then
		ply:UnStuck()
	end

	for _, v in next, seat:GetChildren() do
		if IsValid(v) and v.playerdynseat and IsValid(v.sittingPly) then
			v.sittingPly:ExitVehicle()
		end
	end

	SafeRemoveEntityDelayed(seat, 1)
	UndoSitting(ply)
end

function PLUGIN:AllowPlayerPickup(ply)
	if ply:KeyDown(IN_WALK) then
		return false
	end
end

function PLUGIN:PlayerDeath(pl)
	local veh = pl:GetVehicle()
	if IsValid(veh) and veh.playerdynseat then
		SafeRemoveEntity(veh)
	end

	for k, v in next, pl:GetChildren() do
		if IsValid(v) and v.playerdynseat and IsValid(v.sittingPly) then
			v.sittingPly:ExitVehicle()
		end
	end
end

function PLUGIN:PlayerEnteredVehicle(pl, veh)
	for k,v in next, pl:GetChildren() do
		if IsValid(v) and v.playerdynseat and IsValid(v.sittingPly) then
			v.sittingPly:ExitVehicle()
		end
	end

	DropEntityIfHeld(veh)

	local parent = veh:GetParent()
	if IsValid(parent) then
		DropEntityIfHeld(parent)
	end
end

function PLUGIN:OnPlayerSit(ply, pos, ang, parent, parentbone, vehicle)
	if IsValid(parent) and parent ~= game.GetWorld() then
		if parent:GetModel():sub(1, 6) ~= "models" then return false end
		if parent:IsPlayer() then
			if not ix.config.Get("sitOnPlayerEntities", true) then return false end
			if parent:GetInfoNum("sitting_allow_on_me", 1) == 0 then
				ply:ChatPrint(("%s отключил возможность сидеть на нём!"):format(parent:Name()))
				return false
			end

			if IsValid(parent:GetVehicle()) then return false end
		end
	end
end

function PLUGIN:EntityRemoved(ent)
	if ent.playerdynseat and IsValid(ent.sittingPly) then
		UndoSitting(ent.sittingPly)
	end

	for _, v in next, ent:GetChildren() do
		if IsValid(v) and v.playerdynseat and IsValid(v.sittingPly) then
			v.sittingPly:ExitVehicle()
		end
	end
end

function PLUGIN:PhysgunPickup(ply, ent)
	if ix.config.Get("sitAntiPropSurf", true) then
		local function CheckSeat(checkPly, checkEnt, tbl)
			if not checkPly:InVehicle() then return true end

			local vehicle = checkPly:GetVehicle()
			local parent = vehicle.parent

			if parent == checkEnt then return false end

			for _,v in next, checkEnt:GetChildren() do
				if IsValid(v) and not tbl[v] then
					tbl[v] = true
					if v ~= checkEnt and CheckSeat(checkPly, v, tbl) == false then
						return false
					end
				end
			end

			local cEnts = constraint.GetAllConstrainedEntities(checkEnt)
			if cEnts then
				for _,v in next, cEnts do
					if IsValid(v) and not tbl[v] then
						tbl[v] = true
						if v ~= checkEnt and CheckSeat(checkPly, v, tbl) == false then
							return false
						end
					end
				end
			end
		end
		
		if IsValid(ply:GetVehicle()) and ply:GetVehicle().playerdynseat then
			if CheckSeat(ply, ent, {}) == false then
				return false
			end
		end
	end
end

function PLUGIN:CanTool(ply, tr)
	if ix.config.Get("sitAntiToolAbuse", true) and IsValid(tr.Entity) then
		local function CheckSeat(checkPly, checkEnt, tbl)
			if not checkPly:InVehicle() then return true end

			local vehicle = checkPly:GetVehicle()
			local parent = vehicle.parent

			if parent == checkEnt then return false end

			for _,v in next, checkEnt:GetChildren() do
				if IsValid(v) and not tbl[v] then
					tbl[v] = true
					if v ~= checkEnt and CheckSeat(checkPly, v, tbl) == false then
						return false
					end
				end
			end

			local cEnts = constraint.GetAllConstrainedEntities(checkEnt)
			if cEnts then
				for _,v in next, cEnts do
					if IsValid(v) and not tbl[v] then
						tbl[v] = true
						if v ~= checkEnt and CheckSeat(checkPly, v, tbl) == false then
							return false
						end
					end
				end
			end
		end
		
		if IsValid(ply:GetVehicle()) and ply:GetVehicle().playerdynseat then
			if CheckSeat(ply, tr.Entity, {}) == false then
				return false
			end
		end
	end
end

timer.Create("SitAnywhere_RemoveSeats", 15, 0, function()
	for k,v in pairs(ents.FindByClass("prop_vehicle_prisoner_pod")) do
		if v.playerdynseat and (not IsValid(v.sittingPly) or v:GetDriver() == nil or not v:GetDriver():IsValid() or v:GetDriver():GetVehicle() ~= v) then
			v:Remove()
		end
	end
end)
