--[[--
A Layout contains a tree of widgets with a single `root` widget.

Layouts will resize to fit the window unless a `top` or `left`
property is found in the root widget.

Layouts are drawn in the order that they were shown, so the
most recently shown layout shown will always appear on top.

Other events are sent to layouts in the opposite direction,
and are trapped by the first layout that can handle the event
(for example, the topmost layer that is focused or hovered).

@classmod Layout
--]]--

local ROOT = (...):gsub('[^.]*$', '')

local Backend = require(ROOT .. 'backend')
local Base = require(ROOT .. 'base')
local Event = require(ROOT .. 'event')
local Widget = require(ROOT .. 'widget')
local Input = require(ROOT .. 'input')
local Style = require(ROOT .. 'style')

local Layout = Base:extend()

Layout.isLayout = true

--[[--
Layout constructor.

@function Luigi.Layout

@tparam table data
A tree of widget data.

@treturn Layout
A Layout instance.
--]]--
function Layout:constructor (data, master)
    data = data or {}

    if master then
        self:setMaster(master)
    else
        self:setTheme(require(ROOT .. 'theme.light'))
        self:setStyle()
    end

    self:addDefaultHandlers()

    self.hooks = {}
    self.isShown = false
    self.root = data

    Widget(self, data)

    self.isReady = true
end

--[[--
Create a detached widget.

Internal function used to create widgets that are associated with
a layout, but "detached" from it.

Used by context menus, which use their "owner" widget's layout
for theme and style information but appear in a separate layout.

@tparam table data
A tree of widget data.

@treturn Widget
A widget instance.
--]]--
function Layout:createWidget (data)
    return Widget(self, data)
end

local function clearWidget (widget)
    widget.textData = nil
    widget.fontData = nil
    widget.position = {}
    widget.dimensions = {}
    widget.type = widget.type
    for _, child in ipairs(widget) do
        clearWidget(child)
    end
    local items = widget.items
    if items then
        for _, item in ipairs(items) do
            clearWidget(item)
        end
    end
end

local function reset (self)
    if not self.root then return end
    clearWidget(self.root)
end

--[[--
Set the master layout for this layout.

This layout's theme and style will be set the same as the master layout, and
widgets added to this layout will be indexed and keyboard-accelerated by the
master layout instead of this layout.

@tparam Layout layout
Master layout

@treturn Layout Self
--]]--
function Layout:setMaster (layout)
    self.master = layout

    reset(self)
    return self
end

--[[--
Set the style from a definition table or function.

@tparam table|function rules
Style definition.

@treturn Layout Self
--]]--
function Layout:setStyle (rules)
    if type(rules) == 'function' then
        rules = rules()
    end
    self.style = Style(rules or {}, { 'style' })

    reset(self)
    return self
end

--[[--
Set the theme from a definition table or function.

@tparam table|function rules
Theme definition.
--]]--
function Layout:setTheme (rules)
    if type(rules) == 'function' then
        rules = rules()
    end
    self.theme = Style(rules or {}, { 'type' })
    reset(self)
    return self
end

--[[--
Get the style from master layout or this layout.

@treturn table
Style table.
--]]--
function Layout:getStyle ()
    return self.master and self.master:getStyle() or self.style
end

--[[--
Get the theme from master layout or this layout.

@treturn table
Theme table.
--]]--
function Layout:getTheme ()
    return self.master and self.master:getTheme() or self.theme
end

--[[--
Show the layout.

Hooks all appropriate Love events and callbacks.

@treturn Layout
Return this layout for chaining.
--]]--
function Layout:show ()
    if self.isShown then
        Backend.hide(self)
        self.isShown = nil
    end

    self.isShown = true

    if not self.input then
        self.input = Input.default -- Input(self)
    end

    Backend.show(self)
    self.root:reshape()

    Event.Show:emit(self, self)
    return self
end

--[[--
Hide the layout.

Unhooks Love events and callbacks.

@treturn Layout
Return this layout for chaining.
--]]--
function Layout:hide ()
    if not self.isShown then
        return
    end
    self.isShown = nil
    Backend.hide(self)

    Event.Hide:emit(self, self)
    return self
end

--[[--
Focus next focusable widget.

Traverses widgets using Widget:getNextNeighbor until a focusable widget is
found, and focuses that widget.

@treturn Widget
The widget that was focused, or nil
--]]--
function Layout:focusNextWidget ()
    local widget = self.focusedWidget or self.root
    local nextWidget = widget:getNextNeighbor()

    while nextWidget ~= widget do
        if nextWidget:focus() then return nextWidget end
        nextWidget = nextWidget:getNextNeighbor()
    end
end

--[[--
Focus previous focusable widget.

Traverses widgets using Widget:getPreviousNeighbor until a focusable widget is
found, and focuses that widget.

@treturn Widget
The widget that was focused, or nil
--]]--
function Layout:focusPreviousWidget ()
    local widget = self.focusedWidget or self.root
    local previousWidget = widget:getPreviousNeighbor()

    while previousWidget ~= widget do
        if previousWidget:focus() then return previousWidget end
        previousWidget = previousWidget:getPreviousNeighbor()
    end
end

--[[--
Get the innermost widget at given coordinates.

@tparam number x
Number of pixels from window's left edge.

@tparam number y
Number of pixels from window's top edge.

@tparam[opt] Widget root
Widget to search within, defaults to layout root.
--]]--
function Layout:getWidgetAt (x, y, root)
    if not root then
        root = self.root
    end
    -- Loop through in reverse, because siblings defined later in the tree
    -- will overdraw earlier siblings.
    for i = #root, 1, -1 do
        local child = root[i]
        if child:isAt(x, y) then
            local inner = self:getWidgetAt(x, y, child)
            if inner then return inner end
        end
    end

    if root:isAt(x, y) then return root end
end

--[[--
Place a layout near a point or rectangle.

@tparam number left
Number of pixels from window's left edge.

@tparam number top
Number of pixels from window's top edge.

@tparam[opt] number width
Width of the rectangle to place layout outside of, defaults to 0.

@tparam[opt] number height
Height of the rectangle to place layout outside of, defaults to 0.

@treturn Layout
Return this layout for chaining.
--]]--
function Layout:placeNear (left, top, width, height)
    width, height = width or 0, height or 0
    local root = self.root
    -- place to the left if there's no room to the right
    local layoutWidth = root:getWidth()
    local windowWidth, windowHeight = Backend.getWindowSize()
    if left + width + layoutWidth > windowWidth then
        left = left - layoutWidth - width
    else
        left = left + width
    end
    -- place above if there's no room below
    local layoutHeight = root:getHeight()
    if top + height + layoutHeight > windowHeight then
        top = top - layoutHeight - height
    else
        top = top + height
    end
    root.left = left
    root.top = top
end


-- Add handlers for keyboard shortcuts, tab focus, and mouse wheel scroll
function Layout:addDefaultHandlers ()
    self.shortcuts = {}

    for i = 0, 15 do
        self.shortcuts[i] = {}
    end

    self.behavior = {}

    local function createBehavior (name, hooks)
        self.behavior[name] = hooks
        function hooks.destroy ()
            for _, hook in ipairs(hooks) do
                hook:unhook()
            end
            self.behavior[name] = nil
        end
    end

    createBehavior('context', {
        self:onPressStart(function (event)
            -- show context menu on right click
            if event.button ~= 'right' then return end
            local menu = event.target.context
            if not menu then return end
            menu:bubbleEvent('PressStart', event)
            -- make sure it fits in the window
            -- TODO: open in a new borderless window under SDL?
            menu.menuLayout:placeNear(event.x - 1, event.y - 1, 2, 2)
            return false
        end)
    })

    createBehavior('shortcut', {
        self:onKeyPress(function (event)
            local entry = self.shortcuts[event.modifierFlags]
            local widget = entry and entry[event.key]
            if not widget then return end
            widget.hovered = true
            self.input:handlePressStart(self, 'left', event.x, event.y,
                widget, widget.shortcut)
            return false
        end),

        self:onKeyRelease(function (event)
            local entry = self.shortcuts[event.modifierFlags]
            local widget = entry and entry[event.key]
            if not widget then return end
            widget.hovered = false
            self.input:handlePressEnd(self, 'left', event.x, event.y,
                widget, widget.shortcut)
            return false
        end)
    })

    createBehavior('navigate', {
        self:onKeyPress(function (event)
            -- tab/shift-tab cycles focused widget
            if event.key == 'tab' then
                if Backend.isKeyDown('lshift', 'rshift') then
                    self:focusPreviousWidget()
                else
                    self:focusNextWidget()
                end
                return false
            end

            -- space/enter presses focused widget
            local widget = self.focusedWidget
            if widget and event.key == 'space' or event.key == ' '
            or event.key == 'return' then
                self.input:handlePressStart(self, 'left', event.x, event.y,
                    widget, event.key)
                return false
            end
        end),

        self:onKeyRelease(function (event)
            -- space / enter presses focused widget
            local widget = self.focusedWidget
            if widget and event.key == 'space' or event.key == ' '
            or event.key == 'return' then
                self.input:handlePressEnd(self, 'left', event.x, event.y,
                    widget, event.key)
                return false
            end
        end)
    })

    createBehavior('scroll', {
        self:onWheelMove(function (event)
            if not event.hit then return end
            local amount = event.scrollY ~= 0 and event.scrollY or event.scrollX
            for widget in event.target:eachAncestor(true) do
                if widget:scrollBy(amount) then return false end
            end -- ancestor loop
            return false
        end) -- wheel move
    })

    createBehavior('status', {
        self:onEnter(function (event)
            local statusWidget = (self.master or self).statusWidget
            if not statusWidget then return end

            statusWidget.text = event.target.status
            return false
        end)
    })

end

Event.injectBinders(Layout)

return Layout
