-- $Id: uiQuery-1.0.lua 29464 2007-03-03 18:57:49Z ckknight $
local MAJOR_VERSION = "uiQuery-1.0"
local TEMPLATE_MAJOR_VERSION = "uiTemplate-1.0"
local MINOR_VERSION = "$Revision: 29464 $"

if not AceLibrary then error(MAJOR_VERSION .. " requires AceLibrary.") end
if not AceLibrary:IsNewVersion(MAJOR_VERSION, MINOR_VERSION) then return end

local uiQuery, uiTemplate, basefuncs = {}, {}, {}
local events = {"OnAnimFinished", "OnChar", "OnClick", "OnCursorChanged", "OnDoubleClick", "OnDragStart", "OnDragStop", "OnEditFocusGained", "OnEditFocusLost", "OnEnter", "OnEnterPressed", "OnEscapePressed", "OnEvent", "OnHide", "OnHorizontalScroll", "OnHyperlinkClick", "OnHyperlinkEnter", "OnHyperlinkLeave", "OnInputLanguageChanged", "OnKeyDown", "OnKeyUp", "OnLeave", "OnMessageScrollChanged", "OnMouseDown", "OnMouseUp", "OnMouseWheel", "OnReceiveDrag", "OnScrollRangeChanged", "OnShow", "OnSizeChanged", "OnSpacePressed", "OnTabPressed", "OnTextChanged", "OnTextSet", "OnTooltipAddMoney", "OnTooltipCleared", "OnTooltipSetDefaultAnchor", "", "OnTooltipSetItem", "OnTooltipSetSpell", "OnTooltipSetUnit", "OnUpdate", "OnUpdateModel", "OnVerticalScroll", "OnValueChanged",}

-- Expressions
local function __expSplit(exp)
    if exp:match(">") or exp:match(",") then
        local exp, op, exp2 = exp:match("(%w+) *(.) *(.+)")
        return exp, op, __expSplit(exp2)
    else
        return exp
    end
end

local function __expMerge(tbl, ...)
    local n = #tbl
    for i = 1, select('#', ...) do
        tbl[n + i] = select(i, ...)    
    end
end

local function __expMergeTbls(tbl, tbl2)
    local n = #tbl
    for i = 1, #tbl2 do
        tbl[n + i] = tbl2[i]
    end
end
local function __expMatch(target, exp)
    local loose = exp:match("^~(%w+)$")
    if loose then
        return target:IsObjectType(loose)
    else
        return target:GetObjectType() == exp
    end
end

local function __expSelect(exp, scope)
    if exp == '*' then return scope end
    frames = {}
    if scope[1].GetObjectType then
        for k, obj in ipairs(scope) do
            if __expMatch(obj, exp) then table.insert(frames, obj) end
        end
    elseif scope[1][1] and scope[1][1].GetObjectType then
        for k, sscope in ipairs(scope) do
            for k, obj in ipairs(sscope) do
                if __expMatch(obj, exp) then table.insert(frames, obj) end
            end
        end
    else
        error("Invalid Scope", 2)
    end
    return frames
end

local function __expChildOf(exp, scope)
    local method, frames = "GetChildren"
    if exp == "FontString" or exp == "Texture" then method = "GetRegions" end
    if scope.GetChildren or scope.GetRegions then
        if exp == '*' then
            frames = {scope:GetChildren()} 
            __expMerge(frames, scope:GetRegions())
        else
            frames = __expSelect(exp, {scope[method](scope)})
        end
    elseif #scope == 0 then
        -- no bad monkey!
    else
        if exp == '*' then
            for k, sscope in ipairs(scope) do
                if not frames then frames = {} end
                if sscope.GetChildren then __expMerge(frames, sscope:GetChildren()) end
                if sscope.GetRegions then __expMerge(frames, sscope:GetRegions()) end
            end
        else
            frames = {}
            for k, sscope in ipairs(scope) do
                if sscope[method] then
                    for k, obj in ipairs({sscope[method](sscope)}) do
                        if __expMatch(obj, exp) then table.insert(frames, obj) end
                    end
                end
            end
        end
    end
    return frames
end

local function __expStackWorker(stack, scope)
    if #stack == 0 then 
        return scope
    elseif #stack == 1 then
        error("Invalid Stack",2)
    else
        if stack[1] == ">" and stack[3] == "," then
            table.insert(stack, 2, "*")
            table.insert(stack, 3, ",")
        end
                
        local op, exp = table.remove(stack, 1), table.remove(stack, 1)
      
        if op == ">" then
            return __expStackWorker(stack, __expChildOf(exp, scope))
        elseif op == "," then
            local frames = {}
            while true do
                __expMergeTbls(frames, __expSelect(exp, scope))
                if #stack == 0 or stack[1] ~= "," then break end
                op, exp = table.remove(stack, 1), table.remove(stack, 1)
            end
            return __expStackWorker(stack, frames)
        end
    end
end

local function __exp(exp, scope, op)
    local stack = {__expSplit(exp)}
    table.insert(stack, 1, op)
    return __expStackWorker(stack, scope)
end


-- Internal Helper Functions
local function __handler(self, ...)
    if select('#', ...) > 0 and select(1, ...) then
        local arg1 = ...
        return type(arg1) == "table" and type(arg1[0]) == "userdata" and uiQuery:__createQuery({...}, self) or ...
    end
    return self 
end
local function __sanctifyFrame(frame)
    if frame.isUiQuery then 
        if #frame.__frames > 1 then error("Ambigous argument passed in.  Query must reference only one object.", 3) end
        return frame.__frames[1] 
    else
        return frame
    end
end

-- Sudo Script Handlers
local function __OnDragStart() this:StartMoving() end
local function __OnDragStop() this:StopMovingOrSizing() end
function basefuncs:OnDrag(startfunc, stopfunc, ...) self:EnableMouse(true):SetMovable(true):RegisterForDrag(select('#', ...) > 0 and ... or "RightButton"):OnDragStart(startfunc or __OnDragStart):OnDragStop(stopfunc or __OnDragStop) return self end
function basefuncs:OnHover(enterfunc, leavefunc) self:OnEnter(enterfunc):OnLeave(leavefunc) return self end 
-- Script Handlers
for k, v in ipairs(events) do
    local script = v
    basefuncs[script] = function (self, func) 
        if func ~= nil then
            for k, frame in ipairs(self.__frames) do
                if frame:HasScript(script) then frame:SetScript(script, func or nil) end
            end
        elseif #self.__frames == 1 and self.__frames[1]:HasScript(script) then
            return self.__frames[1]:GetScript(script)
        end
        return self
    end
end

-- Helper Functions
function basefuncs:Center(offX, offY, relTo)
    for k, frame in ipairs(self.__frames) do
        if frame.SetPoint then frame:SetPoint("CENTER", relTo and __sanctifyFrame(relTo) or frame:GetParent(), "CENTER", offX or 0, offY or 0) end
    end
    return self
end

function basefuncs:Size(h, w, s)
    for k, frame in ipairs(self.__frames) do
        if not frame:IsObjectType("Region") then return end
        if not h and not w and not s and #self.__frames == 1 then
            return frame:GetHeight(), frame:GetWidth(), frame.GetScale and frame:GetScale() or nil
        else
            if h then frame:SetHeight(h) end
            if w then frame:SetWidth(w) end
            if s and frame.SetScale then frame:SetScale(s) end
        end
    end
    return self
end

-- Transversing API
function basefuncs:Filter(expression)
    if type(expression) == "function" then 
        local frames = {}
        for k, obj in ipairs(self.__frames) do
            if expression(obj) then table.insert(frames, obj) end
        end
        return uiQuery:__createQuery(frames, self)
    end
    return expression and uiQuery:__createQuery(__exp(expression, self.__frames, ","), self) or self
end

function basefuncs:Children(expression)
    if not expression then expression = '*' end
    return uiQuery:__createQuery( __exp(expression, self.__frames, ">"), self)
end

function basefuncs:End()
    return rawget(self, '__parentQuery') and self.__parentQuery or self
end

function basefuncs:Get(num)
    if not num then return unpack(self.__frames) end
    if self.__frames[num] then return self.__frames[num] end
end

function basefuncs:CreateChildren(type, name, template, number)
    number = number or 1
    local result, tmp
    for k, frame in ipairs(self.__frames) do
        local basename = name and frame:GetName() and frame:GetName()..name or name
        for i=1,number do
            tmp = self:Create(type, basename and basename..i , frame, template)
            result = result and result + tmp or tmp
        end
    end
    return result
end

-- Core API
function basefuncs:Each(func, ...)
    for k, frame in ipairs(self.__frames) do
        func(frame, ...)
    end
    return self
end

function basefuncs:Create(type, name, parent, template)
	-- cncfanatics: quick and dirty fix of the :Create() method with uiquery objects, fix kaelten!
	if(parent and parent.IsUiQuery) then
		parent = parent.__frames[1];
	end
    if template and template:match("uiTemplate:") then
        local query = uiQuery:__createQuery({CreateFrame(type, name, parent)}, self.isUiQuery and self)
        query:Each(uiTemplate:GetTemplate(template))
        return query
    else
        return uiQuery:__createQuery({CreateFrame(type, name, parent, template)}, self.isUiQuery and self)
    end
end

-- Query Metatable functions (nonsense I tell you, nonsense the lot of it)
local function __add(op1, op2)
    local query = uiQuery:__createQuery(op1.__frames)
    __expMergeTbls(query.__frames, op2.__frames)
    query.__parentQuery = op1.__parentQuery or op2.__parentQuery
    return query
end

local function __index(self, key)
    local key = key
    
    self[key] = basefuncs[key] or function(self, arg1, arg2, arg3, ...)
        local template
        if type(arg1) == "table" then arg1 = __sanctifyFrame(arg1) end
        if type(arg2) == "table" then arg2 = __sanctifyFrame(arg2) end
        if type(arg3) == "table" then 
            arg3 = __sanctifyFrame(arg3) 
        elseif type(arg3) == "string" and arg3:match("uiTemplate:") then 
            template = arg3 
            arg3 = nill 
        end
        if #self.__frames > 1 then
            local result, tmp
            for k, frame in ipairs(self.__frames) do
                if frame[key] then 
                    if not arg1 then
                        tmp = __handler(self, frame[key](frame))
                    elseif not arg2 then
                        tmp = __handler(self, frame[key](frame, arg1))
                    elseif not arg3 then
                        tmp = __handler(self, frame[key](frame, arg1, arg2))
                    else
                        tmp = __handler(self, frame[key](frame, arg1, arg2, arg3, ...))
                    end
                    if type(tmp) == "table" and tmp.isUiQuery and tmp ~= self then
                        if result then result = result + tmp else result = tmp end
                    end
                end
            end
            if template then (result or self):Each(uiTemplate:GetTemplate(template)) end
            return result or self
        else
            if self.__frames[1][key] then
                if (key == "CreateFontString" or key =="CreateTexture") and type(template) == "string" then
                    local query = uiQuery:__createQuery(self.__frames[1][key](self.__frames[1], arg1, arg2)):Each(uiTemplate:GetTemplate(template))
                else
                    if not arg1 then
                        return __handler(self, self.__frames[1][key](self.__frames[1]))
                    elseif not arg2 then
                        return __handler(self, self.__frames[1][key](self.__frames[1], arg1))
                    elseif not arg3 then
                        return __handler(self, self.__frames[1][key](self.__frames[1], arg1, arg2))
                    else
                        return __handler(self, self.__frames[1][key](self.__frames[1], arg1, arg2, arg3, ...))
                    end
                end
            end
        end
        return self
    end

    return self[key]
end

-- Constructors
function uiQuery:__call(...)
    local exp, scope = ...
    
    if not exp then
        return self
    elseif type(exp) == "table" then
        return self:__createQuery({...})
    elseif type(exp) == "string" and type(scope) == "table" and scope:IsObjectType("Region") then
        return self:__createQuery(__exp(exp, scope, ">"))
    else
        error("Invalid Constructor Call", 2)
    end
end

function uiQuery:__index(key)
    return basefuncs[key] or self.__blackhole 
end

function uiQuery:__createQuery(frames, parent)
    if not frames or #frames == 0 then return self end
    local query = { __frames = frames, __index = __index, __add = __add, isUiQuery = true, __parentQuery = parent}
    setmetatable(query, query)
    return query
end

function uiQuery:__blackhole() end

-- Stubb stuff
local function activate(self, oldLib, oldDeactivate)
    uiQuery = self
    if oldLib then self.registry = oldLib.registry else self.registry = {} end    
    if oldDeactivate then oldDeactivate(oldLib) end
    setmetatable(uiQuery, uiQuery)
end

AceLibrary:Register(uiQuery, MAJOR_VERSION, MINOR_VERSION, activate)
uiQuery = AceLibrary(MAJOR_VERSION)

if not AceLibrary:IsNewVersion(TEMPLATE_MAJOR_VERSION, MINOR_VERSION) then return end
-- Register and get template functions
function uiTemplate:RegisterTemplate(name, func)
    if type(name) ~= "string" or type(func) ~= "function" then error("Invalid Arguments, must be a string and a function", 2) end
    if self.registry[name] then error(("Template %s Already Exists."):format(name), 2) end
    self.registry[name] = func
end

function uiTemplate:GetTemplate(name)
    if type(name) ~= "string" then error("Invalid Argument, must be a string", 2) end
    local name = name:match("uiTemplate:([a-zA-z:]+)") or name
    return self.registry[name]
end

-- Stubb stuff
local function activate(self, oldLib, oldDeactivate)
    uiTemplate = self
    if oldLib then self.registry = oldLib.registry else self.registry = {} end    
    if oldDeactivate then oldDeactivate(oldLib) end
end

AceLibrary:Register(uiTemplate, TEMPLATE_MAJOR_VERSION, MINOR_VERSION, activate)
uiTemplate = AceLibrary(TEMPLATE_MAJOR_VERSION)
