-- universal_js_loader.lua
-- Powerful pragmatic JS->Lua shim for Flumi/Godot Lua-only browser
-- Requirements: dkjson (for config.json reading), optional luasocket for accurate timers
-- Place here with script.js and header.html/footer.html etc.

local ok_socket, socket = pcall(require, "socket") -- optional but recommended
local json = require("dkjson")
local socket = require("socket")

-- ====== Utilities ======
local function now()
    if ok_socket and socket.gettime then return socket.gettime() end
    return os.time()
end

local function sleep(seconds)
    if ok_socket and socket.select then
        socket.select(nil, nil, seconds)
    else
        -- fallback to OS sleep (seconds must be integer on many systems)
        os.execute("sleep " .. tostring(seconds))
    end
end

-- ====== Console shim ======
local console = {}
function console.log(...) print("[JS]", ...) end
console.info = console.log
console.warn = console.log
console.error = console.log

-- ====== DOM shim ======
local document = { elements = {}, _by_attr = {} }

function document.getElementById(id)
    if not id then return nil end
    if not document.elements[id] then
        document.elements[id] = {
            id = id,
            innerHTML = "",
            textContent = "",
            children = {},
            attributes = {},
            appendChild = function(self, child) table.insert(self.children, child) end,
            removeChild = function(self, child)
                for i,v in ipairs(self.children) do if v == child then table.remove(self.children, i); return end end
            end,
            setAttribute = function(self, k,v) self.attributes[k]=v end,
            getAttribute = function(self,k) return self.attributes[k] end,
        }
    end
    return document.elements[id]
end

function document.createElement(tag)
    return { tag = tag, innerHTML = "", children = {}, attributes = {},
        appendChild = function(self, child) table.insert(self.children, child) end,
        removeChild = function(self, child)
            for i,v in ipairs(self.children) do if v == child then table.remove(self.children, i); return end end
        end,
        setAttribute = function(self, k,v) self.attributes[k]=v end,
        getAttribute = function(self,k) return self.attributes[k] end,
    }
end

function document.querySelector(selector)
    if not selector then return nil end
    if selector:sub(1,1) == "#" then
        return document.getElementById(selector:sub(2))
    end
    -- crude: search by attribute "id" equals selector w/o '#'
    for _,v in pairs(document.elements) do
        if v.id == selector or v.tag == selector then return v end
    end
    return nil
end

-- ====== Event system & window ======
local window = { listeners = {}, timers = {}, timer_next_id = 1, intervals = {}, interval_next_id = 1 }

function window.addEventListener(event, cb)
    if not event or type(cb) ~= "function" then return end
    window.listeners[event] = window.listeners[event] or {}
    table.insert(window.listeners[event], cb)
end
function window.removeEventListener(event, cb)
    if not window.listeners[event] then return end
    for i,f in ipairs(window.listeners[event]) do if f == cb then table.remove(window.listeners[event], i); return end end
end
function window.dispatchEvent(eventName, ...)
    if not window.listeners[eventName] then return end
    for _,cb in ipairs(window.listeners[eventName]) do
        local ok, err = pcall(cb, ...)
        if not ok then console.error("Event handler error:", err) end
    end
end

-- timers
local timers = {}
local timer_id_seq = 0
local function newTimer(at, fn, repeating, interval)
    timer_id_seq = timer_id_seq + 1
    timers[timer_id_seq] = { at = at, fn = fn, repeating = repeating, interval = interval, id = timer_id_seq }
    return timer_id_seq
end

function window.setTimeout(fn, ms)
    ms = tonumber(ms) or 0
    local at = now() + (ms / 1000)
    return newTimer(at, fn, false, nil)
end
function window.clearTimeout(id) timers[id] = nil end

function window.setInterval(fn, ms)
    ms = tonumber(ms) or 0
    local at = now() + (ms / 1000)
    return newTimer(at, fn, true, ms / 1000)
end
function window.clearInterval(id) timers[id] = nil end

-- ====== Promise shim (basic) ======
local Promise = {}
Promise.__index = Promise

function Promise.new(executor)
    local self = setmetatable({ state = "pending", value = nil, handlers = {} }, Promise)
    local function resolve(v)
        if self.state ~= "pending" then return end
        self.state = "fulfilled"
        self.value = v
        -- schedule handlers asynchronously
        window.setTimeout(function()
            for _,h in ipairs(self.handlers) do
                if h.onFulfilled then
                    local ok, err = pcall(h.onFulfilled, self.value)
                    if not ok then console.error("Promise handler error:", err) end
                end
            end
            self.handlers = {}
        end, 0)
    end
    local function reject(err)
        if self.state ~= "pending" then return end
        self.state = "rejected"
        self.value = err
        window.setTimeout(function()
            for _,h in ipairs(self.handlers) do
                if h.onRejected then
                    local ok, e = pcall(h.onRejected, self.value)
                    if not ok then console.error("Promise reject handler error:", e) end
                end
            end
            self.handlers = {}
        end, 0)
    end
    local ok, err = pcall(function() executor(resolve, reject) end)
    if not ok then reject(err) end
    return self
end

function Promise:then_(onFulfilled, onRejected)
    local nextPromise = Promise.new(function(resolve, reject)
        local handler = {
            onFulfilled = function(v)
                if type(onFulfilled) ~= "function" then return resolve(v) end
                local ok, res = pcall(onFulfilled, v)
                if not ok then return reject(res) end
                if getmetatable(res) == Promise then
                    res:then_(resolve, reject)
                else
                    resolve(res)
                end
            end,
            onRejected = function(e)
                if type(onRejected) ~= "function" then return reject(e) end
                local ok, res = pcall(onRejected, e)
                if not ok then return reject(res) end
                resolve(res)
            end
        }
        if self.state == "pending" then
            table.insert(self.handlers, handler)
        elseif self.state == "fulfilled" then
            window.setTimeout(function() handler.onFulfilled(self.value) end, 0)
        else
            window.setTimeout(function() handler.onRejected(self.value) end, 0)
        end
    end)
    return nextPromise
end

function Promise:catch_(onRejected)
    return self:then_(nil, onRejected)
end

-- expose JS-friendly names
local JSAPI = {
    Promise = { new = function(executor) return Promise.new(executor) end },
    console = console,
    document = document,
    window = window,
}

-- For convenience, allow Promise(...) style as well
JSAPI.Promise.resolve = function(value) return Promise.new(function(resolve,_) resolve(value) end) end
JSAPI.Promise.reject = function(value) return Promise.new(function(_,reject) reject(value) end) end

-- ====== fetch shim ======
function fetch(url)
    if url:sub(1,7) == "gurt://" then
        -- Parse gurt://host:port/path
        local host, port, path = url:match("gurt://([^:/]+):?(%d*)(/.*)")
        port = tonumber(port) or 4878  -- default GURT port
        local client = assert(socket.tcp())
        client:connect(host, port)
        client:send("GET " .. path .. " GURT/1.0.0\r\nHost: " .. host .. "\r\n\r\n")

        -- Read headers
        local headers = {}
        while true do
            local line, err = client:receive("*l")
            if not line or line == "" then break end
            table.insert(headers, line)
        end

        -- Read body
        local bodyChunks = {}
        while true do
            local chunk, err = client:receive(1024)
            if not chunk then break end
            table.insert(bodyChunks, chunk)
        end
        client:close()

        local body = table.concat(bodyChunks)
        return {
            ok = true,
            status = 200,
            text = function() return body end,
            json = function() return json.decode(body) end
        }

    else
        -- fallback to local file
        local f = io.open(url, "r")
        if not f then return { ok=false, status=404, text=function() return "" end, json=function() return nil end } end
        local content = f:read("*a"); f:close()
        return { ok=true, status=200, text=function() return content end, json=function() return json.decode(content) end }
    end
end

-- ====== loadHTML (JS-friendly) ======
local function loadHTML(url, containerId)
    local container = document.getElementById(containerId)
    if not container then
        console.error("Container not found:", containerId)
        return nil
    end
    local res = fetch(url)
    if not res.ok then
        console.error("Failed to load", url, "status", res.status)
        return nil
    end
    container.innerHTML = res.text()
    console.log("Loaded", url, "into #"..containerId)
    return container
end
JSAPI.loadHTML = loadHTML

-- ====== JS -> Lua light transpiler ======
local function transpile_js_to_lua(code)
    -- strip CR
    code = code:gsub("\r\n", "\n")
    -- strip single-line comments (naive: won't handle // inside strings)
    code = code:gsub("//[^\n]*", "")
    -- strip multi-line comments
    code = code:gsub("/%*.-%*/", "")
    -- replace var/let/const with local
    code = code:gsub("%f[%w](var)%f[%W]", "local")
    code = code:gsub("%f[%w](let)%f[%W]", "local")
    code = code:gsub("%f[%w](const)%f[%W]", "local")
    -- equality / inequality
    code = code:gsub("!==", "~=")
    code = code:gsub("===", "==")
    code = code:gsub("!=", "~=")
    -- booleans/operators
    code = code:gsub("&&", " and ")
    code = code:gsub("||", " or ")
    -- careful replacement of '!' -> ' not ' (avoid changing != which was already handled)
    code = code:gsub("([^%w_])!%s*", "%1 not ")
    -- null -> nil
    code = code:gsub("%f[%w]null%f[%W]", "nil")
    -- arrow functions with parentheses: (args) => {
    code = code:gsub("(%b())%s*=>%s*{", function(args)
        return "function" .. args .. "\n"
    end)
    -- arrow with single arg: arg => {
    code = code:gsub("([%w_]+)%s*=>%s*{", function(arg)
        return "function(" .. arg .. ")\n"
    end)
    -- convert "function name (args) {" to "local function name (args)"
    code = code:gsub("function%s+([%w_]+)%s*%(", "local function %1(")
    -- convert "{ ... }" blocks -> Lua "do ... end" (naive; avoid object literals in script.js)
    code = code:gsub("{", " do ")
    code = code:gsub("}", " end ")
    -- convert semicolons to newlines
    code = code:gsub(";", "\n")
    -- convert 'await ' -> '' (recommend using .then instead, can't really block the VM)
    code = code:gsub("%f[%w]await%f[%W]%s*", "")
    -- small fixups: JS 'console.log(...)' is okay because console.log exists
    -- Replace "return" outside of functions is okay, keep it.
    return code
end

-- ====== Execution environment ======
local env = {
    console = console,
    document = document,
    window = window,
    fetch = fetch,
    loadHTML = loadHTML,
    Promise = JSAPI.Promise,
    -- helpers
    setTimeout = window.setTimeout,
    setInterval = window.setInterval,
    clearTimeout = window.clearTimeout,
    clearInterval = window.clearInterval,
    -- make JSON library available as json for scripts if needed
    json = json,
}
-- expose console/document/window global-ish
setmetatable(env, { __index = function(t,k) return rawget(JSAPI,k) end })

-- ====== read config.json (optional) ======
local cfgf = io.open("config.json", "r")
if cfgf then
    local content = cfgf:read("*a"); cfgf:close()
    local conf, pos, err = json.decode(content)
    if not conf then console.error("config.json parse error:", err) else
        for _,s in ipairs(conf.sections or {}) do
            if s.file and s.container then loadHTML(s.file, s.container) end
        end
    end
end

-- ====== load & transpile script.js ======
local function load_and_run_script(path)
    local f = io.open(path, "r")
    if not f then
        console.log("No script.js found at", path)
        return
    end
    local code = f:read("*a"); f:close()
    local lua_code = transpile_js_to_lua(code)

    -- attempt to compile
    local chunk, err = load(lua_code, "script.js.transpiled", "t", env)
    if not chunk then
        console.error("Transpile/compile error:", err)
        -- for debugging: save transpiled code
        local dump = io.open("script.transpiled.lua", "w")
        if dump then dump:write(lua_code); dump:close() end
        return
    end
    local ok, res = pcall(chunk)
    if not ok then
        console.error("Runtime error in script.js:", res)
    end
end

load_and_run_script("https://idiotstudios.co.za/script.js")

-- ====== dispatch DOMContentLoaded ======
window.dispatchEvent("DOMContentLoaded")

-- ====== simple event loop so timers and Promise microtasks run ======
local function run_event_loop(timeout_seconds)
    timeout_seconds = tonumber(timeout_seconds) or 2 -- default run for up to 2s (tunable)
    local t0 = now()
    while true do
        local nows = now()
        -- run due timers
        local ran = false
        for id, t in pairs(timers) do
            if t.at and nows >= t.at then
                local ok, err = pcall(t.fn)
                if not ok then console.error("Timer error:", err) end
                ran = true
                if t.repeating then
                    t.at = nows + t.interval
                else
                    timers[id] = nil
                end
            end
        end
        -- exit conditions: no timers and we've passed minimum time
        local any_timers = false
        for _ in pairs(timers) do any_timers = true; break end
        if not any_timers then
            -- no timers remaining; exit
            break
        end
        if (nows - t0) > timeout_seconds then
            console.warn("Event loop timeout reached ("..tostring(timeout_seconds).."s), leaving background timers running.")
            break
        end
        -- sleep a short while
        sleep(0.01)
    end
end

-- Run the event loop. Adjust timeout as needed for heavier scripts.
run_event_loop(3)

-- If you want to keep the VM alive (e.g. for REPL), comment out run_event_loop and provide your own loop.
-- Arch (btw)