local utils = require("utils")
local tHex = require("tHex")

local sub, find, insert = string.sub, string.find, table.insert

return function(name, basalt)
    local base = basalt.getObject("Object")(name, basalt)
    -- Base object
    local objectType = "VisualObject" -- not changeable

    local isVisible,ignOffset,isHovered,isClicked,isDragging = true,false,false,false,false
    local zIndex = 1

    local x, y, width, height = 1,1,1,1
    local dragStartX, dragStartY, dragXOffset, dragYOffset = 0, 0, 0, 0

    local bgColor,fgColor, transparency = colors.black, colors.white, false
    local parent

    local preDrawQueue = {}
    local drawQueue = {}
    local postDrawQueue = {}

    local renderObject = {}

    local function split(str, d)
        local result = {}
        if str == "" then
            return result
        end
        d = d or " "
        local start = 1
        local delim_start, delim_end = find(str, d, start)
            while delim_start do
                insert(result, {x=start, value=sub(str, start, delim_start - 1)})
                start = delim_end + 1
                delim_start, delim_end = find(str, d, start)
            end
        insert(result, {x=start, value=sub(str, start)})
        return result
    end


    local object = {
        getType = function(self)
            return objectType
        end,

        getBase = function(self)
            return base
        end,  
      
        isType = function(self, t)
            return objectType==t or base.isType~=nil and base.isType(t) or false
        end,

        getBasalt = function(self)
            return basalt
        end,

        show = function(self)
            isVisible = true
            self:updateDraw()
            return self
        end,

        hide = function(self)
            isVisible = false
            self:updateDraw()
            return self
        end,

        isVisible = function(self)
            return isVisible
        end,

        setVisible = function(self, _isVisible)
            isVisible = _isVisible or not isVisible
            self:updateDraw()
            return self
        end,

        setTransparency = function(self, _transparency)
            transparency = _transparency~= nil and _transparency or true
            self:updateDraw()
            return self
        end,

        setParent = function(self, newParent, noRemove)
            base.setParent(self, newParent, noRemove)
            parent = newParent
            return self
        end,

        setFocus = function(self)
            if (parent ~= nil) then
                parent:setFocusedChild(self)
            end
            return self
        end,

        setZIndex = function(self, index)
            zIndex = index
            if (parent ~= nil) then
                parent:updateZIndex(self, zIndex)
                self:updateDraw()
            end            
            return self
        end,

        getZIndex = function(self)
            return zIndex
        end,

        updateDraw = function(self)
            if (parent ~= nil) then
                parent:updateDraw()
            end
            return self
        end,

        setPosition = function(self, xPos, yPos, rel)
            local curX, curY = x, y
            if(type(xPos)=="number")then
                x = rel and x+xPos or xPos
            end
            if(type(yPos)=="number")then
                y = rel and y+yPos or yPos
            end
            if(parent~=nil)then parent:customEventHandler("basalt_FrameReposition", self) end
            if(self:getType()=="Container")then parent:customEventHandler("basalt_FrameReposition", self) end
            self:updateDraw()
            self:repositionHandler(curX, curY)
            return self
        end,

        getX = function(self)
            return x
        end,

        setX = function(self, newX)
            return self:setPosition(newX, y)
        end,

        getY = function(self)
            return y
        end,

        setY = function(self, newY)
            return self:setPosition(x, newY)
        end,

        getPosition = function(self)
            return x, y
        end,

        setSize = function(self, newWidth, newHeight, rel)
            local oldW, oldH = width, height
            if(type(newWidth)=="number")then
                width = rel and width+newWidth or newWidth
            end
            if(type(newHeight)=="number")then
                height = rel and height+newHeight or newHeight
            end
            if(parent~=nil)then 
                parent:customEventHandler("basalt_FrameResize", self)
                if(self:getType()=="Container")then parent:customEventHandler("basalt_FrameResize", self) end
            end
            self:resizeHandler(oldW, oldH)
            self:updateDraw()
            return self
        end,

        getHeight = function(self)
            return height
        end,

        setHeight = function(self, newHeight)
            return self:setSize(width, newHeight)
        end,

        getWidth = function(self)
            return width
        end,

        setWidth = function(self, newWidth)
            return self:setSize(newWidth, height)
        end,

        getSize = function(self)
            return width, height
        end,

        setBackground = function(self, color)
            bgColor = color
            self:updateDraw()
            return self
        end,

        getBackground = function(self)
            return bgColor
        end,

        setForeground = function(self, color)
            fgColor = color or false
            self:updateDraw()
            return self
        end,

        getForeground = function(self)
            return fgColor
        end,

        getAbsolutePosition = function(self, x, y)
            -- relative position to absolute position
            if (x == nil) or (y == nil) then
                x, y = self:getPosition()
            end

            if (parent ~= nil) then
                local fx, fy = parent:getAbsolutePosition()
                x = fx + x - 1
                y = fy + y - 1
            end
            return x, y
        end,

        ignoreOffset = function(self, ignore)
            ignOffset = ignore
            if(ignore==nil)then ignOffset = true end
            return self
        end,

        getIgnoreOffset = function(self)
            return ignOffset
        end,

        isCoordsInObject = function(self, x, y)
            if(isVisible)and(self:isEnabled())then
                if(x==nil)or(y==nil)then return false end
                local objX, objY = self:getAbsolutePosition()
                local w, h = self:getSize()            
                if (objX <= x) and (objX + w > x) and (objY <= y) and (objY + h > y) then
                    return true
                end
            end
            return false
        end,

        onGetFocus = function(self, ...)
            for _,v in pairs(table.pack(...))do
                if(type(v)=="function")then
                    self:registerEvent("get_focus", v)
                end
            end
            return self
        end,

        onLoseFocus = function(self, ...)
            for _,v in pairs(table.pack(...))do
                if(type(v)=="function")then
                    self:registerEvent("lose_focus", v)
                end
            end
            return self
        end,

        isFocused = function(self)
            if (parent ~= nil) then
                return parent:getFocusedObject() == self
            end
            return true
        end,

        resizeHandler = function(self, ...)
            if(self:isEnabled())then
                local val = self:sendEvent("basalt_resize", ...)
                if(val==false)then return false end
            end
            return true
        end,

        repositionHandler = function(self, ...)
            if(self:isEnabled())then
                local val = self:sendEvent("basalt_reposition", ...)
                if(val==false)then return false end
            end
            return true
        end,

        onResize = function(self, ...)
            for _,v in pairs(table.pack(...))do
                if(type(v)=="function")then
                    self:registerEvent("basalt_resize", v)
                end
            end
            return self
        end,

        onReposition = function(self, ...)
            for _,v in pairs(table.pack(...))do
                if(type(v)=="function")then
                    self:registerEvent("basalt_reposition", v)
                end
            end
            return self
        end,

        mouseHandler = function(self, button, x, y, isMon)
            if(self:isCoordsInObject(x, y))then
                local objX, objY = self:getAbsolutePosition()
                local val = self:sendEvent("mouse_click", button, x - (objX-1), y - (objY-1), x, y, isMon)
                if(val==false)then return false end
                if(parent~=nil)then
                    parent:setFocusedChild(self)
                end
                isClicked = true
                isDragging = true
                dragStartX, dragStartY = x, y 
                return true
            end
        end,

        mouseUpHandler = function(self, button, x, y)
            isDragging = false
            if(isClicked)then
                local objX, objY = self:getAbsolutePosition()
                local val = self:sendEvent("mouse_release", button, x - (objX-1), y - (objY-1), x, y)
                isClicked = false
            end
            if(self:isCoordsInObject(x, y))then
                local objX, objY = self:getAbsolutePosition()
                local val = self:sendEvent("mouse_up", button, x - (objX-1), y - (objY-1), x, y)
                if(val==false)then return false end
                return true
            end
        end,

        dragHandler = function(self, button, x, y)
            if(isDragging)then 
                local objX, objY = self:getAbsolutePosition()
                local val = self:sendEvent("mouse_drag", button, x - (objX-1), y - (objY-1), dragStartX-x, dragStartY-y, x, y)
                dragStartX, dragStartY = x, y 
                if(val~=nil)then return val end
                if(parent~=nil)then
                    parent:setFocusedChild(self)
                end
                return true
            end

            if(self:isCoordsInObject(x, y))then
                local objX, objY = self:getAbsolutePosition()
                dragStartX, dragStartY = x, y 
                dragXOffset, dragYOffset = objX - x, objY - y
            end
        end,

        scrollHandler = function(self, dir, x, y)
            if(self:isCoordsInObject(x, y))then
                local objX, objY = self:getAbsolutePosition()
                local val = self:sendEvent("mouse_scroll", dir, x - (objX-1), y - (objY-1))
                if(val==false)then return false end
                if(parent~=nil)then
                    parent:setFocusedChild(self)
                end
                return true
            end
        end,

        hoverHandler = function(self, x, y, stopped)
            if(self:isCoordsInObject(x, y))then
                local val = self:sendEvent("mouse_hover", x, y, stopped)
                if(val==false)then return false end
                isHovered = true
                return true
            end
            if(isHovered)then
                local val = self:sendEvent("mouse_leave", x, y, stopped)
                if(val==false)then return false end
                isHovered = false
            end
        end,

        keyHandler = function(self, key, isHolding)
            if(self:isEnabled())and(isVisible)then
                if (self:isFocused()) then
                local val = self:sendEvent("key", key, isHolding)
                if(val==false)then return false end
                return true
                end
            end
        end,

        keyUpHandler = function(self, key)
            if(self:isEnabled())and(isVisible)then
                if (self:isFocused()) then
                    local val = self:sendEvent("key_up", key)
                if(val==false)then return false end
                return true
                end
            end
        end,

        charHandler = function(self, char)
            if(self:isEnabled())and(isVisible)then
                if(self:isFocused())then
                local val = self:sendEvent("char", char)
                if(val==false)then return false end
                return true
                end
            end
        end,

        getFocusHandler = function(self)
            local val = self:sendEvent("get_focus")
            if(val~=nil)then return val end
            return true
        end,

        loseFocusHandler = function(self)
            isDragging = false
            local val = self:sendEvent("lose_focus")
            if(val~=nil)then return val end
            return true
        end,

        addDraw = function(self, name, f, pos, typ, active)
            local queue = (typ==nil or typ==1) and drawQueue or typ==2 and preDrawQueue or typ==3 and postDrawQueue
            pos = pos or #queue+1
            if(name~=nil)then
                for k,v in pairs(queue)do
                    if(v.name==name)then 
                        table.remove(queue, k)
                        break
                    end
                end
                local t = {name=name, f=f, pos=pos, active=active~=nil and active or true}
                table.insert(queue, pos, t)
            end
            self:updateDraw()
            return self
        end,

        addPreDraw = function(self, name, f, pos, typ)
            self:addDraw(name, f, pos, 2)
            return self
        end,

        addPostDraw = function(self, name, f, pos, typ)
            self:addDraw(name, f, pos, 3)
            return self
        end,

        setDrawState = function(self, name, state, typ)
            local queue = (typ==nil or typ==1) and drawQueue or typ==2 and preDrawQueue or typ==3 and postDrawQueue
            for k,v in pairs(queue)do
                if(v.name==name)then 
                    v.active = state
                    break
                end
            end
            self:updateDraw()
            return self
        end,

        getDrawId = function(self, name, typ)
            local queue = typ==1 and drawQueue or typ==2 and preDrawQueue or typ==3 and postDrawQueue or drawQueue
            for k,v in pairs(queue)do
                if(v.name==name)then 
                    return k
                end
            end
        end,

        addText = function(self, x, y, text)
            local obj = self:getParent() or self
            local xPos,yPos = self:getPosition()
            if(parent~=nil)then
                local xO, yO = parent:getOffset()
                xPos = ignOffset and xPos or xPos - xO
                yPos = ignOffset and yPos or yPos - yO
            end
            if not(transparency)then
                obj:setText(x+xPos-1, y+yPos-1, text)
                return
            end
            local t = split(text, "\0")
            for k,v in pairs(t)do
                if(v.value~="")and(v.value~="\0")then
                    obj:setText(x+v.x+xPos-2, y+yPos-1, v.value)
                end
            end
        end,

        addBG = function(self, x, y, bg, noText)
            local obj = parent or self
            local xPos,yPos = self:getPosition()
            if(parent~=nil)then
                local xO, yO = parent:getOffset()
                xPos = ignOffset and xPos or xPos - xO
                yPos = ignOffset and yPos or yPos - yO
            end
            if not(transparency)then
                obj:setBG(x+xPos-1, y+yPos-1, bg)
                return
            end
            local t = split(bg)
            for k,v in pairs(t)do
                if(v.value~="")and(v.value~=" ")then
                    if(noText~=true)then
                        obj:setText(x+v.x+xPos-2, y+yPos-1, (" "):rep(#v.value))
                        obj:setBG(x+v.x+xPos-2, y+yPos-1, v.value)
                    else
                        table.insert(renderObject, {x=x+v.x-1,y=y,bg=v.value})
                        obj:setBG(x+xPos-1, y+yPos-1, fg)
                    end
                end
            end
        end,

        addFG = function(self, x, y, fg)
            local obj = parent or self
            local xPos,yPos = self:getPosition()
            if(parent~=nil)then
                local xO, yO = parent:getOffset()
                xPos = ignOffset and xPos or xPos - xO
                yPos = ignOffset and yPos or yPos - yO
            end
            if not(transparency)then
                obj:setFG(x+xPos-1, y+yPos-1, fg)
                return
            end
            local t = split(fg)
            for k,v in pairs(t)do
                if(v.value~="")and(v.value~=" ")then
                    obj:setFG(x+v.x+xPos-2, y+yPos-1, v.value)
                end
            end
        end,

        addBlit = function(self, x, y, t, fg, bg)
            local obj = parent or self
            local xPos,yPos = self:getPosition()
            if(parent~=nil)then
                local xO, yO = parent:getOffset()
                xPos = ignOffset and xPos or xPos - xO
                yPos = ignOffset and yPos or yPos - yO
            end
            if not(transparency)then
                obj:blit(x+xPos-1, y+yPos-1, t, fg, bg)
                return
            end
            local _text = split(t, "\0")
            local _fg = split(fg)
            local _bg = split(bg)
            for k,v in pairs(_text)do
                if(v.value~="")or(v.value~="\0")then
                    obj:setText(x+v.x+xPos-2, y+yPos-1, v.value)
                end
            end
            for k,v in pairs(_bg)do
                if(v.value~="")or(v.value~=" ")then
                    obj:setBG(x+v.x+xPos-2, y+yPos-1, v.value)
                end
            end
            for k,v in pairs(_fg)do
                if(v.value~="")or(v.value~=" ")then
                    obj:setFG(x+v.x+xPos-2, y+yPos-1, v.value)
                end
            end
        end,

        addTextBox = function(self, x, y, w, h, text)
            local obj = parent or self
            local xPos,yPos = self:getPosition()
            if(parent~=nil)then
                local xO, yO = parent:getOffset()
                xPos = ignOffset and xPos or xPos - xO
                yPos = ignOffset and yPos or yPos - yO
            end
            obj:drawTextBox(x+xPos-1, y+yPos-1, w, h, text)
        end,

        addForegroundBox = function(self, x, y, w, h, col)
            local obj = parent or self
            local xPos,yPos = self:getPosition()
            if(parent~=nil)then
                local xO, yO = parent:getOffset()
                xPos = ignOffset and xPos or xPos - xO
                yPos = ignOffset and yPos or yPos - yO
            end
            obj:drawForegroundBox(x+xPos-1, y+yPos-1, w, h, col)
        end,

        addBackgroundBox = function(self, x, y, w, h, col)
            local obj = parent or self
            local xPos,yPos = self:getPosition()
            if(parent~=nil)then
                local xO, yO = parent:getOffset()
                xPos = ignOffset and xPos or xPos - xO
                yPos = ignOffset and yPos or yPos - yO
            end
            obj:drawBackgroundBox(x+xPos-1, y+yPos-1, w, h, col)
        end,

        render = function(self)
            if (isVisible)then
                self:redraw()
            end
        end,

        redraw = function(self)
            for k,v in pairs(preDrawQueue)do
                if (v.active)then
                    v.f(self)
                end
            end
            for k,v in pairs(drawQueue)do
                if (v.active)then
                    v.f(self)
                end
            end
            for k,v in pairs(postDrawQueue)do
                if (v.active)then
                    v.f(self)
                end
            end
            return true
        end,

        draw = function(self)
            self:addDraw("base", function()
                local w,h = self:getSize()
                if(bgColor~=false)then
                    self:addTextBox(1, 1, w, h, " ")
                    self:addBackgroundBox(1, 1, w, h, bgColor)
                end
                if(fgColor~=false)then
                    self:addForegroundBox(1, 1, w, h, fgColor)
                end
            end, 1)
        end,
    }
    object.__index = object
    return setmetatable(object, base)
end