local basaltEvent = require("basaltEvent")()
local baseObjects = require("loadObjects")
local moddedObjects
local pluginSystem = require("plugin")
local utils = require("utils")
local log = require("basaltLogs")
local uuid = utils.uuid
local wrapText = utils.wrapText
local count = utils.tableCount
local moveThrottle = 300
local dragThrottle = 0
local renderingThrottle = 0
local newObjects = {}

local baseTerm = term.current()
local version = "1.7.0"

local projectDirectory = fs.getDir(table.pack(...)[2] or "")

local activeKey, frames, monFrames, variables, schedules = {}, {}, {}, {}, {}
local mainFrame, activeFrame, focusedObject, updaterActive

local basalt = {}

if not  term.isColor or not term.isColor() then
    error('Basalt requires an advanced (golden) computer to run.', 0)
end

local defaultColors = {}
for k,v in pairs(colors)do
    if(type(v)=="number")then
        defaultColors[k] = {baseTerm.getPaletteColor(v)}
    end
end


local function stop()
    updaterActive = false    
    baseTerm.clear()
    baseTerm.setCursorPos(1, 1)
    for k,v in pairs(colors)do
        if(type(v)=="number")then
            baseTerm.setPaletteColor(v, colors.packRGB(table.unpack(defaultColors[k])))
        end
    end
end

local function schedule(f)
assert(f~="function", "Schedule needs a function in order to work!")
return function(...)
        local co = coroutine.create(f)
        local ok, result = coroutine.resume(co, ...)
        if(ok)then
            table.insert(schedules, co)
        else
            basalt.basaltError(result)
        end
    end
end

basalt.log = function(...)
    log(...)
end

local setVariable = function(name, var)
    variables[name] = var
end

local getVariable = function(name)
    return variables[name]
end

local getObjects = function()
    return moddedObjects
end

local getObject = function(objectName)
    return getObjects()[objectName]
end

local createObject = function(basalt, objectName, id)
    return getObject(objectName)(id, basalt)
end

local bInstance = {
    getDynamicValueEventSetting = function()
        return basalt.dynamicValueEvents
    end,
    
    getMainFrame = function()
        return mainFrame
    end,

    setVariable = setVariable,
    getVariable = getVariable,

    setMainFrame = function(mFrame)
        mainFrame = mFrame
    end,

    getActiveFrame = function()
        return activeFrame
    end,

    setActiveFrame = function(aFrame)
        activeFrame = aFrame
    end,

    getFocusedObject = function()
        return focusedObject
    end,

    setFocusedObject = function(focused)
        focusedObject = focused
    end,
    
    getMonitorFrame = function(name)
        return monFrames[name] or monGroups[name][1]
    end,

    setMonitorFrame = function(name, frame, isGroupedMon)
        if(mainFrame == frame)then mainFrame = nil end
        if(isGroupedMon)then
            monGroups[name] = {frame, sides}
        else
            monFrames[name] = frame
        end
        if(frame==nil)then
            monGroups[name] = nil
        end
    end,

    getTerm = function()
        return baseTerm
    end,

    schedule = schedule,

    stop = stop,
    debug = basalt.debug,
    log = basalt.log,
    
    getObjects = getObjects,

    getObject = getObject,

    createObject = createObject,

    getDirectory = function()
        return projectDirectory
    end
}

local function defaultErrorHandler(errMsg)
    baseTerm.clear()
    baseTerm.setBackgroundColor(colors.black)
    baseTerm.setTextColor(colors.red)
    local w,h = baseTerm.getSize()
    if(basalt.logging)then
        log(errMsg, "Error")
    end

    local text = wrapText("Basalt error: "..errMsg, w)
    local yPos = 1
    for _,v in pairs(text)do
        baseTerm.setCursorPos(1,yPos)
        baseTerm.write(v)
        yPos = yPos + 1
    end 
    baseTerm.setCursorPos(1,yPos+1)
    updaterActive = false
end

local function handleSchedules(event, p1, p2, p3, p4)
    if(#schedules>0)then
        local finished = {}
        for n=1,#schedules do
            if(schedules[n]~=nil)then 
                if (coroutine.status(schedules[n]) == "suspended")then
                    local ok, result = coroutine.resume(schedules[n], event, p1, p2, p3, p4)
                    if not(ok)then
                        basalt.basaltError(result)
                    end
                else
                    table.insert(finished, n)
                end
            end
        end
        for n=1,#finished do
            table.remove(schedules, finished[n]-(n-1))
        end
    end
end

local function drawFrames()
    if(updaterActive==false)then return end
    if(mainFrame~=nil)then
        mainFrame:render()
        mainFrame:updateTerm()
    end
    for _,v in pairs(monFrames)do
        v:render()
        v:updateTerm()
    end
end

local stopped, moveX, moveY = nil, nil, nil
local moveTimer = nil
local function mouseMoveEvent(_, stp, x, y)
    stopped, moveX, moveY = stp, x, y
    if(moveTimer==nil)then
        moveTimer = os.startTimer(moveThrottle/1000)
    end
end

local function moveHandlerTimer()
    moveTimer = nil
    mainFrame:hoverHandler(moveX, moveY, stopped)
    activeFrame = mainFrame
end

local btn, dragX, dragY = nil, nil, nil
local dragTimer = nil
local function dragHandlerTimer()
    dragTimer = nil
    mainFrame:dragHandler(btn, dragX, dragY)
    activeFrame = mainFrame
end

local function mouseDragEvent(_, b, x, y)
    btn, dragX, dragY = b, x, y
    if(dragThrottle<50)then 
        dragHandlerTimer() 
    else
        if(dragTimer==nil)then
            dragTimer = os.startTimer(dragThrottle/1000)
        end
    end
end


local renderingTimer = nil
local function renderingUpdateTimer()
    renderingTimer = nil
    drawFrames() 
end

local function renderingUpdateEvent(timer)
    if(renderingThrottle<50)then 
        drawFrames() 
    else
        if(renderingTimer==nil)then
            renderingTimer = os.startTimer(renderingThrottle/1000)
        end
    end
end

local function basaltUpdateEvent(event, ...)
    local a = {...}
    if(basaltEvent:sendEvent("basaltEventCycle", event, ...)==false)then return end
    if(event=="terminate")then basalt.stop() end
    if(mainFrame~=nil)then
        local mouseEvents = {
            mouse_click = mainFrame.mouseHandler,
            mouse_up = mainFrame.mouseUpHandler,
            mouse_scroll = mainFrame.scrollHandler,
            mouse_drag = mouseDragEvent,
            mouse_move = mouseMoveEvent,
        }
        local mouseEvent = mouseEvents[event]
        if(mouseEvent~=nil)then
            mouseEvent(mainFrame, ...)
            handleSchedules(event, ...)
            renderingUpdateEvent()
            return
        end
    end

    if(event == "monitor_touch") then
        for k,v in pairs(monFrames)do
            if(v:mouseHandler(1, a[2], a[3], true, a[1]))then
                activeFrame = v
            end
        end
        handleSchedules(event, ...)
        renderingUpdateEvent()
        return
    end

    if(activeFrame~=nil)then
    local keyEvents = {
        char = activeFrame.charHandler,
        key = activeFrame.keyHandler,
        key_up = activeFrame.keyUpHandler,
    }
    local keyEvent = keyEvents[event]
        if(keyEvent~=nil)then
            if(event == "key")then
                activeKey[a[1]] = true
            elseif(event == "key_up")then
                activeKey[a[1]] = false
            end
            keyEvent(activeFrame, ...)
            handleSchedules(event, ...)
            renderingUpdateEvent()
            return
        end
    end

    if(event=="timer")and(a[1]==moveTimer)then
        moveHandlerTimer()
    elseif(event=="timer")and(a[1]==dragTimer)then
        dragHandlerTimer()
    elseif(event=="timer")and(a[1]==renderingTimer)then
        renderingUpdateTimer()
    else
        for _, v in pairs(frames) do
            v:eventHandler(event, ...)
        end
        for _, v in pairs(monFrames) do
            v:eventHandler(event, ...)
        end
        handleSchedules(event, ...)
        renderingUpdateEvent()
    end
end

local loadedObjects = false
local loadedPlugins = false
local function InitializeBasalt()
    if not(loadedObjects)then
        for _,v in pairs(newObjects)do
            if(fs.exists(v))then
                if(fs.isDir(v))then
                    local files = fs.list(v)
                    for _,object in pairs(files)do
                        if not(fs.isDir(v.."/"..object))then
                            local name = object:gsub(".lua", "")
                            if(name~="example.lua")and not(name:find(".disabled"))then
                                if(baseObjects[name]==nil)then
                                    baseObjects[name] = require(v.."."..object:gsub(".lua", ""))
                                else
                                    error("Duplicate object name: "..name)
                                end
                            end
                        end
                    end
                else
                    local name = v:gsub(".lua", "")
                    if(baseObjects[name]==nil)then
                        baseObjects[name] = require(name)
                    else
                        error("Duplicate object name: "..name)
                    end
                end
            end
        end
        loadedObjects = true
    end
    if not(loadedPlugins)then
        moddedObjects = pluginSystem.loadPlugins(baseObjects, bInstance)
        local basaltPlugins = pluginSystem.get("basalt")
        if(basaltPlugins~=nil)then
            for k,v in pairs(basaltPlugins)do
                for a,b in pairs(v(basalt))do
                    basalt[a] = b
                    bInstance[a] = b
                end
            end
        end
        local basaltPlugins = pluginSystem.get("basaltInternal")
        if(basaltPlugins~=nil)then
            for _,v in pairs(basaltPlugins)do
                for a,b in pairs(v(basalt))do
                    bInstance[a] = b
                end
            end
        end
        loadedPlugins = true
    end
end

local function createFrame(name)
    InitializeBasalt()
    for _, v in pairs(frames) do
        if (v:getName() == name) then
            return nil
        end
    end
    local newFrame = moddedObjects["BaseFrame"](name, bInstance)
    newFrame:init()
    newFrame:load()
    newFrame:draw()
    table.insert(frames, newFrame)
    if(mainFrame==nil)and(newFrame:getName()~="basaltDebuggingFrame")then
        newFrame:show()
    end
    return newFrame
end

basalt = {
    basaltError = defaultErrorHandler,
    logging = false,
    dynamicValueEvents = false,
    drawFrames = drawFrames,
    log = log,
    getVersion = function()
        return version
    end,

    memory = function()
        return math.floor(collectgarbage("count")+0.5).."KB"
    end,
    
    addObject = function(path)
        if(fs.exists(path))then
            table.insert(newObjects, path)
        end
    end,

    addPlugin = function(path)
        pluginSystem.addPlugin(path)
    end,

    getAvailablePlugins = function()
        return pluginSystem.getAvailablePlugins()
    end,

    getAvailableObjects = function()
        local objectNames = {}
        for k,_ in pairs(baseObjects)do
            table.insert(objectNames, k)
        end
        return objectNames
    end,

    setVariable = setVariable,
    getVariable = getVariable,

    getObjects = getObjects,
    getObject = getObject,

    createObject = createObject,

    setBaseTerm = function(_baseTerm)
        baseTerm = _baseTerm
    end,

    resetPalette = function()
        for k,v in pairs(colors)do
            if(type(v)=="number")then
                --baseTerm.setPaletteColor(v, colors.packRGB(table.unpack(defaultColors[k])))
            end
        end
    end,

    setMouseMoveThrottle = function(amount)
        if(_HOST:find("CraftOS%-PC"))then
            if(config.get("mouse_move_throttle")~=10)then config.set("mouse_move_throttle", 10) end
            if(amount<100)then
                moveThrottle = 100
            else
                moveThrottle = amount
            end
            return true
        end
        return false
    end,

    setRenderingThrottle = function(amount)
        if(amount<=0)then
            renderingThrottle = 0
        else
            renderingTimer = nil
            renderingThrottle = amount
        end
    end,

    setMouseDragThrottle = function(amount)
        if(amount<=0)then
            dragThrottle = 0
        else
            dragTimer = nil
            dragThrottle = amount
        end
    end,

    autoUpdate = function(isActive)
        updaterActive = isActive
        if(isActive==nil)then updaterActive = true end
        local function f()
            drawFrames()
            while updaterActive do
                basaltUpdateEvent(os.pullEventRaw())
            end
        end
        while updaterActive do
            local ok, err = xpcall(f, debug.traceback)
            if not(ok)then
                basalt.basaltError(err)
            end
        end
    end,
    
    update = function(event, ...)
        if (event ~= nil) then
            local args = {...}
            local ok, err = xpcall(function() basaltUpdateEvent(event, table.unpack(args)) end, debug.traceback)
            if not(ok)then
                basalt.basaltError(err)
                return
            end
        end
    end,
    
    stop = stop,
    stopUpdate = stop,
    
    isKeyDown = function(key)
        if(activeKey[key]==nil)then return false end
        return activeKey[key];
    end,
    
    getFrame = function(name)
        for _, value in pairs(frames) do
            if (value.name == name) then
                return value
            end
        end
    end,
    
    getActiveFrame = function()
        return activeFrame
    end,
    
    setActiveFrame = function(frame)
        if (frame:getType() == "Container") then
            activeFrame = frame
            return true
        end
        return false
    end,

    getMainFrame = function()
        return mainFrame
    end,
    
    onEvent = function(...)
        for _,v in pairs(table.pack(...))do
            if(type(v)=="function")then
                basaltEvent:registerEvent("basaltEventCycle", v)
            end
        end
    end,

    schedule = schedule,
    
    addFrame  = createFrame,
    createFrame = createFrame,

    addMonitor = function(name)
        InitializeBasalt()
        for _, v in pairs(frames) do
            if (v:getName() == name) then
                return nil
            end
        end
        local newFrame = moddedObjects["MonitorFrame"](name, bInstance)
        newFrame:init()
        newFrame:load()
        newFrame:draw()
        table.insert(monFrames, newFrame)
        return newFrame
    end,
    
    removeFrame = function(name)
        frames[name] = nil
    end,

    setProjectDir = function(dir)
        projectDirectory = dir
    end,
}

local basaltPlugins = pluginSystem.get("basalt")
if(basaltPlugins~=nil)then
    for k,v in pairs(basaltPlugins)do
        for a,b in pairs(v(basalt))do
            basalt[a] = b
            bInstance[a] = b
        end
    end
end
local basaltPlugins = pluginSystem.get("basaltInternal")
if(basaltPlugins~=nil)then
    for k,v in pairs(basaltPlugins)do
        for a,b in pairs(v(basalt))do
            bInstance[a] = b
        end
    end
end

return basalt
