-- RestrictedFrames.lua (Part of the new Secure Headers implementation)
--
-- Handle objects to access frames during restricted execution in order to
-- allow safe manipulation of frame properties. These handles can be passed
-- around safely without allowing their destinations or functions to be
-- tampered with.
--
-- Daniel Stephens (iriel@vigilance-committee.org)
-- Nevin Flanagan (alestane@comcast.net)
--
-- The design consists of several components...
--
-- HANDLES: These are lightweight userdata objects with a shared metatable
--          that stand in for frames. They're persistent once created (i.e.
--          a given frame always has the same handle). Internally these map
--          to a 'copied' frame object. Only explcitly protected frames can
--          be assigned handles.
--
-- METHODS: The handler methods are contained in a table which is then set
--          as the __index on the handlers. These are in two groups, there
--          are 'read' methods that may be blocked in some situations, and
--          there are 'write' methods which simply require that an execution
--          is active.
--
-- Various methods (SetPoint/SetParent) take frame handles as relative
-- frame arguments also. You can safely obtain frame handles (out of combat)
-- by setting the '_frame-<id>' attribute on a header frame to the frame
-- you want a handle to, and it'll set the 'frameref-<id>' attribute to be
-- the handle to that frame (or nil if it's invalid or unprotected).
-- This handle can then be retrieved using a GetAttribute call.
---------------------------------------------------------------------------

local issecure = issecure;
local select = select;
local type = type;
local unpack = unpack;
local wipe = wipe;
local pairs = pairs;
local newproxy = newproxy;
local error = error;
local tostring = tostring;
local tonumber = tonumber;
local string = string;
local rawget = rawget;
local securecall = securecall;
local GetCursorPosition = GetCursorPosition;
local InCombatLockdown = InCombatLockdown;

---------------------------------------------------------------------------
-- Frame Handles -- Userdata handles referencing explicitly protected frames
--
-- LOCAL_FrameHandle_Protected_Frames -- handle keys, frame surrogate values
--                                       (explicitly protected)
-- LOCAL_FrameHandle_Other_Frames -- handle keys, frame surrogate values
--                                   (possibly protected)
-- LOCAL_FrameHandle_Lookup -- frame keys, handle values
--
-- The lookup table auto-populates via an __index metamethod

local LOCAL_FrameHandle_Protected_Frames = {};
local LOCAL_FrameHandle_Other_Frames = {};
local LOCAL_FrameHandle_Lookup = {};
setmetatable(LOCAL_FrameHandle_Protected_Frames, { __mode = "k" });
setmetatable(LOCAL_FrameHandle_Other_Frames, { __mode = "k" });

-- Setup metatable for prototype object
local LOCAL_FrameHandle_Prototype = newproxy(true);
-- HANDLE is the frame handle method namespace (populated below)
local HANDLE = {};
do
    local meta = getmetatable(LOCAL_FrameHandle_Prototype);
    meta.__index = HANDLE;
    meta.__metatable = false;
end

function IsFrameHandle(handle, protected)
    local handle = LOCAL_FrameHandle_Protected_Frames[handle];
    if (handle == nil) and not protected then
        handle = LOCAL_FrameHandle_Other_Frames[handle];
    end
    return (handle ~= nil);
end

function GetFrameHandleFrame(handle, protected)
    local handle = LOCAL_FrameHandle_Protected_Frames[handle];
    if handle ~= nil then
        return handle[1];
    end
    if not protected or not InCombatLockdown() then
        handle = LOCAL_FrameHandle_Other_Frames[handle];
        if handle ~= nil then
            return handle[1];
        end
    end
end

local function FrameHandleLookup_index(t, frame)
    -- Create a 'surrogate' frame object
    local surrogate = { [0] = frame[0], [1] = frame };
    setmetatable(surrogate, getmetatable(frame));
    -- Test whether the frame is explcitly protected
    local _, protected = surrogate:IsProtected();
    if not issecure() then
        return;
    end
    local handle = newproxy(LOCAL_FrameHandle_Prototype);
    LOCAL_FrameHandle_Lookup[frame] = handle;
    if protected then
        LOCAL_FrameHandle_Protected_Frames[handle] = surrogate;
    else
        LOCAL_FrameHandle_Other_Frames[handle] = surrogate;
    end
    return handle;
end
setmetatable(LOCAL_FrameHandle_Lookup, { __index = FrameHandleLookup_index; });

-- Gets the handle for a frame (if available)
function GetFrameHandle(frame, protected)
    local handle = LOCAL_FrameHandle_Lookup[frame];
    if protected and (frame ~= nil) then
        if not LOCAL_FrameHandle_Protected_Frames[handle] then
            return nil;
        end
    end
    return handle;
end

---------------------------------------------------------------------------
-- Action implementation support function
--
-- GetHandleFrame -- Get the frame for a handle

local function GetUnprotectedHandleFrame(handle)
    local frame = LOCAL_FrameHandle_Protected_Frames[handle];
    if not frame then
        frame = LOCAL_FrameHandle_Other_Frames[handle];
        if not frame then
            error("Invalid frame handle");
            return;
        end
    end
    return frame;
end

local function GetHandleFrame(handle)
    local frame = LOCAL_FrameHandle_Protected_Frames[handle];
    if not frame then
        frame = LOCAL_FrameHandle_Other_Frames[handle];
        if frame then
            if frame:IsProtected() or not InCombatLockdown() then
                return frame;
            end
        end
        error("Invalid frame handle");
        return;
    end
    return frame;
end

---------------------------------------------------------------------------
-- "GETTER" methods

function HANDLE:GetName()   return GetUnprotectedHandleFrame(self):GetName() end

function HANDLE:GetID()     return GetHandleFrame(self):GetID()     end
function HANDLE:IsShown()   return GetHandleFrame(self):IsShown()   end
function HANDLE:IsVisible() return GetHandleFrame(self):IsVisible() end
function HANDLE:GetWidth()  return GetHandleFrame(self):GetWidth()  end
function HANDLE:GetHeight() return GetHandleFrame(self):GetHeight() end
function HANDLE:GetRect()   return GetHandleFrame(self):GetRect() end
function HANDLE:GetScale()  return GetHandleFrame(self):GetScale()  end
function HANDLE:GetEffectiveScale()
    return GetHandleFrame(self):GetEffectiveScale()
end
-- Cannot expose GetAlpha since alpha is not protected
function HANDLE:GetFrameLevel()
    return GetHandleFrame(self):GetFrameLevel()  end
function HANDLE:GetObjectType()
    return GetUnprotectedHandleFrame(self):GetObjectType()
end
function HANDLE:IsObjectType(ot)
    return GetUnprotectedHandleFrame(self):IsObjectType(tostring(ot))
end
function HANDLE:IsProtected()
    return GetUnprotectedHandleFrame(self):IsProtected();
end


function HANDLE:GetAttribute(name)
    if type(name) ~= "string" or name:match("^_") then
        return;
    end
    local val = GetHandleFrame(self):GetAttribute(name)
    local tv = type(val);
    if tv == "string" or tv == "number" or tv == "boolean" or val == nil then
        return val;
    end
    if (tv == "userdata" and
        (LOCAL_FrameHandle_Protected_Frames[val]
         or LOCAL_FrameHandle_Other_Frames[val])) then
        return val;
    end
    return nil;
end

function HANDLE:GetFrameRef(label)
    if type(label) ~= "string" then
        return;
    end
    local val = GetHandleFrame(self):GetAttribute("frameref-" .. label);
    local tv = type(val);
    if (tv == "userdata" and
        (LOCAL_FrameHandle_Protected_Frames[val]
         or LOCAL_FrameHandle_Other_Frames[val])) then
        return val;
    end
    return nil;
end

function HANDLE:GetEffectiveAttribute(name, button, prefix, suffix)
    if type(name) ~= "string" or name:match("^_") then
        return;
    end
    if button ~= nil then button = tostring(button) end
    if prefix ~= nil then
        prefix = tostring(prefix)
        if prefix:match("^_") then
            prefix = nil;
        end
    end
    if suffix ~= nil then suffix = tostring(suffix) end
    local val = SecureButton_GetModifiedAttribute(GetHandleFrame(self),
                                                  name, button, prefix,
                                                  suffix);
    local tv = type(val);
    if tv == "string" or tv == "number" or tv == "boolean" or val == nil then
        return val;
    end
    if (tv == "userdata" and
        (LOCAL_FrameHandle_Protected_Frames[val]
         or LOCAL_FrameHandle_Other_Frames[val])) then
        return val;
    end
    return nil;
end


local function FrameHandleMapper(nolockdown, frame, nextFrame, ...)
    if not frame then
        return;
    end
    -- Do an explicit protection check to avoid errors from
    -- the frame handle lookup
    local p = nolockdown;
    if not p then
        p = frame:IsProtected();
    end
    if p then
        frame = LOCAL_FrameHandle_Lookup[frame];
        if frame then
            if nextFrame then
                return frame, FrameHandleMapper(nolockdown, nextFrame, ...);
            else
                return frame;
            end
        end
    end
    if nextFrame then
        return FrameHandleMapper(nolockdown, nextFrame, ...);
    end
end

local function FrameHandleInserter(result, ...)
    local nolockdown = not InCombatLockdown();
    local idx = #result;
    for i = 1, select('#', ...) do
        local frame = select(i, ...);
        -- Do an explicit protection check to avoid errors from
        -- the frame handle lookup
        local p = nolockdown;
        if not p then
            p = frame:IsProtected();
        end
        if p then
            frame = LOCAL_FrameHandle_Lookup[frame];
            if frame then
                idx = idx + 1;
                result[idx] = frame;
            end
        end
    end

    return result;
end

function HANDLE:GetChildren()
    return FrameHandleMapper(not InCombatLockdown(),
                             GetHandleFrame(self):GetChildren());
end

function HANDLE:GetChildList(tbl)
    return FrameHandleInserter(tbl, GetHandleFrame(self):GetChildren());
end

function HANDLE:GetParent()
    return FrameHandleMapper(not InCombatLockdown(),
                             GetHandleFrame(self):GetParent());
end

-- NOTE: Cannot allow the frame to figure out if it has mouse focus
-- because an insecure frame could be appearing on top of it.

function HANDLE:GetMousePosition()
    local frame = GetHandleFrame(self);
    local x, y = GetCursorPosition()
    local l, b, w, h = frame:GetRect()
    if not w or not h or w == 0 or h == 0 then return nil; end
    local e = frame:GetEffectiveScale();
    x, y = x / e, y /e;
    x = x - l
    y = y - b
    if x < 0 or x > w or y < 0 or y > h then
        return nil
    else
        return x / w, y / h
    end
end

-- Used only for recursive check, since it's more expensive
--
-- Only check protected and visible children
local function RF_CheckUnderMouse(x, y, ...)
    for i = 1, select('#', ...) do
        local frame = select(i, ...);
        if frame and frame:IsProtected() and frame:IsVisible() then
            local l, b, w, h = frame:GetRect()
            if w and h then
                local e = frame:GetEffectiveScale();
                local fx = x / e - l;
                if (fx >= 0) and (fx <= w) then
                    local fy = y / e - b;
                    if (fy >= 0) and (fy <= h) then return true; end
                end
            end
            if RF_CheckUnderMouse(x, y, frame:GetChildren()) then
                return true;
            end
        end
    end
end

function HANDLE:IsUnderMouse(recursive)
    local frame = GetHandleFrame(self);
    local x, y = GetCursorPosition();
    local l, b, w, h = frame:GetRect()
    if w and h then
        local e = frame:GetEffectiveScale();
        local fx = x / e - l;
        if (fx >= 0) and (fx <= w) then
            local fy = y / e - b;
            if (fy >= 0) and (fy <= h) then return true; end
        end
    end
    if not recursive then
        return;
    end
    return RF_CheckUnderMouse(x, y, frame:GetChildren());
end

function HANDLE:GetNumPoints()
    return GetHandleFrame(self):GetNumPoints();
end

function HANDLE:GetPoint(i)
    local point, frame, relative, dx, dy = GetHandleFrame(self):GetPoint(i);
    local handle;
    if frame then
        handle = FrameHandleMapper(not InCombatLockdown(), frame);
    end
    if handle or not frame then
        return point, handle, relative, dx, dy;
    end
end

---------------------------------------------------------------------------
-- "SETTER" methods and actions

function HANDLE:Show(skipAttr)
    local frame = GetHandleFrame(self);
    frame:Show();
    if not skipAttr then
        frame:SetAttribute("statehidden", nil);
    end
end

function HANDLE:Hide(skipAttr)
    local frame = GetHandleFrame(self);
    frame:Hide();
    if not skipAttr then
        frame:SetAttribute("statehidden", true);
    end
end

function HANDLE:SetID(id)
    GetHandleFrame(self):SetID(tonumber(id) or 0);
end

function HANDLE:SetWidth(width)
    GetHandleFrame(self):SetWidth(tonumber(width));
end

function HANDLE:SetHeight(height)
    GetHandleFrame(self):SetHeight(tonumber(height));
end

function HANDLE:SetScale(scale)
    GetHandleFrame(self):SetScale(tonumber(scale));
end

function HANDLE:SetAlpha(alpha)
    GetHandleFrame(self):SetAlpha(tonumber(alpha));
end

local _set_points = {
    TOP=true; BOTTOM=true; LEFT=true; RIGHT=true; CENTER=true;
    TOPLEFT=true; BOTTOMLEFT=true; TOPRIGHT=true; BOTTOMRIGHT=true;
};

function HANDLE:ClearAllPoints()
    GetHandleFrame(self):ClearAllPoints();
end

function HANDLE:SetPoint(point, relframe, relpoint, xofs, yofs)
    if type(relpoint) == "number" then
        relpoint, xofs, yofs = nil, relpoint, xofs;
    end
    if relpoint == nil then
        relpoint = point;
    end
    if (xofs == nil) and (yofs == nil) then
        xofs, yofs = 0, 0;
    else
        xofs, yofs = tonumber(xofs), tonumber(yofs);
    end
    if not _set_points[point] then
        error("Invalid point '" .. tostring(point) .. "'");
        return;
    end
    if not _set_points[relpoint] then
        error("Invalid relative point '" .. tostring(relpoint) .. "'");
        return;
    end
    if not (xofs and yofs) then
        error("Invalid offset");
        return
    end

    local frame = GetHandleFrame(self);

    local realrelframe = nil;
    if type(relframe) == "userdata" then
        realrelframe = LOCAL_FrameHandle_Protected_Frames[relframe];
        if not realrelframe then
            error("Invalid relative frame handle");
            return;
        end
    elseif (relframe == nil) or (relframe == "$screen") then
        realrelframe = nil;
    elseif relframe == "$cursor" then
        local cx, cy = GetCursorPosition();
        local eff = frame:GetEffectiveScale();
        xofs = xofs + (cx / eff);
        yofs = yofs + (cy / eff);
        relpoint = "BOTTOMLEFT";
        realrelframe = nil;
    elseif relframe == "$parent" then
        realrelframe = frame:GetParent();
    else
        error("Invalid relative frame id '" .. tostring(relframe) .. "'");
        return;
    end

    frame:SetPoint(point, realrelframe, relpoint, xofs, yofs);
end

function HANDLE:SetAllPoints(relframe)
    local frame = GetHandleFrame(self);

    local realrelframe = nil;
    if type(relframe) == "userdata" then
        realrelframe = LOCAL_FrameHandle_Protected_Frames[relframe];
        if not realrelframe then
            error("Invalid relative frame handle");
            return;
        end
    elseif (relframe == nil) or (relframe == "$screen") then
        realrelframe = nil;
    elseif relframe == "$parent" then
        realrelframe = frame:GetParent();
    else
        error("Invalid relative frame id '" .. tostring(relframe) .. "'");
        return;
    end

    frame:SetAllPoints(realrelframe);
end

function HANDLE:SetAttribute(name, value)
    if type(name) ~= "string" or name:match("^_") then
        error("Invalid attribute name");
        return;
    end
    local tv = type(value);
    if (tv ~= "string" and tv ~= "nil" and tv ~= "number"
        and tv ~= "boolean") then
        if (not (tv == "userdata" and
                 (LOCAL_FrameHandle_Protected_Frames[value]
                  or LOCAL_FrameHandle_Other_Frames[value]))) then
            error("Invalid attribute value");
            return;
        end
    end
    GetHandleFrame(self):SetAttribute(name, value);
end

function HANDLE:ClearBindings()
    ClearOverrideBindings(GetHandleFrame(self));
end

function HANDLE:ClearBinding(key)
    SetOverrideBinding(GetHandleFrame(self), true, key, nil);
end

function HANDLE:SetBindingClick(priority, key, name, button)
    local tn = type(name);
    if tn == "userdata" then
        if (LOCAL_FrameHandle_Protected_Frames[name]
            or LOCAL_FrameHandle_Other_Frames[name]) then
            name = name:GetName();
            tn = type(name);
        end
    end
    if tn ~= "string" or name:match(":") then
        error("Invalid click target name");
        return;
    end
    if (button ~= nil) and type(button) ~= "string" then
        error("Invalid button name");
        return;
    end
    SetOverrideBindingClick(GetHandleFrame(self), priority, key, name, button);
end

function HANDLE:SetBinding(priority, key, action)
    if action ~= nil and type(action) ~= "string" then
        error("Invalid binding action");
        return;
    end
    SetOverrideBinding(GetHandleFrame(self), priority, key, action);
end

function HANDLE:SetBindingSpell(priority, key, spell)
    if type(spell) ~= "string" then
        error("Invalid binding spell");
        return;
    end
    SetOverrideBindingSpell(GetHandleFrame(self), priority, key, spell);
end

function HANDLE:SetBindingMacro(priority, key, macro)
    if type(macro) == "number" then
        macro = tostring(macro);
    elseif type(macro) ~= "string" then
        error("Invalid binding macro");
        return;
    end
    SetOverrideBindingMacro(GetHandleFrame(self), priority, key, macro);
end

function HANDLE:SetBindingItem(priority, key, item)
    if type(item) ~= "string" then
        error("Invalid binding item");
        return;
    end
    SetOverrideBindingItem(GetHandleFrame(self), priority, key, item);
end

function HANDLE:Raise()
    GetHandleFrame(self):Raise();
end

function HANDLE:Lower()
    GetHandleFrame(self):Lower();
end

function HANDLE:SetFrameLevel(level)
    GetHandleFrame(self):SetFrameLevel(tonumber(level));
end

function HANDLE:SetParent(handle)
    local parent = nil;
    if handle ~= nil then
        if type(handle) ~= "userdata" then
            error("Invalid frame handle for SetParent");
            return;
        end
        parent = LOCAL_FrameHandle_Protected_Frames[handle];
        if not parent then
            error("Invalid frame handle for SetParent");
            return;
        end
    end

    GetHandleFrame(self):SetParent(parent);
end

---------------------------------------------------------------------------
-- Type specific methods

function HANDLE:Disable()
    local frame = GetHandleFrame(self);
    if not frame:IsObjectType("Button") then
        error("Frame is not a Button");
        return;
    end
    frame:Disable();
end

function HANDLE:Enable()
    local frame = GetHandleFrame(self);
    if not frame:IsObjectType("Button") then
        error("Frame is not a Button");
        return;
    end
    frame:Enable();
end
