local S = minetest.get_translator(minetest.get_current_modname())

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

local regw = br_weather.registered_weathers

br_weather.has_aom_settings = minetest.get_modpath("aom_settings") ~= nil
if br_weather.has_aom_settings then
    -- TL: setting name
    aom_settings.register_setting("gameplay_night_brightness", 0.15, S("Night Brightness"), "server")
end
function br_weather.get_setting(player, key, default)
    if br_weather.has_aom_settings then
        return aom_settings.get_setting(player, key, default)
    else
        return default
    end
    return default
end

br_weather.mod_storage = core.get_mod_storage()

br_weather.night_brightness = br_weather.get_setting(nil, "gameplay_night_brightness", 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 br_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] = task.biome
                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 br_weather.fix_timeofday(player, factor, force, override)
    if not br_weather.allow_sky then return end
    local real_time = minetest.get_timeofday()
    local bs_time = 0
    -- don't ask.
    if real_time > 0.5 then
        bs_time = 1 - clamp((real_time - 0.7) / 0.1, 0, 1)
    else
        bs_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
        bs_time = override
    else
        bs_time = clamp(bs_time, br_weather.night_brightness, 1) * pl[player].light_factor
        bs_time = clamp(bs_time, br_weather.night_brightness * 0.75, 1)
    end

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

function br_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 = 0.01,
            particlespawners = {},
            sounds = {},
            shadow_intensity = 0.33,
            shadow_last_target = 0.33,
            shadow_target = 0.33,
            shadow_update = 0.2,
            light_factor = 0.5,
            last_bs_time = 0.5,
            _enter_weather_delay = 0.0,
        }
        player:set_lighting({ shadows = { intensity = pl[player].shadow_intensity} })
    end
end

function br_weather.request_sky(player, sky_table)
    if not br_weather.allow_sky then return end
    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 br_weather.request_lighting(player, intensity)
    if not br_weather.allow_sky then return end
    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 br_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 br_weather.on_player_enter_weather(player, weatherdef, from_weather)
    weatherdef.on_enter_weather(player, pl[player], from_weather)
end
function br_weather.on_player_leave_weather(player, weatherdef, to_weather)
    weatherdef.on_leave_weather(player, pl[player], to_weather)
end

local update_tick = 0.1
function br_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
    br_weather.fix_timeofday(player, weatherdef.light_factor)
end

minetest.register_on_leaveplayer(function(player, timed_out)
    if pl[player] then
        br_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

function br_weather.force_stop_weather(player)
    check_player(player)
    local pli = pl[player]
    pli._enter_weather_delay = nil
    local old_weatherdef = regw[pl[player].weather or "none"]
    pl[player]._STOP = true
    br_weather.on_player_leave_weather(player, old_weatherdef)
    pl[player]._STOP = nil
    pl[player].weather = "none"
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 and pli.weather then
        br_weather.on_player_step_weather(player, regw[pli.weather], dtime)
        return
    end

    local biome = minetest.get_biome_name((minetest.get_biome_data(player:get_pos()) or {}).biome)

    -- if this is an _OCEAN auto biome, just use the normal one for weather
    local l = string.len(biome)
    if string.sub(biome, l-5, l) == "_OCEAN" then
        biome = string.sub(biome, 0, l-6)
    end

    local biome_weather_name = wbiome[biome]
    local new_weatherdef = regw[biome_weather_name or "none"]
    local old_weatherdef = regw[pli.weather]

    -- delay to prevent flickering
    if (biome_weather_name ~= pli.weather) and pli._enter_weather_delay ~= nil then
        if pli._enter_weather_delay > 0 then
            pli._enter_weather_delay = pli._enter_weather_delay - dtime
            return
        elseif pli._enter_weather_delay <= 0 then
            pli._enter_weather_delay = nil
        end
    end
    if (biome_weather_name ~= pli.weather) and (not pli._enter_weather_delay) then
        pli._enter_weather_delay = 4
    end

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

function br_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
        br_weather.on_player_leave_weather(player, old_weatherdef, new_weatherdef)
    end
    if new_weatherdef then
        br_weather.on_player_enter_weather(player, new_weatherdef, old_weatherdef)
    end

    pl[player].weather = weather_name
    pl[player].weather_override = true
end
function br_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", "br_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 br_weather.set_weather(weather_name)
    local ins = new_weather_instance(weather_name)
    current_weather_instance = ins
    current_def = regw[weather_name]
    br_weather.mod_storage:set_string("current_weather", core.serialize(current_weather_instance))
    propagate_biomes()
end

--
function br_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
            br_weather.set_weather(wname)
            return true
        end
    end
    br_weather.set_weather("clear")
end

-- try to get existing weather from storage
local function load_weather_from_storage()
    local datastring = br_weather.mod_storage:get_string("current_weather")
    if datastring ~= nil then
        current_weather_instance = core.deserialize(datastring, true)
    end
    if not current_weather_instance then
        br_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
                    br_weather.set_weather(wname)
                    break
                end
            end
        end
        if to_weather == "" then
            br_weather.get_random_weather()
        end
    end
end

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

br_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"
    },
})

br_weather.register_weather("clear", {
    biomes = {},
    chance = 200,
    min_duration = 120, -- sec
    max_duration = 1200,
    on_enter_weather = function(player, meta, from_weather)
        br_weather.request_sky(player, {
            base_color = "#ffffff",
            type = "regular",
            clouds = true,
            sky_color = {
                day_sky = br_weather.color(90, 150, 245),
                day_horizon = br_weather.color(140, 180, 245),
                dawn_sky = br_weather.color(180, 186, 250),
                dawn_horizon = br_weather.color(186, 193, 255),
                night_sky = br_weather.color(50, 41, 72),
                night_horizon = br_weather.color(50, 41, 255),
                indoors = br_weather.color(59, 59, 59),
                fog_sun_tint = br_weather.color(244, 125, 29),
                fog_moon_tint = br_weather.color(127, 153, 204),
                fog_tint_type = "custom",
            },
            fog = {
                fog_start = 0.5,
            },
        })
        player:set_clouds({
            density = 0.4,
            thickness = 20,
            ambient = "#111111",
            color = "#ffffffa0",
            height = 200,
            speed = {x=4, z=2}
        })
        player:set_sun({
            visible = true,
            sunrise_visible = true,
        })
        player:set_moon({
            visible = true,
        })
        br_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
        local l = string.len(name)
        if string.sub(name, l-6, l) ~= "_OCEAN" then
            wbiome[name] = {}
        end
        -- 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

    br_weather.night_brightness = br_weather.get_setting(nil, "gameplay_night_brightness", 0.15)
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()
    -- br_weather.set_weather("heavy_rain")
    -- br_weather.set_weather("light_rain")
    -- br_weather.set_weather("clear")
    minetest.register_globalstep(on_step)
end)



minetest.register_chatcommand("weather", {
    params = "/weather [weather name] [time in seconds]",
    description = "Changes the weather globally. Leave blank parameters to list weathers.",
    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
            br_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
                    -- TL: @1 is weather name ("heavy_rain"), @2 is number ("600")
                    return true, S("Weather set to @1 for @2 seconds", weather_name, tostring(time))
                end
            end
            -- TL: @1 is weather name ("heavy_rain")
            return true, S("Weather set to @1", weather_name)
        end

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