local clamp = function(a, min, max)
    return math.min(math.max(a, min), max)
end

local regw = pmb_weather.regisered_weathers

pmb_weather.night_brightness = world_storage.wget("pmb_weather_night_brightness") or 0.15

local new_meta = function() return {
    age = 0,
} end

local wbiome = {
    default = "none"
}

local chance_total = 0

local pl = {}

local current_weather_instance = nil
local current_def = nil

--------------------------------------------
-- biomes
--------------------------------------------

local _to_add_biomes = {}

-- allow for biomes to set their own weathers
function pmb_weather.allow_biome_weathers(biome, weathers, remove)
    _to_add_biomes[#_to_add_biomes+1] = {biome=biome, weathers=weathers, remove=remove}
end

local function assign_extra_biome_weathers()
    for j, task in ipairs(_to_add_biomes) do
        for i, weather_name in ipairs(task.weathers) do
            local wdef = regw[weather_name]
            if wdef then
                if not task.remove then
                    wdef.biomes[#wdef.biomes+1] = weather_name
                elseif task.remove then
                    local index = table.indexof(wdef.biomes, weather_name)
                    if index > 0 then
                        table.remove(wdef.biomes, index)
                    end
                end
            end
        end
    end
    _to_add_biomes = nil
end

--------------------------------------------
-- extra
--------------------------------------------

function pmb_weather.fix_timeofday(player, factor, force, override)
    local real_time = minetest.get_timeofday()
    local bullshit_time = 0
    -- don't ask.
    if real_time > 0.5 then
        bullshit_time = 1 - clamp((real_time - 0.7) / 0.1, 0, 1)
    else
        bullshit_time = (real_time - 0.15) / 0.1
    end

    if not force then
        pl[player].light_factor = (pl[player].light_factor) * 0.95 + (factor or 1) * 0.05
    else
        pl[player].light_factor = factor
    end

    if override then
        bullshit_time = override
    else
        bullshit_time = clamp(bullshit_time, pmb_weather.night_brightness, 1) * pl[player].light_factor
        bullshit_time = clamp(bullshit_time, pmb_weather.night_brightness * 0.75, 1)
    end

    if math.abs(bullshit_time - pl[player].last_bullshit_time) > 0.01 then
        player:override_day_night_ratio(bullshit_time)
        pl[player].last_bullshit_time = bullshit_time
    end
end

function pmb_weather.color(r,g,b,a)
    if not a then a = 256 end
    return {a=a,r=r,g=g,b=b}
end

--------------------------------------------
-- player meta
--------------------------------------------

local function check_player(player)
    if not pl[player] then
        pl[player] = {
            weather = "",
            to_sky = nil,
            time_until_can_change_sky = 1,
            particlespawners = {},
            sounds = {},
            shadow_intensity = 0.33,
            shadow_last_target = 0.33,
            shadow_target = 0.33,
            shadow_update = 0.2,
            light_factor = 0.5,
            last_bullshit_time = 0.5
        }
        player:set_lighting({ shadows = { intensity = pl[player].shadow_intensity} })
    end
end

function pmb_weather.request_sky(player, sky_table)
    check_player(player)
    local pli = pl[player]
    if pli.time_until_can_change_sky > 0 then
        pli.to_sky = sky_table
    else
        pli.to_sky = sky_table
        player:set_sky(pli.to_sky)
        pli.time_until_can_change_sky = 1
    end
end

-- lets the system smoothly interpolate lighting intensity instead of forcing it instantly
function pmb_weather.request_lighting(player, intensity)
    check_player(player)
    pl[player].shadow_last_target = pl[player].shadow_target
    pl[player].shadow_target = intensity
end

-- forcibly sets the lighting intensity instantly
function pmb_weather.force_set_lighting(player, intensity)
    check_player(player)
    local pli = pl[player]
    pli.shadow_target = intensity
    pli.shadow_intensity = intensity
    pli.shadow_last_target = intensity
    player:set_lighting({ shadows = { intensity = pli.shadow_intensity} })
end

function pmb_weather.on_player_enter_weather(player, weatherdef)
    weatherdef.on_enter_weather(player, pl[player], pl[player].weather)
end
function pmb_weather.on_player_leave_weather(player, weatherdef)
    weatherdef.on_leave_weather(player, pl[player], pl[player].weather)
end

local update_tick = 0.1
function pmb_weather.on_player_step_weather(player, weatherdef, dtime)
    weatherdef.on_step(player, pl[player], dtime)
    if update_tick > 0 then update_tick = update_tick - dtime return else update_tick = 0.1 end
    pmb_weather.fix_timeofday(player, weatherdef.light_factor)
end

minetest.register_on_leaveplayer(function(player, timed_out)
    if pl[player] then
        pmb_weather.on_player_leave_weather(player, regw[pl[player].weather])
    end
end)

local function shadow_intensity_interpolate(player, dtime)
    local pli = pl[player]
    if pli.shadow_update > 0 then pli.shadow_update = pli.shadow_update - dtime return
    else pli.shadow_update = 0.2 end

    local l = pli.shadow_intensity
    local t = pli.shadow_target

    if math.abs(l - t) < 0.06 then return end

    local dir = (t > l and 1) or -1

    pli.shadow_intensity = pli.shadow_intensity + (dir * 0.05)

    player:set_lighting({ shadows = { intensity = pli.shadow_intensity} })
end

local function do_player_weather(player, dtime)
    check_player(player)
    local pli = pl[player]
    shadow_intensity_interpolate(player, dtime)
    if pli.time_until_can_change_sky > 0 then
        pli.time_until_can_change_sky = pli.time_until_can_change_sky - dtime
    elseif pli.to_sky then
        player:set_sky(pli.to_sky)
        pli.time_until_can_change_sky = 2
    end

    if pli.weather_override then return end

    local biome = minetest.get_biome_name((minetest.get_biome_data(player:get_pos()) or {}).biome)
    local biome_weather_name = wbiome[biome]
    local new_weatherdef = regw[biome_weather_name or "none"]
    local old_weatherdef = regw[pli.weather]

    if biome_weather_name ~= pli.weather then
        if old_weatherdef then
            pmb_weather.on_player_leave_weather(player, old_weatherdef)
        end
        if new_weatherdef then
            pmb_weather.on_player_enter_weather(player, new_weatherdef)
        end
        pli.weather = biome_weather_name or "none"
    elseif new_weatherdef then
        pmb_weather.on_player_step_weather(player, new_weatherdef, dtime)
    end
end

function pmb_weather.override_player_weather(player, weather_name)
    check_player(player)
    local old_weatherdef = regw[pl[player].weather or "none"]
    local new_weatherdef = regw[weather_name]

    if old_weatherdef then
        pmb_weather.on_player_leave_weather(player, old_weatherdef)
    end
    if new_weatherdef then
        pmb_weather.on_player_enter_weather(player, new_weatherdef)
    end

    pl[player].weather = weather_name
    pl[player].weather_override = true
end
function pmb_weather.unoverride_player_weather(player)
    check_player(player)
    pl[player].weather_override = false
end

--------------------------------------------
-- weather ticks
--------------------------------------------

-- generate a weather instance used to tick weather
local function new_weather_instance(weather_name)
    local d = regw[weather_name] or regw["clear"]
    return {
        name = weather_name,
        fallbacks = d.fallbacks,
        time = (math.random() * (d.max_duration - d.min_duration)) + d.min_duration,
    }
end

local function propagate_biomes()
    if (not current_def) or (not current_weather_instance) then
        minetest.log("error", "pmb_weather --> propagate_biomes unable to propagate.")
        return
    end
    for biome_name, old_weather_name in pairs(wbiome) do
        if (#current_def.biomes == 0 and biome_name ~= "default") or (table.indexof(current_def.biomes, biome_name) > 0) then
            wbiome[biome_name] = current_weather_instance.name
        else
            for k, fallback in pairs(current_def.fallbacks) do
                local fallback_def = regw[fallback]
                if fallback_def and (#fallback_def.biomes == 0 and biome_name ~= "default") or (table.indexof(fallback_def.biomes, biome_name) > 0) then
                    wbiome[biome_name] = fallback
                    break
                end
            end
        end
    end
end

-- set weather to something
function pmb_weather.set_weather(weather_name)
    local ins = new_weather_instance(weather_name)
    current_weather_instance = ins
    current_def = regw[weather_name]
    world_storage.set("pmb_weather", "current_weather", current_weather_instance)
    propagate_biomes()
end

-- NOT IMPLEMENTED
function pmb_weather.get_random_weather()
    local threshold = math.random(chance_total)
    local cur = 0
    for wname, def in pairs(regw) do
        cur = cur + def.chance
        if cur >= threshold then
            pmb_weather.set_weather(wname)
            return true
        end
    end
    pmb_weather.set_weather("clear")
end

-- try to get existing weather from world storage
local function load_weather_from_storage(skip)
    current_weather_instance = (not skip) and world_storage.get("pmb_weather", "current_weather")
    if not current_weather_instance then
        pmb_weather.set_weather("clear")
    else
        current_def = regw[current_weather_instance.name]
        propagate_biomes()
    end
end

local function tick_weather(dtime)
    if not current_weather_instance then return end
    current_weather_instance.time = current_weather_instance.time - dtime
    if current_weather_instance.time <= 0 then
        local to_weather = ""
        if current_def and current_def.next then
            for wname, chance in pairs(current_def.next) do
                if math.random() < chance then
                    to_weather = wname
                    pmb_weather.set_weather(wname)
                    break
                end
            end
        end
        if to_weather == "" then
            pmb_weather.get_random_weather()
        end
    end
end

function pmb_weather.register_weather(name, def)
    chance_total = chance_total + def.chance
    regw[name] = def
end

pmb_weather.register_weather("none", {
    biomes = {"default"},
    chance = 0,
    min_duration = 1200,
    max_duration = 1200,
    on_enter_weather = function(player, meta, from_weather)
    end,
    on_leave_weather = function(player, meta, to_weather)
    end,
    on_step = function(player, dtime, meta)
    end,
    fallbacks = {
        "clear"
    },
})

pmb_weather.register_weather("clear", {
    biomes = {},
    chance = 200,
    min_duration = 120, -- sec
    max_duration = 1200,
    on_enter_weather = function(player, meta, from_weather)
        pmb_weather.request_sky(player, {
            base_color = "#ffffff",
            type = "regular",
            clouds = true,
            sky_color = {
                day_sky = pmb_weather.color(150, 200, 255),
                day_horizon = pmb_weather.color(170, 210, 255),
                dawn_sky = pmb_weather.color(180, 186, 250),
                dawn_horizon = pmb_weather.color(186, 193, 255),
                night_sky = pmb_weather.color(50, 41, 72),
                night_horizon = pmb_weather.color(50, 41, 255),
                indoors = pmb_weather.color(59, 59, 59),
                fog_sun_tint = pmb_weather.color(244, 125, 29),
                fog_moon_tint = pmb_weather.color(127, 153, 204),
                fog_tint_type = "custom",
            }
        })
        player:set_clouds({
            density = 0.4,
            thickness = 20,
            ambient = "#111111",
            color = "#ffffffa0",
            height = 200,
            speed = {x=4, z=2}
        })
        player:set_sun({
            visible = true,
            texture = "blank.png",
            sunrise_visible = true,
        })
        player:set_moon({
            visible = true,
        })
        pmb_weather.request_lighting(player, 0.33)
    end,
    on_leave_weather = function(player, meta, to_weather)
        -- do nothing I guess?
    end,
    on_step = function(player, dtime, meta)
    end,
    fallbacks = {
    },
})

local function get_biomes()
    -- local ret = ""
    for name, def in pairs(minetest.registered_biomes) do
        wbiome[name] = {}
        -- ret = ret..name.."\n"
    end
    -- error(ret)
end


--------------------------------------------
-- global
--------------------------------------------

local _t = 1
local function update_vars(dtime)
    if _t > 0 then _t = _t - dtime return end
    _t = 1

    pmb_weather.night_brightness = world_storage.wget("pmb_weather_night_brightness") or 0.10
end

local function on_step(dtime)
    if not current_weather_instance then return end
    update_vars(dtime)
    tick_weather(dtime)

    for i, player in pairs(minetest.get_connected_players()) do
        do_player_weather(player, dtime)
    end
end

minetest.register_on_mods_loaded(function()
    get_biomes()
    assign_extra_biome_weathers()
    load_weather_from_storage()
    -- pmb_weather.set_weather("heavy_rain")
    -- pmb_weather.set_weather("light_rain")
    -- pmb_weather.set_weather("clear")
    minetest.register_globalstep(on_step)
end)



minetest.register_chatcommand("weather", {
    params = "",
    description = "weather [weather name]",
    privs = { server = 1 },
    func = function(name, param)

        local splitparam = string.split(param, " ", true)
        local weather_name = splitparam[1]

        local def = regw[weather_name]
        if def then
            pmb_weather.set_weather(weather_name)
            if #splitparam > 1 then
                local time = tonumber(splitparam[2])
                if time and current_weather_instance then
                    current_weather_instance.time = time
                    return true, "Weather set to "..weather_name.." for "..tostring(time).." seconds"
                end
            end
            return true, "Weather set to "..weather_name
        end

        local list = ""
        for wname, wdef in pairs(regw) do
            list = list..wname.."\n"
        end
        return false, "Weather list: \n"..list
    end
})
