local lib = starlit.mod.lib
local world = starlit.world

function world.date()
	local days = minetest.get_day_count()
	local year = math.floor(days / world.planet.orbit);
	local day = days % world.planet.orbit;
	return {
		year = year, day = day;
		season = day / world.planet.orbit + 0.5; -- begin summer
	}
end
local lerp = lib.math.lerp

local function gradient(grad, pos)
	local n = #grad
	if n == 1 then return grad[1] end
	local op = pos*(n-1)
	local idx = math.floor(op)
	local t = op-idx
	return lerp(t, grad[1 + idx], grad[2 + idx])
end

local altitudeCooling = 10 / 100
local heatRange = {min = -70, max = 70} -- translate mt temps into real temps

-- this function provides the basis for temperature calculation,
-- which is performed by adding this value to the ambient temperature,
-- determined by querying nearby group:heatSource items in accordance
-- with the inverse-square law
function world.climate.eval(pos, tod, season)
	local data = minetest.get_biome_data(pos)
	local biome = world.ecology.biomes.db[minetest.get_biome_name(data.biome)]
	local heat, humid = data.heat, data.humidity
	heat = lerp(heat/100, heatRange.min, heatRange.max)
	tod = tod or minetest.get_timeofday()
	heat = lerp(math.abs(tod - 0.5)*2, heat, heat + biome.nightTempDelta)
-- 	print('base heat', heat)

	local td = world.date()
	heat = heat + gradient(biome.seasonalTemp, season or td.season)
-- 	print('seasonal heat', heat)
	if pos.y > 0 then
		heat = heat - pos.y*altitudeCooling 
	end
-- 	print('altitude heat', heat)

	return {
		surfaceTemp = heat;
		waterTemp = heat + biome.waterTempDelta;
		surfaceHumid = humid;
	}
end

local vdsq = lib.math.vdsq
function world.climate.temp(pos) --> irradiance at pos in W
	local cl = world.climate.eval(pos)
	local radCenters = starlit.region.radiator.store:get_areas_for_pos(pos, false, true)
	local irradiance = 0
	for _,e in pairs(radCenters) do
		local rpos = minetest.string_to_pos(e.data)
		local rdef = assert(minetest.registered_nodes[assert(minetest.get_node(rpos)).name])
		local rc = rdef._starlit.radiator
		local r_max = rc.radius(rpos)

		local dist_sq = vdsq(rpos,pos)
		if dist_sq <= r_max^2 then
			-- cheap bad way
			-- if minetest.line_of_sight(rpos,pos) then
			--
			-- expensive way
			local obstruct = 0
			local ray = Raycast(rpos, pos, true, true)
			for p in ray do
				if p.type == 'node' then obstruct = obstruct + 1 end
			end

			if obstruct < 4 then
				local power, customFalloff = rc.radiate(rpos, pos)
				-- okay this isn't the real inverse square law but i 
				-- couldn't figure out a better way to simplify the
				-- model without checking an ENORMOUS number of nodes
				-- maybe someone else who isn't completely
				-- mathtarded can do better.
				if not customFalloff then
					power = power * (1 - (dist_sq / ((r_max+1)^2)))
				end
				power = power * (1 - (obstruct/5))
				irradiance = irradiance + power
			end
		end
	end
	return irradiance + cl.surfaceTemp
end

world.ecology.biomes.foreach('starlit:biome-gen', {}, function(id, b)
	b.def.name = id
	minetest.register_biome(b.def)
end)

world.ecology.plants.foreach('starlit:plant-gen', {}, function(id, b)
	local stageCt = #b.stages
	local function stageID(n)
		if n == stageCt then return id end
		return id .. string.format('_stage_%s', n)
	end
	b.stageNodes = {}
	b.req = b.req or {}
	local function regStage(n, st)
		local base = {
			description = b.name;
			drawtype = "plantlike";
			tiles = { tostring(st.tex) };
			paramtype = "light";
			paramtype2 = "meshoptions";
			place_param2 = b.meshOpt;
			walkable = false;
			buildable_to = true;
			groups = {
				plant = 1;
				plant_grow = stageCt ~= n and 1 or 0;
				attached_node = 3;
			};
			drop = st.drop;
			_starlit = {
				plant = {
					id = id, stage = n;
				};
				recover = starlit.type.fab {
					time = { shred = .3; };
					cost = { shredPower = 1; };
				};
				recover_vary = function(rng, ctx)
					return starlit.type.fab {
						element = {
							carbon    = rng:int(0,1);
							potassium = rng:int(0,1);
						}
					};
				end;
			};
		}
		if st.swap then
			base.node_dig_prediction = ""
			function base.after_dig_node(pos, node, digger)
				node.name = stageID(st.swap)
				minetest.swap_node(pos, node)
				return true
			end
		end
		if st.biolum then base.light_source = st.biolum; end
		return base
	end
	for i, v in ipairs(b.stages) do
		local n = regStage(i, v)
		minetest.register_node(stageID(i), n)
		b.stageNodes[i] = stageID(i)
	end
	b.fullyGrown = stageID(stageCt)

	local dec = {
		deco_type = 'simple';
		decoration = b.stageNodes;
		height = 1;
		param2 = b.meshOpt or 0;
	}
	for k,v in pairs(b.decoration) do dec[k] = v end
	b.decoration = minetest.register_decoration(dec)
end)

local toward = lib.math.toward
local hfinterval = 1.5
starlit.startJob('starlit:heatflow', hfinterval, function(delta)

	-- our base thermal conductivity (κ) is measured in °C/°C/s. say the
	-- player is in -30°C weather, and has an internal temperature of
	-- 10°C. then:
	--   κ  = .1°C/C/s (which is apparently 100mHz)
	--   Tₚ =  10°C
	--   Tₑ = -30°C
	--   d  = Tₑ − Tₚ = -40°C
	--   ΔT = κ×d = -.4°C/s
	-- too cold:
	--		x = beginning of danger zone
	--    κ × (x - Tₚ) = y where y < Tₚ
	-- our final change in temperature is computed as tΔC where t is time
	local kappa = starlit.constant.heat.thermalConductivity
	for name,user in pairs(starlit.activeUsers) do
		local tr = user:species().tempRange
		local t = starlit.world.climate.temp(user.entity:get_pos())

		do -- this bit probably belongs in starlit:bio but we do it here in order
		   -- to spare ourselves another call into the dark swamp of climate.temp
		   local urg = 1
		   local function alarm(kind)
			   user:alarm(urg, kind, nil, {
				   elt = user.hud.elt.temp, ofs = {x=100,y=0};
				   tex = 'starlit-ui-alert-'..kind..'.png';
			   })
		   end
		   local hz = user:tempHazard(t)
			local tr = user:species().tempRange.survivable
		   if hz == 'cold' then
			   if tr[1] - t > 7 then urg = 2 end
			   alarm 'temp-cold'
		   elseif hz == 'hot' then
			   if t - tr[2] > 7 then urg = 2 end
			   alarm 'temp-hot'
		   end
		end

		local insul = 0
		local naked = user:naked()
		local suitDef
		if not naked then
			suitDef = user:suitStack():get_definition()
			insul = suitDef._starlit.suit.temp.insulation
		end

		local warm = user:effectiveStat 'warmth'
		local tSafeMin, tSafeMax = tr.survivable[1], tr.survivable[2]
		local tComfMin, tComfMax = tr.comfort[1], tr.comfort[2]

		local tDelta = (kappa * (1-insul)) * (t - warm) * hfinterval
		local tgt = warm + tDelta

		-- old logic: we move the user towards the exterior temperature, modulated
		-- by her suit insulation.
		--local tgt = toward(warm, t, hfinterval * thermalConductivity * (1 - insul))

		if not naked then
			local suit = user:getSuit()
			local suitPower = suit:powerState()
			local suitPowerLeft = suit:powerLeft()
			if suitPower ~= 'off' then
				local coilPower = 1.0
				local st = suitDef._starlit.suit.temp
				if suitPower == 'powerSave' and (tgt >= tSafeMin and tgt <= tSafeMax) then coilPower = 0.5 end
				if tgt < tComfMin and st.maxHeat > 0 then
					local availPower = user:suitDrawCurrent(st.heatPower*coilPower, hfinterval)
					tgt = tgt + (availPower / st.heatPower) * st.maxHeat * coilPower * hfinterval
				end
				if tgt > tComfMax and st.maxCool > 0 then
					local availPower = user:suitDrawCurrent(st.coolPower*coilPower, hfinterval)
					tgt = tgt - (availPower / st.coolPower) * st.maxCool * coilPower * hfinterval
				end
			end
		end

		user:statDelta('warmth', tgt - warm) -- dopey but w/e

		warm = tgt -- for the sake of readable code

		-- does this belong in starlit:bio? unsure tbh
		if warm < tSafeMin or warm > tSafeMax then
			local dv
			if warm < tSafeMin then
				dv = math.abs(warm - tSafeMin)
			else
				dv = math.abs(warm - tSafeMax)
			end
			-- for every degree of difference you suffer 2 points of damage/s
			local dmg = math.ceil(dv * 2)
			user:statDelta('health', -dmg)
		end
	end
end)


world.ecology.trees.foreach('starlit:tree-gen', {}, function(id, t)
	for i,td in ipairs(t.decorate) do
		local dec = {
			deco_type = 'lsystem';
			treedef = t.def;
		}
		for k,v in pairs(td) do dec[k]=v end
		minetest.register_decoration(dec)
	end
end)

minetest.register_abm {
	label = "plant growth";
	nodenames = {'group:plant_grow'};
	chance = 15;
	interval = 20;
	catch_up = true;
	action = function(pos, node)
		local def = minetest.registered_nodes[node.name]._starlit.plant
		-- 5 W: maximum power for UV lamps
		-- 7 W: maximum solar power
		local uv = (minetest.get_natural_light(pos) / 15) * 7
		-- TODO compute artificial contribution
		local req = lib.tbl.defaults({
			uv = 3;
			soil = 'soil';
			temp = -10;
			humid = nil;
		}, def.growReq);

		-- TODO check other reqs

		if uv > req.uv then
			local plant = starlit.world.ecology.plants.db[def.id]
			local nextStage = plant.stageNodes[def.stage + 1]
			minetest.swap_node(pos, {name=nextStage})
		end
	end;
}