----------------------------------------------------------------
-- Carpenter
----------------------------------------------------------------
-- 
-- Author:  Sebastian Lindfors (Munk / MunkDev)
-- Website: https://github.com/seblindfors
-- Licence: GPL version 2 (General Public License)
--
-- Description:
--  Carpenter is a dynamic markup language processor, that provides a simple syntax to create frames.
--  Lua and XML are bridged by allowing dynamic inserts, such as local variables, and merging several
--  templates into one to create inheritance. WoW XML is too rigid to handle dynamic frame creation,
--  and implementing frames in pure Lua is messy, without clear hierarchy. This dynamic markup language
--  works well in any code editor, to expand/collapse items of interest during development.
--  Recursive "blueprints" automatically set name tags on items, and adds keys to the parent.
--  See example at the bottom.
--
-- Usage:
--  Carpenter(type, name, parent, inheritXML, blueprint) -> return Carpenter:CreateFrame(...)
--  Carpenter(frame, blueprint) -> return Carpenter:BuildFrame(...)
--  Carpenter:CreateFrame(type, name, parent, inheritXML, blueprint) -> creates a frame from scratch.
--  Carpenter:BuildFrame(frame, blueprint) -> builds blueprint on top of existing frame.
--  Carpenter:ExtendAPI(name, func, force) -> adds an API function that can be called from blueprints.

local Lib = LibStub:NewLibrary('Carpenter', 1)
if not Lib then return end
--------------------------------------------------------------------------
local   assert, pairs, ipairs, type, unpack, wipe, tconcat, strmatch = 
        assert, pairs, ipairs, type, unpack, wipe, table.concat, strmatch
--------------------------------------------------------------------------
local   Create, IsWidget = CreateFrame, C_Widget.IsWidget
--------------------------------------------------------------------------
local   anchor, onload, constructor, call, callMethodsOnWidget, -- build
        packtbl, getbuildinfo, getrelative, strip -- misc
--------------------------------------------------------------------------
local   SPECIAL, API, RESERVED
local   ANCHOR, ONLOAD = {}, {}


--------------------------------------------------------------------------
API = { -- Syntax: _CallID = value or {value1, ..., valueN};
--------------------------------------------------------------------------
--  [1]        = function(obj, bp)      Lib:BuildFrame(obj, bp, true) end;
    ID         = function(widget, ...)  widget:SetID(...)             end;
    --- Texture ----------------------------------------------------------
    Atlas      = function(texture, ...) texture:SetAtlas(...)         end;
    Blend      = function(texture, ...) texture:SetBlendMode(...)     end;
    Coords     = function(texture, ...) texture:SetTexCoord(...)      end;
    Gradient   = function(texture, ...) texture:SetGradientAlpha(...) end;
    Texture    = function(texture, ...) texture:SetTexture(...)       end;
    --- FontString -------------------------------------------------------
    AlignH     = function(fontstr, ...) fontstr:SetJustifyH(...)      end;
    AlignV     = function(fontstr, ...) fontstr:SetJustifyV(...)      end;
    Color      = function(fontstr, ...) fontstr:SetTextColor(...)     end;
    Font       = function(fontstr, ...) fontstr:SetFont(...)          end;
    FontH      = function(fontstr, ...) fontstr:SetHeight(...)        end;
    Text       = function(fontstr, ...) fontstr:SetText(...)          end;
    --- LayeredRegion ----------------------------------------------------
    Layer      = function(widget, ...)  widget:SetDrawLayer(...)      end;
    Vertex     = function(widget, ...)  widget:SetVertexColor(...)    end;
    --- Frame packages ---------------------------------------------------
    Attributes = function(frame, attr)  for k, v in pairs(attr)   do frame:SetAttribute(k, v) end end;
    Events     = function(frame, ...)   for _, v in ipairs({...}) do frame:RegisterEvent(v)   end end;
    Hooks      = function(frame, src)   for k, v in pairs(src)    do frame:HookScript(k, v)   end end;
    Scripts    = function(frame, src)   for k, v in pairs(src)    do frame:SetScript(k, v)    end end;
    --- Frame ------------------------------------------------------------
    Backdrop   = function(frame, info)  Lib:SetBackdrop(frame, info)  end;
    Level      = function(frame, ...)   frame:SetFrameLevel(...)      end;
    Strata     = function(frame, ...)   frame:SetFrameStrata(...)     end;
    --- Region -----------------------------------------------------------
    Alpha      = function(widget, ...)  widget:SetAlpha(...)          end;
    Clear      = function(widget)       widget:ClearAllPoints()       end;
    Fill       = function(widget, tar)  widget:SetAllPoints(tar ~= true and getrelative(widget, tar)) end;
    Height     = function(widget, ...)  widget:SetHeight(...)         end;
    Hide       = function(widget, ...)  widget:Hide()                 end;
    Show       = function(widget, ...)  widget:Show()                 end;
    Size       = function(widget, ...)  widget:SetSize(...)           end;
    Scale      = function(widget, ...)  widget:SetScale(...)          end;
    Width      = function(widget, ...)  widget:SetWidth(...)          end;
    --- Button -----------------------------------------------------------
    Click      = function(button, ...)  button:SetAttribute('type', 'click')  button:SetAttribute('clickbutton', ...) end;
    Macro      = function(button, ...)  button:SetAttribute('type', 'macro')  button:SetAttribute('macrotext', ...) end;
    Action     = function(button, ...)  button:SetAttribute('type', 'action') button:SetAttribute('action', ...) end;
    Spell      = function(button, ...)  button:SetAttribute('type', 'spell')  button:SetAttribute('spell', ...) end;
    Unit       = function(button, ...)  button:SetAttribute('type', 'target') button:SetAttribute('unit', ...) end;
    Item       = function(button, ...)  button:SetAttribute('type', 'item')   button:SetAttribute('item', ...) end;
    --- Constructor ------------------------------------------------------
    OnLoad     = function(widget, ...)  packtbl(ONLOAD, widget, ...)  end;
    --- Points -----------------------------------------------------------
    Point      = function(widget, ...)  packtbl(ANCHOR, widget, ...)  end;
    Points     = function(widget, ...)  for _, point in ipairs({...}) do packtbl(ANCHOR, widget, unpack(point)) end end;
    -- Custom handlers ---------------------------------------------------
    Mixin      = function(widget, ...)  Lib:Mixin(widget, ...)        end;
    MixinDry   = function(widget, ...)  Mixin(widget, ...)            end;
    --- Multiple runs of single function ---------------------------------
    ForEach    = function(widget, multiTable)
        for k, v in pairs(multiTable) do k = strip(k)
            for _, args in pairs(v) do
                call(widget, k, args)
            end
        end
    end;
};

--------------------------------------------------------------------------
SPECIAL = { -- Special constructors
--------------------------------------------------------------------------
    AnimationGroup = function(parent, key, setup, anon)
        local name = not anon and '$parent'..key or nil;
        if setup then
            return parent:CreateAnimationGroup(name, unpack(setup))
        end
        return parent:CreateAnimationGroup(name)
    end;
    Animation = function(parent, key, setup, anon)
        return parent:CreateAnimation(setup, not anon and '$parent'..key or nil)
    end;
    FontString = function(parent, key, setup, anon)
        local name = not anon and '$parent'..key or nil;
        if setup then
            return parent:CreateFontString(name, unpack(setup))
        end
        return parent:CreateFontString(name)
    end;
    Texture = function(parent, key, setup, anon)
        local name = not anon and '$parent'..key or nil;
        if setup then
            return parent:CreateTexture(name, unpack(setup))
        end
        return parent:CreateTexture(name)
    end;
    ---
    ScrollFrame = function(parent, key, setup, anon)
        local frame = Create('ScrollFrame', not anon and '$parent'..key or nil, parent, setup and not IsWidget(setup) and unpack(setup))
        local child = IsWidget(setup) and setup or Create('Frame', not anon and '$parentChild' or nil, frame)
        frame.Child = child
        child:SetParent(frame)
        child:SetAllPoints()
        frame:SetScrollChild(child)
        frame:SetToplevel(true)
        return frame
    end;
    ---
    Global = function(parent, key, region, anon)
        if not anon then
            _G[parent:GetName()..key] = region
        end
        parent[key] = region
        region:SetParent(parent)
        return region
    end;
};

--------------------------------------------------------------------------
RESERVED = { -- Reserved keywords in blueprints
--------------------------------------------------------------------------
    [1]        = true, -- don't run blueprints in function stack
    _Type      = true, -- used to determine frame type
    _Mixin     = true, -- specially handled before function calls
    _Setup     = true, -- used to determine inheritance
    _Repeat    = true, -- ignore since it denotes a loop
    _Anonymous = true, -- denotes anonymous parent/children
};

----------------------------------------------------------------
-- Create a new frame.
----------------------------------------------------------------
-- @param   object    : Type of object to be created.
-- @param   name      : Name of the object to be created.
-- @param   parent    : Parent of the object.
-- @param   xml       : Inherited xml.
-- @param   blueprint : Table consisting of additional regions to be created.
-- @return  frame     : Returns the created object.
----------------------------------------------------------------
function Lib:CreateFrame(object, name, parent, xml, blueprint, recursive, anonymous)
    ----------------------------------
    local frame = Create(object, name, parent, xml)
    ----------------------------------
    if blueprint then
        if recursive then
            self:BuildFrame(frame, blueprint, true, anonymous)
        else
            local children = blueprint[1]
            callMethodsOnWidget(frame, blueprint)
            self:BuildFrame(frame, children, true)
        end
    end
    ----------------------------------
    if not recursive then
        anchor()
        onload()
    end
    return frame
end

----------------------------------------------------------------
-- Build frame from blueprint.
----------------------------------------------------------------
-- @param   frame     : Parent of the blueprint.
-- @param   blueprint : Blueprint to be constructed.
-- @param   recursive : Whether this is a recursive call.
-- @param   anonframe : Whether tree is anonymous.
-- @return  frame     : Returns the altered frame.
----------------------------------------------------------------
function Lib:BuildFrame(frame, blueprint, recursive, anonframe)
    for key, config in pairs(blueprint) do
        ----------------------------------
        local object, objectType, buildInfo, isLoop, anonobj = getbuildinfo(config)
        ----------------------------------
        for i = 1, ( isLoop or 1 ) do
            local key    = isLoop and key..i or key
            local anon   = anonframe or anonobj
            local name   = not anon and '$parent'..key
            local widget = IsWidget(frame[key]) and frame[key]
            if not widget then
                ----------------------------------
                if object then
                    -- Region type has special constructor.
                    if SPECIAL[object] then
                        local bp  = config[1]
                        widget = SPECIAL[object](frame, key, buildInfo, anon)
                        if bp then
                            widget = self:BuildFrame(widget, bp, true, anon)
                        end
                    -- Region already exists.
                    elseif (objectType == 'table') and IsWidget(object) then
                        widget = SPECIAL.Global(frame, key, object, anon)
                    -- Region should be a type of frame.
                    elseif (objectType == 'string') then
                        local xml = type(buildInfo) == 'table' and tconcat(buildInfo, ', ') or buildInfo
                        local bp  = config[1]
                        widget = self:CreateFrame(object, name, frame, xml, bp, true, anon);
                    end
                else -- Assume this is a data table.
                    widget = config
                end
                ----------------------------------
                frame[key] = widget
            else
                local bp = config[1]
                if bp then
                    widget = self:BuildFrame(widget, bp, true, anon)
                end
            end

            if isLoop and widget.SetID then widget:SetID(i) end
            callMethodsOnWidget(widget, config)
        end
    end
    -- parse if explicitly called without wrapping (building on top of existing frame)
    if not recursive then
        anchor()
        onload()
    end
    return frame
end

----------------------------------------------------------------
-- Extend API
----------------------------------------------------------------
-- @param   name    : Name of the API function to add.
-- @param   func    : Function to add.
-- @param   force   : Forcefully replace existing function.
-- @return  success : Returns true when successful.
----------------------------------------------------------------
function Lib:ExtendAPI(name, func, force)
    assert(type(name) == 'string',   format("bad argument #1 to 'ExtendAPI' (string expected, got %s)", type(name)))
    assert(type(func) == 'function', format("bad argument #2 to 'ExtendAPI' (function expected, got %s)", type(func)))
    assert(not API[name] or force,   format('%s is already registered in the API.', tostring(name)))
    API[name] = func
    return true
end

function Lib:Mixin(obj, ...)
    local scriptObject = (type(obj.HasScript) == 'function')
    for i = 1, select('#', ...) do
        local mixin = select(i, ...)
        for k, v in pairs(mixin) do
            if scriptObject and obj:HasScript(k) then
                if obj:GetScript(k) then
                    obj:HookScript(k, v)
                else
                    obj:SetScript(k, v)
                end
            end
            obj[k] = v
        end
    end
    return obj
end

function Lib:SetBackdrop(frame, ...)
    if BackdropTemplateMixin then
        if not frame.OnBackdropLoaded then 
            Mixin(frame, BackdropTemplateMixin)
            frame:HookScript('OnSizeChanged', frame.OnBackdropSizeChanged)
        end
        BackdropTemplateMixin.SetBackdrop(frame, ...)
    else
        getmetatable(frame).__index.SetBackdrop(frame, ...)
    end
end

setmetatable(Lib, {
    __index = API;
    __call = function(self, arg1, ...)
        if IsWidget(arg1) then
            return self:BuildFrame(arg1, ...)
        end
        return self:CreateFrame(arg1, ...)
    end;
})

--------------------------------------------------------------------------
-- Internal circuit
--------------------------------------------------------------------------
function call(widget, method, data)
    if data == 'nil' then data = nil end
    local func = API[method] or widget[method]
    if type(func) == 'function' then
        -- if sequential array, just unpack it.
        if ( type(data) == 'table' and #data ~= 0 ) then
            return func(widget, unpack(data))
        else
            return func(widget, data)
        end
    elseif type(data) == 'function' and widget.HasScript and widget:HasScript(method) then
        if widget:GetScript(method) then
            widget:HookScript(method, data)
        else
            widget:SetScript(method, data)
        end 
    else
        widget[method] = data
    end
end

function callMethodsOnWidget(widget, methods)
    -- mixin before running the rest of the widget method stack,
    -- since mixed in functions may be called from the blueprint
    local mixin = methods._Mixin
    if mixin then
        call(widget, 'Mixin', mixin)
        -- if the mixin has an onload script, add it to the constructor stack.
        -- remove the onload function from the object itself.
        if widget.OnLoad and not methods._OnLoad then
            -- use :GetScript in case more than one load script was hooked.
            methods._OnLoad = widget:GetScript('OnLoad')
            widget:SetScript('OnLoad', nil)
            widget.OnLoad = nil
        end
    end

    for method, data in pairs(methods) do
        if not RESERVED[method] then
            local func = strip(method)
            if func then
                call(widget, func, data)
            else
                widget[method] = data
            end
        end
    end
end

function getrelative(region, query)
    if IsWidget(query) then 
        return query
    elseif type(query) == 'string' then 
        local relative
        for key in query:gmatch('%w+') do
            if ( key == 'parent' ) then
                relative = relative and relative:GetParent() or region:GetParent()
            elseif relative then
                relative = relative[key]
            else
                relative = region[key]
            end
        end
        return relative
    end
end

function anchor()
    for _, setup in ipairs(ANCHOR) do
        local numArgs = #setup
        if numArgs == 2 then
            local region, point = unpack(setup)
            region:SetPoint(point)
        elseif numArgs == 4 then
            local region, point, x, y = unpack(setup)
            region:SetPoint(point, x, y)
        elseif numArgs == 6 then
            local region, point, relativeRegion, relativePoint, x, y = unpack(setup)
            region:SetPoint(point, getrelative(region, relativeRegion), relativePoint, x, y)
        elseif numArgs == 8 then
            local region, point, relativeRegion, relativePoint, x, y, xIncr, yIncr = unpack(setup)
            region:SetPoint(point, getrelative(region, relativeRegion), relativePoint,
                x + (xIncr * ((region:GetID() or 1) - 1)),
                y + (yIncr * ((region:GetID() or 1) - 1))
            );
        end
    end
    wipe(ANCHOR)
end

function constructor(region, load, ...)
    if not load then return end
    load(region)
    return constructor(region, ...)
end

function onload()
    for _, setup in ipairs(ONLOAD) do
        local region, load = unpack(setup)
        if ( type(load) == 'table' ) then
            constructor(region, unpack(load))
        else
            constructor(region, load)
        end
    end
    wipe(ONLOAD)
end

function getbuildinfo(bp) return 
    bp._Type,
    type(bp._Type),
    bp._Setup,
    bp._Repeat,
    bp._Anonymous
end

function packtbl(tbl, ...) tbl[#tbl + 1] = {...} end;
function strip(key) return strmatch(key, '_(%w+)') end;

--------------------------------------------------------------------------
-- Example:
--------------------------------------------------------------------------
-- This implements an action bar with simple page swapping:
--[[

local bar = Carpenter:CreateFrame('Frame', 'ActionBarExample', UIParent, 'SecureHandlerStateTemplate', {
    _Size   = {NUM_ACTIONBAR_BUTTONS * 40, 36};
    _Point  = {'CENTER', 0, 0};
    _OnLoad = function(self)
        local macro = '1';
        for i=1, NUM_ACTIONBAR_PAGES do
            macro = format('[bar:%d] %d;', i, i) .. macro; 
        end
        for i=1, 4 do
            macro = format('[bonusbar:%d] %d;', i, i + NUM_ACTIONBAR_PAGES) .. macro;
        end
        RegisterStateDriver(self, 'actionpage', macro);
    end;
    _Attributes = {
        ['type'] = 'actionbar';
        ['actionpage'] = 1;
        ['_onstate-actionpage'] = [=[
            self:SetAttribute('actionpage', newstate);
            control:ChildUpdate('actionpage', newstate);
        ]=];
    };

    {   -- A keyless table contains the children
        ActionButton = {
            _Repeat = NUM_ACTIONBAR_BUTTONS;
            _Type   = 'Button';
            _Point  = {'LEFT', '$parent', 'LEFT', 0, 0, 40, 0};
            _Setup  = {'ActionButtonTemplate', 'SecureActionButtonTemplate'};
            _Attributes = {
                ['type'] = 'action';
                ['_childupdate-actionpage'] = 'self:SetAttribute("actionpage", newstate)';
            };
            -- adding a custom method
            _UpdateTexture = function(self)
                local page = self:GetParent():GetAttribute('actionpage');
                local actionID = (page - 1) * NUM_ACTIONBAR_BUTTONS + self:GetID();
                self.icon:SetTexture(GetActionTexture(actionID));
            end;
            -- calling custom method on load
            _OnLoad = function(self)
                self:UpdateTexture();
            end;
            -- setting a script handler
            _OnAttributeChanged = function(self)
                self:UpdateTexture();
            end;
        }
    }
})

]]