--   this file is part of reno desktop.
--
--  reno desktop is free software: you can redistribute it and/or modify it under the terms of the gnu general public license as published by the free software foundation, either version 3 of the license, or (at your option) any later version.
--
--  reno desktop is distributed in the hope that it will be useful, but without any warranty; without even the implied warranty of merchantability or fitness for a particular purpose. see the gnu general public license for more details.
--
--  you should have received a copy of the gnu general public license along with reno desktop. if not, see <https://www.gnu.org/licenses/>. 
-- renotk (formerly awmtk2) - template/granular styling library for reno
local wibox = require("wibox")
local gears = require("gears")
local beautiful = require("beautiful")
beautiful.widgets = beautiful.widgets or {}
beautiful.templates = beautiful.templates or {}

local awmtk = {}

-- {{{ utils
awmtk.create_delta = function(name,instance_delta,class_delta,parent_delta,vertical)
    -- to save memory, we create proxies for lower layers called "deltas"
    -- this function creates that proxy layer using metatables
    -- fun story - i used instance_delta instead of {} at first.
    -- the results were horrifying and confusing.
    return setmetatable({},{
        __index = function(_,k)
            -- per-instance overrides are top priority
            if rawget(instance_delta,k) then
                return rawget(instance_delta,k)
            -- class-wide overrides are second in priority
            elseif type(class_delta[name]) == "table"
                and rawget(class_delta[name],k) then
                if vertical and
                   type(rawget(class_delta[name],"vertical")) == "table" and
                   rawget(class_delta[name],"vertical")[k] then
                    return rawget(class_delta[name].vertical,k)
                else
                    return rawget(class_delta[name],k)
                end
            -- parent is fallback
            elseif parent_delta[k] then
                return parent_delta[k]
            end
        end
    })
end

awmtk.create_style = function(name,parent,overrides,vertical)
    -- a style is essentially a layer of deltas upon the previous (parent) style
    local new_style = {}
    local odelta = (overrides and overrides[name]) or {}
    local cdelta = beautiful.widgets[name] or {}
    for class_name,parent_class in pairs(parent) do
        new_style[class_name] = awmtk.create_delta(
            class_name,
            odelta,
            cdelta,
            parent_class,
            vertical
        )
    end
    return new_style
end

awmtk.create_template_lib = function(name,parent,overrides)
    -- same thing but beautiful.templates 
    return awmtk.create_delta(
        name,
        overrides or {},
        beautiful.templates or {},
        parent
    )
end

awmtk.build_templates = function(templates,style,vertical)
    local new_templates = {}
    for name,_ in pairs(awmtk.proto_templates) do
        new_templates[name] = templates[name](style,vertical)
    end
    return new_templates
end

awmtk.mask_object_call = function(obj,call)
    local new = {}
    for k,v in pairs(obj) do
        new[k] = v
    end
    return setmetatable(new,{
        __index = obj,
        __call = call
    })
end

awmtk.wrap_hooks = function(w,callbacks)
    -- attach hooks to function
    local mcall = (getmetatable(w) and getmetatable(w).__call) or w
    local call_wrapper = function(...)
        if callbacks and callbacks.on_create_pre then
            callbacks.on_create_pre(...)
        end
        local widget = mcall(...)
        if callbacks and callbacks.on_create then
            callbacks.on_create(widget,...)
        end
        if callbacks and callbacks.on_ready then
            callbacks._on_ready_called = false
            local func = function()
                if not callbacks._on_ready_called then
                    callbacks.on_ready(widget)
                    callbacks._on_ready_called = true
                end
            end
            widget:connect_signal("widget::layout_changed",func)
            widget:connect_signal("widget::layout_changed",function()
                widget:disconnect_signal("widget::layout_changed",func)
            end)
        end
        return widget
    end
    return (getmetatable(w) and awmtk.mask_object_call(w,call_wrapper)) or
           call_wrapper
end

awmtk.merge = gears.table.join
-- }}}


-- {{{ default style

-- prototype style
-- notice that it's not awmtk.default style - it's used as a base for default.
awmtk.proto_style = {
    base = setmetatable({
        -- { backgrounds
        -- custom background color for highlighting elements
        bg_highlight = beautiful.bg_highlight or beautiful.bg_focus,
        -- allow more complex themes to define background images
        bgimage_focus = beautiful.bgimage_focus,
        bgimage_normal = beautiful.bgimage_normal,
        -- }
        -- { borders
        -- borders for popups
        shape_border_width = beautiful.shape_border_width or 0,
        shape_border_color = beautiful.shape_border_color or beautiful.bg_normal,
        -- }
        -- { callbacks
        -- a tiny bit more complex thing to account for more extensibility 
        -- the stub functions do nothing - you should implement functionality inside theme
        onpress = function() end,
        onrelease = function() end,
        -- }
        -- { shapes
        margins = 5,
        spacing = 5,
        shape = function(cr, width, height)
            return require("gears").shape.rounded_rect(cr,width,height,5)
        end,
        -- }
    },{__index = beautiful})
}

-- subclasses
awmtk.proto_style.container = awmtk.create_delta("container",{
},awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.button = awmtk.create_delta("button",{
    margins = 3
},awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.icon = awmtk.create_delta("icon",{
    margins = 1
},awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.textbox = awmtk.create_delta("textbox",{
    font = beautiful.font or "sans 8"
}, awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.separator = awmtk.create_delta("separator",{
    thickness = 1,
    color = beautiful.bg_focus,
    border_width = 0
}, awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.article = awmtk.create_delta("article", {
    icon_size = 16,
    small_font = beautiful.small_font or beautiful.font,
    font_align = "left",
    small_font_align = "left"
}, awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.popup = awmtk.create_delta("popup", {
}, awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.titlebar = awmtk.create_delta("titlebar", {
    margins = 1
}, awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.wibar = awmtk.create_delta("wibar", {
    margins = 1
}, awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.menu = awmtk.create_delta("menu", {
    margins = 1
}, awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.center = awmtk.create_delta("center", {
    margins = 1
}, awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.slider = awmtk.create_delta("slider", {
    margins = 1
}, awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.checkbox = awmtk.create_delta("checkbox", {
}, awmtk.proto_style,awmtk.proto_style.base)

awmtk.proto_style.icon = awmtk.create_delta("icon", {
    constraint = "max",
    height = 100,
    width = 100,
    valign = "center",
    halign = "center",
    margins = 0
}, awmtk.proto_style,awmtk.proto_style.base)
-- }}}  

-- {{{ generic templates 
awmtk.proto_templates = {
    -- templates are built first using the given style, then applied to contents
    -- through returned function
    container = function(style)
        -- container is practically any "box" that contains buttons, textboxes,
        -- and anything you want to put into the container. do not confuse with
        -- popup - containers are designed to separate contents within a popup.
        return function(layout,options)
            return awmtk.merge({
                {
                    layout,
                    margins = style.container.margins,
                    layout = wibox.container.margin
                },
                bgimage = style.container.bgimage_normal,
                bg = style.container.bg_normal,
                fg = style.container.fg_normal,
                shape = style.container.shape,
                shape_border_color = style.container.shape_border_color,
                shape_border_width = style.container.shape_border_width,
                widget = awmtk.wrap_hooks(wibox.container.background,options)
            },options or {})
        end
    end,

    button = function(style)
        -- self explanatory. notice that this does not bear any function - 
        -- only the visual part of the button. by design, onpress and onrelease
        -- callbacks should be connected to button events for animations
        return function(layout,options)
            return awmtk.merge({
                {
                    layout,
                    margins = style.button.margins,
                    layout = wibox.container.margin
                },
                bgimage = style.button.bgimage_normal,
                bg = style.button.bg_normal,
                fg = style.button.fg_normal,
                shape = style.button.shape,
                shape_border_color = style.button.shape_border_color,
                shape_border_width = style.button.shape_border_width,
                widget = awmtk.wrap_hooks(wibox.container.background,options)
            },options or {})
        end
    end,

    textbox = function(style)
        -- nothing fancy here, but you can set per-widget fonts using beautiful.
        return function(options)
            return awmtk.merge({
                font = style.textbox.font,
                widget = awmtk.wrap_hooks(wibox.widget.textbox,options)
            },options or {})
        end
    end,

    hseparator = function(style)
        -- wow, i guess?
        return function(options)
            return awmtk.merge({
                widget = awmtk.wrap_hooks(wibox.widget.separator,options),
                orientation = "horizontal",
                thickness = style.separator.thickness,
                color = style.separator.color,
                border_width = style.separator.border_width
            },options or {})
        end
    end,

    icon = function(style)
        return function(args)
            args = args or {}
            return {
                {
                    {
                        {
                            image = args.image,
                            id = args.id,
                            resize = args.resize,
                            widget = wibox.widget.imagebox
                        },
                        widget = wibox.container.margin,
                        margins = args.margins or style.icon.margins
                    },
                    strategy = args.constraint or style.icon.constraint,
                    height = args.height or
                        style.icon.height,
                    width = args.width or
                        style.icon.width,
                    widget = awmtk.wrap_hooks(wibox.container.constraint,
                        args),
                    id = "constraint"
                },
                --widget = awmtk.wrap_hooks(
                widget = wibox.container.place,
                 --   args
                --),
                valign = args.valign or style.icon.valign,
                halign = style.icon.halign or style.icon.halign
            }
        end
    end,

    vseparator = function(style)
        -- i'm running out of comments
        return function(options)
            return awmtk.merge({
                widget = awmtk.wrap_hooks(wibox.widget.separator,options),
                orientation = "vertical",
                thickness = style.separator.thickness,
                color = style.separator.color,
                border_width = style.separator.border_width
            },options or {})
        end
    end,

    article = function(style)
        -- article is a template that combines 3 common pieces of a full item:
        -- icon, name and description. designed to be placed within a container
        -- or a button. 
        return function(options)
            return awmtk.merge({
                (options.icon and {
                    {
                        {
                            image = options.icon,
                            id = options.icon_id,
                            resize = options.resize,
                            widget = wibox.widget.imagebox
                        },
                        strategy = "exact",
                        height = options.icon_size or
                            style.article.icon_size,
                        width = options.icon_size or
                            style.article.icon_size,
                        widget = wibox.container.constraint
                    },
                    widget = wibox.container.place,
                    valign = "center",
                    halign = "center"
                }),
                {
                    {
                        markup = options.title or "",
                        id = options.title_id,
                        widget = wibox.widget.textbox,
                        font = style.article.font,
                        valign = style.article.title_valign or "center",
                        align = style.article.title_align or "left"
                    },
                    (options.description and {
                        markup = options.description or "",
                        id = options.desc_id,
                        widget = wibox.widget.textbox,
                        font = style.article.small_font,
                        valign = style.article.desc_valign or "center",
                        align = style.article.desc_align or "left"
                    }),
                    spacing = style.article.spacing,
                    layout = wibox.layout.flex.vertical
                },
                spacing = style.article.spacing,
                layout = awmtk.wrap_hooks(
                    wibox.layout.fixed.horizontal,
                    options
                )
            }, options or {})
        end
    end,

    center = function(style)
        return function(layout,options)
            options = options or {}
            return awmtk.merge({
                {
                    layout,
                    strategy = "exact",
                    height = options.height or
                        style.center.height,
                    width = options.width or
                        style.center.width,
                    widget = wibox.container.constraint
                },
                widget = awmtk.wrap_hooks(
                    wibox.container.place,
                    options
                ),
                valign = "center",
                halign = "center"
            },options or {})
        end
    end,

    popup = function(style)
        -- popup is a distinct template designed to accomodate the "root" of 
        -- a popup, allowing one to add titlebars to popups, for example.
        return function(widget,options)
            return awmtk.merge({
                widget = {
                    widget,
                    margins = style.popup.margins,
                    layout = awmtk.wrap_hooks(wibox.container.margin,options)
                },
                bgimage = style.popup.bgimage_normal,
                shape = style.popup.shape,
                visible = false,
                ontop = true
            },options or {})
        end
    end,

    titlebar = function(style)
        -- titlebar is a separate class specifically for window and popup 
        -- titlebars. the decision to make it a separate class was due to 
        -- the fact that much customization is done through default theme table
        return function(layout,options)
            -- if there's one thing that fascinates me, it's how much weird
            -- bugs i manage to uncover by some sort of miraculous accident.
            -- this one fixes a race condition in margins+(left/right/bottom/top) configuration scenario
            local margins = style.titlebar.margins
            if (style.titlebar.left or
                style.titlebar.right or
                style.titlebar.bottom or
                style.titlebar.top) then
                margins = nil
            end
            return awmtk.merge({
                layout,
                margins = margins,
                layout = awmtk.wrap_hooks(wibox.container.margin,options),
                left = style.titlebar.left,
                right = style.titlebar.right,
                bottom = style.titlebar.bottom,
                top = style.titlebar.top
            },options or {})
        end
    end,

    wibar = function(style)
        -- just you regular old wibar, but as a style template.
        return function(layout,options)
            local margins = style.wibar.margins
            if (style.wibar.left or
                style.wibar.right or
                style.wibar.bottom or
                style.wibar.top) then
                margins = nil
            end
            return awmtk.merge({
                layout,
                margins = margins,
                layout = awmtk.wrap_hooks(wibox.container.margin,options),
                left = style.wibar.left,
                right = style.wibar.right,
                bottom = style.wibar.bottom,
                top = style.wibar.top
            },options or {})
        end
    end,

    slider = function(style,vertical)
        -- slider widget but wired to work with the awmtk2 namespace system
        return function(args)
            if not vertical then
                return awmtk.merge({
                    handle_shape = style.slider.handle_shape or style.slider.shape,
                    handle_color = style.slider.bg_normal,
                    handle_margins = style.slider.handle_margins,
                    handle_width = style.slider.handle_width,
                    handle_border_color = style.slider.handle_border_color,
                    handle_border_width = style.slider.handle_border_width,
                    bar_shape = style.slider.bar_shape or style.slider.shape,
                    bar_height = style.slider.bar_height,
                    bar_color = style.slider.bg_focus,
                    bar_margins = style.slider.bar_margins,
                    bar_border_width = style.slider.bar_border_width,
                    bar_border_color = style.slider.bar_border_color,
                    forced_width = style.slider.width,
                    forced_height = style.slider.height,
                    widget = awmtk.wrap_hooks(wibox.widget.slider,args)
                },args or {})
            else
                return {
                    awmtk.merge({
                        handle_shape = style.slider.handle_shape or style.slider.shape,
                        handle_color = style.slider.bg_normal,
                        handle_margins = style.slider.handle_margins,
                        handle_width = style.slider.handle_width,
                        handle_border_color = style.slider.handle_border_color,
                        handle_border_width = style.slider.handle_border_width,
                        bar_shape = style.slider.bar_shape or style.slider.shape,
                        bar_height = style.slider.bar_height,
                        bar_color = style.slider.bg_focus,
                        bar_margins = style.slider.bar_margins,
                        bar_border_width = style.slider.bar_border_width,
                        bar_border_color = style.slider.bar_border_color,
                        forced_width = style.slider.width,
                        forced_height = style.slider.height,
                        widget = awmtk.wrap_hooks(wibox.widget.slider,args)
                    },args or {}),
                    widget = wibox.container.rotate,
                    direction = "west"
                }
            end
        end
    end,

    checkbox = function(style)
        return function(args)
            return awmtk.merge({
                color = style.checkbox.color or style.checkbox.bg_normal,
                paddings = style.checkbox.paddings,
                shape = style.checkbox.shape,
                border_width = style.checkbox.border_width,
                border_color = style.checkbox.border_color or style.checkbox.bg_normal,
                bg = style.checkbox.bg or style.checkbox.bg_focus,
                check_color = style.checkbox.check_color or style.checkbox.bg_normal,
                check_shape = style.checkbox.check_shape,
                widget = awmtk.wrap_hooks(wibox.widget.checkbox,args)
            },args)
        end
    end
}

-- last but not least - we export a default template lib and default style.
-- this is done in order to allow overriding default style behaviour from theme
awmtk.default = awmtk.create_style("default",awmtk.proto_style,{})
awmtk.templates = awmtk.create_template_lib("templates",awmtk.proto_templates,{})

-- generic styles for widgets that need them
awmtk.generic = {}
awmtk.generic.menu = awmtk.create_style("generic_menu",awmtk.default,{})
awmtk.generic.button_list = awmtk.create_style("generic_button_list",awmtk.default,{})
awmtk.generic.iconified_widget = awmtk.create_style("generic_iconified_widget",awmtk.default,{})
awmtk.generic.status_widget = awmtk.create_style("generic_status_widget",awmtk.default,{})
awmtk.generic.oneline_widget = awmtk.create_style("generic_oneline_widget",awmtk.default,{})
awmtk.generic.composite_widget = awmtk.create_style("generic_composite_widget",awmtk.default,{})
awmtk.generic.popup = awmtk.create_style("generic_popup",awmtk.default,{})
-- }}}
return awmtk
