--[[
| This file was obtained through the combined efforts
| of Madbluntz & Plymouth Antiquarian Society.
|
| Credits: lifestorm, Gregory Wayne Rossel JR.,
| 	Maloy, DrPepper10 @ RIP, Atle!
|
| Visit for more: https://plymouth.thetwilightzone.ru/
--]]

-- Updates the weather for given players

local lastSet = 0
local CurrentWeather
local CurrentPercent = 1

local function isColor(t)
	return t.r and t.g and t.b and true or false
end
local function Blender(nFraction, vFrom, vTo) -- Will it blend?
	-- Nils should be false, if one of them is a boolean
	if type(vFrom) == "nil" and type(vTo) == "boolean" then
		vFrom = false
	end
	if type(vTo) == "nil" and type(vFrom) == "boolean" then
		vTo = false
	end
	-- If the same value, then return it
	if vTo == vFrom then return vTo end
	-- In case of two diffrent variables.
	if type(vFrom) ~= type(vTo) then
		StormFox2.Warning("Mixer called with values of two different types[" .. type(vFrom) .. "," .. type(vTo) .. "]")
		debug.Trace()
		return vFrom
	elseif type(vTo) == "string" or type(vTo) == "IMaterial" or type(vTo) == "boolean" then -- String, material or bool. Return vTo.
		return vTo
	elseif type(vTo) == "number" then -- Number
		return Lerp(nFraction, vFrom, vTo)
	elseif type(vTo) == "table" and isColor(vTo) then -- Color
		local r = Lerp( nFraction, vFrom.r or 255, vTo.r )
		local g = Lerp( nFraction, vFrom.g or 255, vTo.g )
		local b = Lerp( nFraction, vFrom.b or 255, vTo.b )
		local a = Lerp( nFraction, vFrom.a or 255, vTo.a )
		return Color( r, g, b, a )
	end
	--StormFox2.Warning("ERROR: Unsupported mix value type[" .. type(vTo) .. "]. Returning original value")
	--debug.Trace()
	return vFrom
end

local function IsSame(sName, nPercentage, nDelta)
	if CurrentPercent ~= nPercentage then return false end
	if not CurrentWeather then return false end
	return CurrentWeather.Name == sName
end

local function ApplyWeather(sName, nPercentage, nDelta)
	hook.Run("StormFox2.weather.prechange", sName ,nPercentage )
	if nDelta and nDelta <= 0 then
		nDelta = nil
	elseif nDelta then
		local sp = StormFox2.Time.GetSpeed_RAW()
		if sp > 0 then
			nDelta = nDelta / sp
		end
	end
	local bSameWeather = sName == (CurrentWeather and CurrentWeather.Name or "Clear")
	if CurrentWeather and CurrentWeather.OnChange then
		CurrentWeather:OnChange( sName, nPercentage, nDelta )
	end
	if CurrentWeather and CurrentWeather.OnRemove and not bSameWeather then
		CurrentWeather:OnRemove( sName, nPercentage, nDelta )
	end
	local clear = StormFox2.Weather.Get( "Clear" )
	CurrentWeather = StormFox2.Weather.Get( sName )
	CurrentPercent = nPercentage
	local stamp = StormFox2.Sky.GetLastStamp()
	
	if sName == "Clear" then
		nPercentage = 1
	end
	if nPercentage >= 1 then
		for _,key in ipairs( StormFox2.Weather.GetKeys() ) do
			local v = CurrentWeather:Get( key, stamp )
			if type(v) == "table" and not (v.r and v.g and v.b) then
				StormFox2.Data.Set(key, v)
			else
				StormFox2.Data.Set(key, v, nDelta)
			end
		end
	elseif nPercentage <= 0 then
		for _,key in ipairs( StormFox2.Weather.GetKeys() ) do
			StormFox2.Data.Set(key, clear:Get( key, stamp ), nDelta)
		end
	else -- Mixing bin
		for _,key in ipairs( StormFox2.Weather.GetKeys() ) do
			local var2,b_nomix = CurrentWeather:Get( key, stamp )
			local d = nDelta
			if type(var2) == "table" and not (var2.r and var2.g and var2.b) then
				d = nil
			end
			if b_nomix then
				StormFox2.Data.Set(key, var2, d)
			else
				local var1 = clear:Get( key, stamp )
				if var2 and not var1 then -- This is not a default variable
					if type(var2) == "number" then
						var1 = 0
					end
				end
				if not var1 and var2 then -- THis is not a default varable
					StormFox2.Data.Set(key, var2, d)
				elseif var1 and var2 then
					StormFox2.Data.Set(key, Blender(nPercentage, var1, var2), d)
				end
			end
		end
	end
	if CurrentWeather.Init and not bSameWeather then
		CurrentWeather.Init()
	end
	if CurrentWeather.Tick10 then
		CurrentWeather.Tick10()
	end
	hook.Run("StormFox2.weather.postchange", sName ,nPercentage, nDelta )
end

hook.Add("StormFox2.Sky.StampChange","StormFox2.Weather.Stamp",function(_,nLerpTime)
	ApplyWeather(CurrentWeather and CurrentWeather.Name or "Clear", CurrentPercent, nLerpTime)
end)

---Returns the current weather-type.
---@return Weather
function StormFox2.Weather.GetCurrent()
	return CurrentWeather or StormFox2.Weather.Get( "Clear" )
end

---Returns the current weather percent.
---@return number Percent
function StormFox2.Weather.GetPercent()
	return StormFox2.Data.Get("w_Percentage",CurrentPercent) 
end

---Returns the weather percent we're lerping to.
---@return number
function StormFox2.Weather.GetFinishPercent()
	return CurrentPercent
end

---Returns the current weather description. Like 'Snow', 'Storm' .. ect.
---Second argument isn't translated.
---@return string Description
---@return string Description_Untranslated
function StormFox2.Weather.GetDescription()
	local c = StormFox2.Weather.GetCurrent()
	if not c.GetName then
		return c.Name
	end
	local a,b = c:GetName(StormFox2.Time.Get(), StormFox2.Temperature.Get(), StormFox2.Wind.GetForce(), StormFox2.Thunder.IsThundering(), StormFox2.Weather.GetPercent())
	return a,b or a
end

local errM = Material("error")

---Returns the current weather-icon.
---@return userdata Material
function StormFox2.Weather.GetIcon()
	local c = StormFox2.Weather.GetCurrent()
	if not c.GetIcon then
		return errM
	end
	return c.GetIcon(StormFox2.Time.Get(), StormFox2.Temperature.Get(), StormFox2.Wind.GetForce(), StormFox2.Thunder.IsThundering(), StormFox2.Weather.GetPercent())
end

local SF_UPDATE_WEATHER = 0
local SF_INIT_WEATHER 	= 1

if SERVER then
	local l_data

	---Sets the weather.
	---@server 
	---@param sName string
	---@param nPercentage number
	---@param nDelta? number
	---@return boolean success
	function StormFox2.Weather.Set( sName, nPercentage, nDelta )
		if not StormFox2.Setting.GetCache("enable", true) then return end -- Just in case
		if nDelta and l_data and nDelta == l_data then
			if IsSame(sName, nPercentage) then return false end
		end
		l_data = nDelta
		-- Default vals
		if not nDelta then
			nDelta = 4
		end
		if not nPercentage then 
			nPercentage = 1
		end
		-- Unknown weathers gets replaced with 'Clear'
		if not StormFox2.Weather.Get( sName ) then
			StormFox2.Warning("Unknown weather: " .. tostring(sName))
			sName = "Clear"
		end
		-- In case we set the weather to clear, change it so it is the current weather at 0 instead
		if sName == "Clear" and CurrentWeather and nDelta > 0 then
			nPercentage = 0
			sName = CurrentWeather.Name
		elseif sName == "Clear" then
			nPercentage = 1
		end
		lastSet = CurTime()
		net.Start( StormFox2.Net.Weather )
			net.WriteBit(SF_UPDATE_WEATHER)
			net.WriteUInt( math.max(0, StormFox2.Data.GetLerpEnd( "w_Percentage" )), 32)
			net.WriteFloat(nPercentage)
			net.WriteString(sName)
			net.WriteFloat(CurTime() + nDelta)
		net.Broadcast()
		ApplyWeather(sName, nPercentage, nDelta)
		if sName == "Clear" then
			nPercentage = 0
		end
		StormFox2.Data.Set("w_Percentage",nPercentage,nDelta)
		return true
	end
	net.Receive( StormFox2.Net.Weather, function(len, ply) -- OI, what weather?
		local lerpEnd = StormFox2.Data.GetLerpEnd( "w_Percentage" )
		net.Start( StormFox2.Net.Weather )
			net.WriteBit(SF_INIT_WEATHER)
			net.WriteUInt( math.max(0, StormFox2.Data.GetLerpEnd( "w_Percentage" )), 32)
			net.WriteFloat( CurrentPercent )
			net.WriteString( StormFox2.Weather.GetCurrent().Name )
			net.WriteFloat( StormFox2.Data.Get("w_Percentage",CurrentPercent) )
		net.Send(ply)
	end)
	-- Handles the terrain logic
	timer.Create("StormFox2.terrain.updater", 4, 0, function()
		local cW = StormFox2.Weather.GetCurrent()
		local cT = StormFox2.Terrain.GetCurrent()

		if not cW then return end -- No weather!?
		local terrain = cW:Get("Terrain")
		if not cT and not terrain then return end -- No terrain detected
		if cT and terrain and cT == terrain then return end -- Same terrain detected
		if terrain then -- Switch terraintype out. This can't be the same as the other
			StormFox2.Terrain.Set(terrain.Name)
		elseif not terrain and not cT.lock then -- This terrain doesn't have a lock. Reset terrain
			StormFox2.Terrain.Reset()
		elseif not terrain and cT.lock then -- Check the lock of cT and see if we can reset
			if cT:lock() then -- Lock tells us we can reset the terrain
				StormFox2.Terrain.Reset()
			end
		end
	end)
	local tS = CurTime()
	-- In case no weather was set
	timer.Simple(8, function()
		-- Clear up weather when it reaches 0
		timer.Create("StormFox2.weather.clear",1,0,function()
			if not CurrentWeather then return end
			if CurrentWeather.Name == "Clear" then return end
			local p = StormFox2.Weather.GetPercent()
			if p <= 0 then
				StormFox2.Weather.Set("Clear", 1, 0)
			end
		end)
		if CurrentWeather then return end
		StormFox2.Weather.Set("Clear", 1, 0)
	end)
else
	local hasLocalWeather = false
	local svWeather
	local function SetW( sName, nPercentage, nDelta )
		-- Block same weather
		if IsSame(sName, nPercentage) then return false end
		ApplyWeather(sName, nPercentage, nDelta)
		if sName == "Clear" then
			nPercentage = 0
		end
		StormFox2.Data.Set("w_Percentage",nPercentage,nDelta)
	end

	---Sets the weather on the client. Server-side stuff won't be set.
	---@client
	---@param sName? string
	---@param nPercentage? number
	---@param nDelta? number
	---@param nTemperature? number
	function StormFox2.Weather.SetLocal( sName, nPercentage, nDelta, nTemperature)
		-- If nil then remove the local weather
		if not sName then
			return StormFox2.Weather.RemoveLocal()
		end
		-- Unknown weathers gets replaced with 'Clear'
		if not StormFox2.Weather.Get( sName ) then
			StormFox2.Warning("Unknown weather: " .. tostring(sName))
			sName = "Clear"
		end
		if not hasLocalWeather then
			svWeather = {StormFox2.Weather.GetCurrent().Name, StormFox2.Weather.GetFinishPercent(), StormFox2.Temperature.Get()}
		end
		StormFox2.Temperature.SetLocal(nTemperature)
		-- Block same weather
		SetW(sName, nPercentage or 1, nDelta)
		hasLocalWeather = true
	end

	---Removes the local weather.
	---@client
	function StormFox2.Weather.RemoveLocal()
		if not hasLocalWeather then return end
		SetW(svWeather[1], svWeather[2], 4)
		StormFox2.Temperature.SetLocal(nil)
		svWeather = nil
		hasLocalWeather = false
	end
	net.Receive( StormFox2.Net.Weather, function(len)
		local flag = net.ReadBit() == SF_UPDATE_WEATHER
		local wTarget = net.ReadUInt(32)
		local nPercentage = net.ReadFloat()
		local sName = net.ReadString()
		if flag then
			local nDelta = net.ReadFloat() - CurTime()
			-- Calculate the time since server set this
			if not hasLocalWeather then
				SetW(sName, nPercentage, nDelta)
			else
				svWeather[1] = sName
				svWeather[2] = nPercentage
			end
		else
			local current = net.ReadFloat()
			if not hasLocalWeather then
				local secondsLeft = wTarget - CurTime()
				if secondsLeft <= 0 then
					SetW(sName, nPercentage, 0)
				else
					SetW(sName, current, 0)
					SetW(sName, nPercentage, secondsLeft)
				end
			else
				svWeather[1] = sName
				svWeather[2] = nPercentage
			end
		end
	end)
	-- Ask the server what weather we have
	hook.Add("StormFox2.InitPostEntity", "StormFox2.terrain", function()
		net.Start( StormFox2.Net.Weather )
		net.SendToServer()
	end)
end

hook.Add("Think", "StormFox2.Weather.Think", function()
	if not CurrentWeather then return end
	if not CurrentWeather.Think then return end
	if not StormFox2.Setting.SFEnabled() then return end
	CurrentWeather:Think()
end)

timer.Create("StormFox2.Weather.tickslow", 1, 0, function()
	if not CurrentWeather then return end
	if not CurrentWeather.TickSlow then return end
	CurrentWeather.TickSlow()
end)

hook.Add("StormFox2.weather.postchange", "StormFox2.weather.slowtickinit", function()
	if not CurrentWeather then return end
	if not CurrentWeather.TickSlow then return end
	CurrentWeather.TickSlow()
end)

if CLIENT then
	local c_tab = {"PostDrawTranslucentRenderables", "PreDrawTranslucentRenderables", "HUDPaint"}
	for i,v in ipairs(c_tab) do
		hook.Add(v, "StormFox2.Weather." .. v, function(...)
			if not CurrentWeather then return end
			if not CurrentWeather[v] then return end
			CurrentWeather[v](...)
		end)
	end
end

-- Some functions to make it easier.

---Returns true if it is raining, or if current weather is child of rain.
---@return boolean
---@shared
function StormFox2.Weather.IsRaining()
	local wT = StormFox2.Weather.GetCurrent()
	if wT.Inherit == "Rain" then return true end
	if wT.Name ~= "Rain" then return false end
	return StormFox2.Temperature.Get() > -2 or false
end

---Returns true if it is snowing.
---@return boolean
---@shared
function StormFox2.Weather.IsSnowing()
	local wT = StormFox2.Weather.GetCurrent()
	if wT.Name ~= "Rain" then return false end
	return StormFox2.Temperature.Get() <= -2 or false
end

---Returns the rain / snow amount. Between 0 - 1.
---@return number
---@shared
function StormFox2.Weather.GetRainAmount()
	if not StormFox2.Weather.IsRaining() then return 0 end
	return StormFox2.Weather.GetPercent()
end

---Returns true if the current weather is raining, snowing or inherit from rain.
---@return boolean
---@shared
function StormFox2.Weather.HasDownfall()
	local wT = StormFox2.Weather.GetCurrent()
	if wT.Inherit == "Rain" then return true end
	return wT.Name == "Rain"
end

-- Downfall

---Returns true if the entity is hit by rain or any downfall.
---@param eEnt Entity
---@param bDont_cache boolean
---@return boolean
---@shared
function StormFox2.DownFall.IsEntityHit(eEnt, bDont_cache)
	if not StormFox2.Weather.HasDownfall() then return false end
	return (StormFox2.Wind.IsEntityInWind(eEnt,bDont_cache))
end

---Checks to see if the given point is hit by rain.
---@param vPos Vector
---@return boolean
---@shared
function StormFox2.DownFall.IsPointHit(vPos)
	if not StormFox2.Weather.HasDownfall() then return false end
	local t = util.TraceLine( {
		start = vPos,
		endpos = vPos + -StormFox2.Wind.GetNorm() * 262144,
		mask = StormFox2.DownFall.Mask
	} )
	return t.HitSky
end