--[[
        DYNAMIC ZONE

        Original Author(s)
          Singustromo <singustromo at disroot.org>

        Edited by

        License
          Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)
          (https://creativecommons.org/licenses/by-nc-sa/4.0)

        This file provides the rough structure for saving transitions and
        their pairs - called routes in this context.

        Additional info:
          tables, functions, userdata and threads are passed around by reference,
          while numbers, booleans, and nil are passed by value
          Despite this we reference entries by their index because both tables
          are saved in m_data. Can't reference them.
--]]

parent = _G["dynamic_zone"]
if not (parent and parent.VERSION and parent.VERSION >= 20241224) then return end

--------------------------
--     Dependencies     --
--------------------------

-- verified in main script
local utils = dynamic_zone_utils
local debug = dynamic_zone_debug

CONST_LOGGING_PREFIX = "Routes"
local log = debug.log_register("info", CONST_LOGGING_PREFIX)
local log_error = debug.log_register("error", CONST_LOGGING_PREFIX)

---------------------
--    Structure    --
---------------------

registered_routes = {}
registered_transitions = {
--[[
        [size] = <int>,
        [transition_name] = {
                route = route_id,       -- index in registeres_routes
                master = true,          -- exception for 1 -> n routes (e.g. truck cemetary)
                spawned_anomalies = {}
        },
        ...
--]]
}

function route_count()
        return ((utils.is_table_empty(registered_routes))
                and 0 or size_table(registered_routes))
end

function transition_count()
        return (registered_transitions.size or 0)
end

function clear()
        registered_routes = {}
        registered_transitions = {}
end

-- first initialization of a route
--- @returns    id of route (index)
function route_create()
        local id = #registered_routes +1

        registered_routes[id] = {
                id = id,                        -- Needed in rare cases
                members = { },                  -- transition names
                connects = { },                 -- level names (game_levels.ltx)
                blacklisted = false,
                unlocked = true,
                blocked = false,
                block_discovered = false,
                recently_discovered = false,    -- set during emission (controls blip)
                anomaly_theme = 0,
        }

        return id
end

function transition_register(transition_name, route_id)
        if not utils.valid_type{ caller = "transition_register", "str", transition_name,
                "tbl", registered_routes[route_id] } then return end

        local id = get_story_object_id(transition_name)
        if (utils.assert_failed(id, "%s is not a Story-ID. Not registered.")) then return end

        local route_members = registered_routes[route_id].members
        if utils.table_has(route_members, transition_name) then return end

        registered_transitions[transition_name] = {
                route = route_id,
                master = false,
                spawned_anomalies = {}, -- holds the object ids
        }

        table.insert(route_members, transition_name)

        local index = (registered_transitions.size or 0) +1
        registered_transitions.size = index
        return index
end

-------------------------------------------------------------------------------

function transition_table_register(tbl, route_id)
        local registered = {}

        if not utils.valid_type{ caller = "transition_table_register",
                "tbl", tbl, "int", route_id } then return registered end

        for _, transition_name in pairs(tbl) do
                if not transition_register(transition_name, route_id) then
                        log_error("Transition '%s' has already been registered!", transition_name)
                else
                        registered[#registered +1] = transition_name
                end
        end

        return registered
end

-- @returns     mutable reference
function get_route(route_id)
        return registered_routes[route_id]
end

-- @returns     mutable reference
function get_transition(transition_name)
        return registered_transitions[transition_name]
end

function route_exists(route_id)
        return (nil ~= registered_routes[route_id])
end

function transition_exists(transition_name)
        return (nil ~= registered_transitions[transition_name])
end

-------------------------------------------------------------------------------

-- Wacky implementation
-- TODO: Improve condition declaration and parsing
-- Conditions are table entries (route flags) which are or'ed
-- attributes are negated with an '!' or can be joined via an and '*'
-- e.g. `get_routes{"blocked*!discovered", "!blocked*discovered"}`
-- Should only be used with route flags (true, false)
-- @returns     <table> (route_ids)
function get_routes(conditions)
        if not utils.valid_type{ caller = "get_routes", "tbl", conditions } then return end
        if utils.is_table_empty(conditions) then return end

        local matches = {}

        local func_body = ""
        for _, condition in pairs(conditions) do
                local parsed = condition
                parsed = parsed:gsub('*!', ' and not ref.')
                parsed = parsed:gsub('*', ' and ref.')
                parsed = parsed:gsub('!', 'not ref.')
                parsed = (string.find(parsed, "^not")) and parsed or "ref." .. parsed
                func_body = func_body .. "(" .. parsed .. ") or "
        end
        func_body = func_body:sub(1, -5) -- remove last ' or '

        local to_eval = "return function(ref) return (" .. func_body .. ") end"
        local func, err = loadstring(to_eval)
        if (err) then
                log_error("Unable to evaluate '%s'\n! %s", to_eval, err)
                return
        end

        -- loadstring encapsulates input into another function
        local check_func = func()

        for route_id, route in iterate_routes(true) do
                local status, retval = pcall(check_func, route)

                if (status and retval) then
                        matches[#matches +1] = route_id
                end
        end

        return matches
end

-------------------------------------------------------------------------------

-- Filters locked and blacklisted routes by default
-- @returns     name, modifiable route reference
function iterate_routes(include_inactive)
        local index = 0

        return function()
                index = index + 1

                if not (include_inactive) then
                        while (registered_routes[index] and route_inactive(index)) do
                                index = index + 1
                        end
                end

                if (index <= #registered_routes) then
                        return index, registered_routes[index]
                end
        end
end

-- @returns     name, mutable-reference
function iterate_transitions(route_id)
        local route = get_route(route_id)
        local members = route and route.members
        if (not members) then return end

        local index = 0
        return function()
                index = index + 1
                if (index > #members) then return end

                return members[index], get_transition(members[index])
        end
end

-------------------------------------------------------------------------------

function set_transition_property(name, property, value)
        local transition = get_transition(name)
        if not utils.valid_type{ caller = "set_transition_property",
                "tbl", transition, "str", property} then return end

        if (type(value) == 'nil') then return end
        if (type(transition[property]) == "nil") then return end

        transition[property] = value
        return true
end

-- @returns     immutable reference
function get_transition_property(name, property)
        local transition = get_transition(name)
        if (not transition) then return end

        return transition[property]
end

-------------------------------------------------------------------------------

function set_route_property(route_id, property, value)
        local route = get_route(route_id)
        if not utils.valid_type{ caller = "set_route_property",
                "tbl", route, "str", property} then return end

        if (type(value) == 'nil') then return end
        if (type(route[property]) == "nil") then return end

        route[property] = value
        return true
end

-- @returns     immutable reference
function get_route_property(route_id, property)
        local route = get_route(route_id)
        if (not route) then return end

        return route[property]
end

function get_route_id(transition_name)
        local transition = get_transition(transition_name)
        return (transition and transition.route)
end

-- used when we only have the transition name
-- @returns     modifyable route reference
function get_route_by_transition(gizmo)
        local transition_name = (type(gizmo) == "number")
                and get_object_story_id(gizmo) or gizmo

        if (utils.assert_failed(transition_name)) then return end

        local transition = get_transition(transition_name)
        return (transition and get_route(transition.route))
end
has_route = get_route_by_transition -- alias for readability's sake

-- @returns     immutable reference
function get_route_members(route_id)
        local route = get_route(route_id)
        return (route and route.members)
end

-------------------------------------------------------------------------------

function route_inactive(route_id)
        local route = get_route(route_id)
        if (not route) then return true end

        return (route.blacklisted or (not route.unlocked))
end

-- only really need this, if we do not replace accessible zone
function route_unlock(route_id)
        local route = get_route(route_id)
        if (not route) then return end

        route.unlocked = true
        return true
end

function route_unlocked(route_id)
        local route = get_route(route_id)
        if (not route) then return end

        return (route.unlocked)
end

function route_locked(route_id)
        return (not route_unlocked(route_id))
end

-------------------------------------------------------------------------------

function route_block(route_id)
        local route = get_route(route_id)
        if not (route and route.unlocked) then return end

        route.blocked = true
        return true
end

function route_unblock(route_id)
        local route = get_route(route_id)
        if not (route and route.unlocked and route.blocked) then return end

        route.blocked = false
        return true
end

function route_blocked(route_id)
        local route = get_route(route_id)
        if (not route) then return end

        return (route.blocked)
end

-------------------------------------------------------------------------------

-- We also pass an additional functor to be executed here
function route_discover(route_id, functor, ...)
        local vararg = {...}

        local route = get_route(route_id)
        if not (route and route.blocked) then return end

        route.block_discovered = true

        if (functor and type(functor) == "function") then
                functor(unpack(vararg))
        end

        return true
end

function route_discovered(route_id)
        local route = get_route(route_id)
        return (route and route.block_discovered)
end

function route_known_state_differs(route_id)
        local route = get_route(route_id)
        return route and (route.blocked ~= route.block_discovered)
end

-------------------------------------------------------------------------------

function transition_tostring(transition_name, indentation, indent_str)
        indentation = (indentation) or 0
        indent_str = (indent_str) or "  "
        local transition = get_transition(transition_name)
        local string = string.rep(indent_str, indentation)
                .. "[" .. transition_name .. "] = {\n" 

        for key, value in pairs(transition) do
                if (key == "route") then
                        goto next_attribute
                end

                string = string .. string.rep(indent_str, indentation +1)
                        .. key .. " = "

                if (type(value) == "table") then
                        string = string .. "{" .. table.concat(value, ",") .. "}"
                else
                        string = string .. tostring(value)
                end
                string = string .. "\n"

                :: next_attribute ::
        end

        return string .. string.rep(indent_str, indentation) .. "}\n"
end

-- crude and specific string generation of attribute tables
function route_tostring(route_id, include_member_attributes)
        local route = get_route(route_id)
        if (not route) then
                log_error("Printinfo No route with ID #%s !", route_id)
                return
        end

        local indentation = "    "
        local string = string.format("[%03d]\n", route_id)
        for key, value in pairs(route) do
                if (key == 'id') then goto next_attribute end

                string = string .. indentation .. key .. " = "

                if (type(value) ~= "table") then
                        string = string .. tostring(value)
                        goto continue
                end

                if ((not include_member_attributes) or key ~= "members") then
                        string = string .. "{" .. table.concat(value, ", ") .. "}"
                        goto continue
                end

                -- lazy way to serialize member attributes
                string = string .. "{\n"
                for _, section in pairs(value) do
                        string = string .. transition_tostring(section, 2, indentation)
                end
                string = string .. indentation .. "}"

                :: continue ::
                string = string .. "\n"

                :: next_attribute ::
        end

        return string:sub(1, -2) -- remove last new line
end

-- Prints all routes with the specified attributes
-- @returns             amount of all blocked routes
function print_routes(conditions)
        local output_string = string.format("Route Attributes%s: ",
                (conditions) and " (" .. table.concat(conditions, ", ") .. ")" or "")

        local routes = get_routes(conditions)
        if (not routes or utils.is_table_empty(routes)) then return end

        for _, route_id in pairs(routes) do
                output_string = output_string .. "\n" .. route_tostring(route_id, true)
        end

        log("%s", output_string)
end

-- Prints all the information agnostic to what type the parameter is
function printinfo(gizmo)
        local output_string = "Attributes of"
        if route_exists(gizmo) then
                output_string = string.format("%s route #%s:\n%s",
                        output_string, gizmo, route_tostring(gizmo, true))
        elseif transition_exists(gizmo) then
                output_string = string.format("%s transition '%s':\n%s",
                        output_string, gizmo, transition_tostring(gizmo))
        else return end

        log("%s", output_string)
end