local _,RPAL=...

-- ODYN Eye click motion is driven from one lightweight frame so the widgets
-- keep their own OnUpdate handlers (notably the player-frame drag handler).
-- Only artwork layers move: the clickable frame and its anchor never shift.
local TAU=math.pi*2
local active=setmetatable({},{__mode="k"})
local renownLoops=setmetatable({},{__mode="k"})
local hoverPreviews=setmetatable({},{__mode="k"})
local driver=CreateFrame("Frame")
driver:Hide()

local DURATIONS={
    blink=.44,runeSpin=.72,voidOrbit=.78,sunburst=.62,moonSway=.82,
    emberFlicker=.58,threadWeave=.74,petalTurn=.82,solarBeat=.88,
}

local function Clamp(value,minimum,maximum)
    if value<minimum then return minimum end
    if value>maximum then return maximum end
    return value
end

local function Smooth(value)
    value=Clamp(value,0,1)
    return value*value*(3-(2*value))
end

local function EyeSize(button)
    local width=tonumber(button and button.GetWidth and button:GetWidth()) or 26
    local height=tonumber(button and button.GetHeight and button:GetHeight()) or width
    return math.max(18,math.min(width,height))
end

local function Centre(texture,button,x,y)
    if not texture then return end
    texture:ClearAllPoints();texture:SetPoint("CENTER",button,"CENTER",x or 0,y or 0)
end

local function Rotate(texture,angle)
    if texture and texture.SetRotation then texture:SetRotation(angle or 0) end
end

local function Size(texture,width,height)
    if texture then texture:SetSize(width,height or width) end
end

local function Finish(button)
    active[button]=nil
    if button and not renownLoops[button] and RPAL.LayoutODYNEyeArt then RPAL.LayoutODYNEyeArt(button) end
end

local function FinishRenownLoop(button)
    renownLoops[button]=nil
    if button and RPAL.LayoutODYNEyeArt then RPAL.LayoutODYNEyeArt(button) end
end

local function Animate(button,state,progress)
    local art=button and button._odynEyeArt
    if not art then return end
    local baseSize=EyeSize(button)
    local hoverScale=math.max(0.75,tonumber(button._odynEyeHoverScale) or 1)
    local renownScale=math.max(0.75,tonumber(button._odynRenownPulseScale) or 1)
    local size=baseSize*hoverScale*renownScale
    local primarySize=size*1.08
    local ringScale=button._odynEyePreview and 1.22 or 1.44
    local pulse=math.sin(progress*math.pi)
    local eased=Smooth(progress)
    local mode=state.mode
    local themePrimary=RPAL.GetODYNEyeThemeColors and RPAL.GetODYNEyeThemeColors() or {1,.78,.32,1}

    -- A restrained halo response is shared so every successful click reads as
    -- input; each Eye's authored layers then perform their own signature move.
    Size(art.halo,size*(1.28+(pulse*.30)))
    art.halo:SetVertexColor(themePrimary[1] or 1,themePrimary[2] or .78,themePrimary[3] or .32,.20+(pulse*.68))
    art.halo:SetAlpha(1)
    if button.ring then
        Size(button.ring,size*(ringScale+(pulse*.11)))
        button.ring:SetVertexColor(themePrimary[1] or 1,themePrimary[2] or .78,themePrimary[3] or .32,.76+(pulse*.24))
        button.ring:SetAlpha(1)
    end

    if mode=="blink" then
        local lid=math.sin(progress*math.pi*2)
        lid=lid*lid
        Size(art.primary,primarySize,primarySize*(1-(lid*.54)))
        Size(art.accent,primarySize,primarySize*(1-(lid*.68)))
        art.accent:SetAlpha(.88-(lid*.28))
    elseif mode=="runeSpin" then
        Rotate(art.primary,eased*TAU)
        Rotate(art.accent,-eased*TAU*.76)
        local runePulse=math.sin(progress*math.pi*3);runePulse=runePulse*runePulse
        Size(art.accent,primarySize*(1+(runePulse*.15)))
        art.accent:SetAlpha(.62+(runePulse*.38))
    elseif mode=="voidOrbit" then
        local orbit=progress*TAU*1.25
        Rotate(art.primary,-eased*TAU*.42)
        Rotate(art.accent,eased*TAU*1.18)
        Centre(art.primary,button,math.cos(orbit)*size*.075,math.sin(orbit)*size*.045)
        Centre(art.accent,button,-math.cos(orbit)*size*.095,-math.sin(orbit)*size*.060)
        art.disc:SetAlpha(.72+(math.cos(progress*math.pi*2)*.25))
    elseif mode=="sunburst" then
        local burst=math.sin(progress*math.pi)
        Size(art.primary,primarySize*(1+(burst*.18)))
        Size(art.accent,primarySize*(1+(burst*.43)))
        Rotate(art.accent,eased*TAU*.24)
        art.accent:SetAlpha(.72+(burst*.28))
        Size(art.halo,size*(1.30+(burst*.62)))
    elseif mode=="moonSway" then
        local sway=math.sin(progress*math.pi*2)
        local lift=math.sin(progress*math.pi)*size*.075
        Centre(art.primary,button,sway*size*.055,lift)
        Centre(art.accent,button,-sway*size*.075,-lift*.45)
        Rotate(art.primary,sway*.18)
        Rotate(art.accent,-sway*.12)
        art.halo:SetAlpha(.40+(math.sin(progress*math.pi)*.50))
    elseif mode=="emberFlicker" then
        local flame=math.abs(math.sin(progress*math.pi*6))*(1-progress*.28)
        local flare=math.sin(progress*math.pi)
        Size(art.primary,primarySize*(1+(flame*.10)),primarySize*(1+(flame*.24)))
        Size(art.accent,primarySize*(1+(flare*.18)),primarySize*(1+(flame*.31)))
        Centre(art.accent,button,math.sin(progress*TAU*3)*size*.035,flame*size*.045)
        art.accent:SetAlpha(.54+(flame*.46))
    elseif mode=="threadWeave" then
        local weave=math.sin(progress*math.pi*2)
        Size(art.primary,primarySize*(1+(weave*.18)),primarySize*(1-(weave*.12)))
        Size(art.accent,primarySize*(1-(weave*.14)),primarySize*(1+(weave*.20)))
        Rotate(art.primary,weave*.20)
        Rotate(art.accent,-weave*.24)
        Centre(art.primary,button,weave*size*.045,0)
        Centre(art.accent,button,-weave*size*.045,0)
    elseif mode=="petalTurn" then
        local bloom=math.sin(progress*math.pi)
        Rotate(art.primary,eased*(TAU/5))
        Rotate(art.accent,-eased*(TAU/5)*1.35)
        Size(art.primary,primarySize*(1+(bloom*.20)))
        Size(art.accent,primarySize*(1+(bloom*.34)))
        Centre(art.accent,button,math.sin(progress*TAU)*size*.045,math.sin(progress*math.pi)*size*.055)
    elseif mode=="solarBeat" then
        local beat=math.sin(progress*math.pi*3);beat=beat*beat
        local crown=math.sin(progress*math.pi)
        Rotate(art.primary,eased*TAU*.48)
        Rotate(art.accent,-eased*TAU*.72)
        Size(art.primary,primarySize*(1+(beat*.12)))
        Size(art.accent,primarySize*(1+(crown*.28)))
        Size(art.halo,size*(1.30+(beat*.46)))
        art.accent:SetAlpha(.62+(beat*.38))
    end
end

-- Renown uses the same authored Eye identities, but as genuinely seamless
-- cycles. Unlike click FX this function is intentionally periodic at p=0/1:
-- no layer can land on a different rotation/offset at the loop seam.
local function AnimateLoop(button,state,progress)
    local art=button and button._odynEyeArt
    if not art then return end
    progress=(tonumber(progress) or 0)%1
    local baseSize=EyeSize(button)
    local hoverScale=math.max(0.75,tonumber(button._odynEyeHoverScale) or 1)
    local renownScale=math.max(0.75,tonumber(button._odynRenownPulseScale) or 1)
    local size=baseSize*hoverScale*renownScale
    local primarySize=size*1.08
    local ringScale=button._odynEyePreview and 1.22 or 1.44
    local phase=progress*TAU
    local wave=(1-math.cos(phase))*.5
    local sway=math.sin(phase)
    local mode=state.mode
    local themePrimary=RPAL.GetODYNEyeThemeColors and RPAL.GetODYNEyeThemeColors() or {1,.78,.32,1}

    Size(art.halo,size*(1.28+(wave*.25)))
    art.halo:SetVertexColor(themePrimary[1] or 1,themePrimary[2] or .78,themePrimary[3] or .32,.24+(wave*.50))
    art.halo:SetAlpha(1)
    if button.ring then
        Size(button.ring,size*(ringScale+(wave*.08)))
        button.ring:SetVertexColor(themePrimary[1] or 1,themePrimary[2] or .78,themePrimary[3] or .32,.78+(wave*.18))
        button.ring:SetAlpha(1)
    end

    if mode=="blink" then
        -- One complete close/open cycle. wave is 0 at both ends.
        local lid=wave*wave
        Size(art.primary,primarySize,primarySize*(1-(lid*.54)))
        Size(art.accent,primarySize,primarySize*(1-(lid*.68)))
        art.accent:SetAlpha(.88-(lid*.28))
    elseif mode=="runeSpin" then
        Rotate(art.primary,phase)
        Rotate(art.accent,-phase)
        local runePulse=math.sin(phase*2);runePulse=runePulse*runePulse
        Size(art.accent,primarySize*(1+(runePulse*.15)))
        art.accent:SetAlpha(.62+(runePulse*.38))
    elseif mode=="voidOrbit" then
        Rotate(art.primary,-phase)
        Rotate(art.accent,phase)
        Centre(art.primary,button,math.cos(phase)*size*.075,math.sin(phase)*size*.045)
        Centre(art.accent,button,-math.cos(phase)*size*.095,-math.sin(phase)*size*.060)
        art.disc:SetAlpha(.72+(math.cos(phase)*.25))
    elseif mode=="sunburst" then
        Size(art.primary,primarySize*(1+(wave*.18)))
        Size(art.accent,primarySize*(1+(wave*.43)))
        Rotate(art.accent,phase)
        art.accent:SetAlpha(.72+(wave*.28))
        Size(art.halo,size*(1.30+(wave*.62)))
    elseif mode=="moonSway" then
        local lift=wave*size*.075
        Centre(art.primary,button,sway*size*.055,lift)
        Centre(art.accent,button,-sway*size*.075,-lift*.45)
        Rotate(art.primary,sway*.18)
        Rotate(art.accent,-sway*.12)
        art.halo:SetAlpha(.40+(wave*.50))
    elseif mode=="emberFlicker" then
        local flame=math.sin(phase*3);flame=flame*flame
        Size(art.primary,primarySize*(1+(flame*.10)),primarySize*(1+(flame*.24)))
        Size(art.accent,primarySize*(1+(wave*.18)),primarySize*(1+(flame*.31)))
        Centre(art.accent,button,math.sin(phase*3)*size*.035,flame*size*.045)
        art.accent:SetAlpha(.54+(flame*.46))
    elseif mode=="threadWeave" then
        local weave=sway
        Size(art.primary,primarySize*(1+(weave*.18)),primarySize*(1-(weave*.12)))
        Size(art.accent,primarySize*(1-(weave*.14)),primarySize*(1+(weave*.20)))
        Rotate(art.primary,weave*.20)
        Rotate(art.accent,-weave*.24)
        Centre(art.primary,button,weave*size*.045,0)
        Centre(art.accent,button,-weave*size*.045,0)
    elseif mode=="petalTurn" then
        -- The Maplefall masks are five-fold radial, so 1/5 turn closes the
        -- visual seam while keeping the slower authored petal motion.
        Rotate(art.primary,progress*(TAU/5))
        Rotate(art.accent,-progress*(TAU/5))
        Size(art.primary,primarySize*(1+(wave*.20)))
        Size(art.accent,primarySize*(1+(wave*.34)))
        Centre(art.accent,button,sway*size*.045,wave*size*.055)
    elseif mode=="solarBeat" then
        local beat=math.sin(phase*2);beat=beat*beat
        Rotate(art.primary,sway*.26)
        Rotate(art.accent,-sway*.34)
        Size(art.primary,primarySize*(1+(beat*.12)))
        Size(art.accent,primarySize*(1+(wave*.28)))
        Size(art.halo,size*(1.30+(beat*.46)))
        art.accent:SetAlpha(.62+(beat*.38))
    end
end

driver:SetScript("OnUpdate",function(self,elapsed)
    local any=false
    for button,state in pairs(renownLoops) do
        if not button or (button.IsShown and not button:IsShown()) then
            FinishRenownLoop(button)
        else
            state.elapsed=(state.elapsed or 0)+(tonumber(elapsed) or 0)
            local duration=math.max(.05,tonumber(state.duration) or .64)
            AnimateLoop(button,state,(state.elapsed%duration)/duration)
            any=true
        end
    end
    for button,state in pairs(active) do
        if renownLoops[button] then
            active[button]=nil
        elseif not button or (button.IsShown and not button:IsShown()) then
            Finish(button)
        else
            state.elapsed=state.elapsed+(tonumber(elapsed) or 0)
            local progress=Clamp(state.elapsed/state.duration,0,1)
            if progress>=1 then Finish(button) else Animate(button,state,progress);any=true end
        end
    end
    for owner,preview in pairs(hoverPreviews) do
        if not owner or (owner.IsShown and not owner:IsShown()) then
            hoverPreviews[owner]=nil
            if preview and preview.button then Finish(preview.button) end
        else
            preview.elapsed=(preview.elapsed or 0)+(tonumber(elapsed) or 0)
            if preview.elapsed>=(preview.interval or 1.55) then
                preview.elapsed=0
                if RPAL.PlayODYNEyeClickFX then RPAL.PlayODYNEyeClickFX(preview.button,preview.name) end
            end
            any=true
        end
    end
    if not any then self:Hide() end
end)


function RPAL.IsODYNEyeFXActive(button)
    return button and (active[button]~=nil or renownLoops[button]~=nil) or false
end

function RPAL.IsODYNEyeRenownLoopActive(button)
    return button and renownLoops[button]~=nil or false
end

function RPAL.BeginODYNEyeRenownLoop(button,name)
    if not button then return false end
    name=name or button._odynEyeStyle or "Default"
    local style=RPAL.ODYN_EYE_STYLES and (RPAL.ODYN_EYE_STYLES[name] or RPAL.ODYN_EYE_STYLES.Default)
    local mode=(style and style.fx) or "blink"
    if RPAL.LayoutODYNEyeArt then RPAL.LayoutODYNEyeArt(button) end
    active[button]=nil
    renownLoops[button]={mode=mode,elapsed=0,duration=DURATIONS[mode] or .64}
    driver:Show()
    AnimateLoop(button,renownLoops[button],0)
    return true
end

function RPAL.EndODYNEyeRenownLoop(button)
    if not button or not renownLoops[button] then return false end
    FinishRenownLoop(button)
    return true
end

function RPAL.PlayODYNEyeClickFX(button,name)
    if not button then return false end
    -- Renown collection owns the Eye motion from the first +1 impact through
    -- the last. Clicking the orb during that window must not restart the cycle.
    if renownLoops[button] then return true end
    name=name or button._odynEyeStyle or "Default"
    local style=RPAL.ODYN_EYE_STYLES and (RPAL.ODYN_EYE_STYLES[name] or RPAL.ODYN_EYE_STYLES.Default)
    local mode=(style and style.fx) or "blink"
    if RPAL.LayoutODYNEyeArt then RPAL.LayoutODYNEyeArt(button) end
    active[button]={mode=mode,elapsed=0,duration=DURATIONS[mode] or .64}
    driver:Show()
    Animate(button,active[button],0)
    return true
end

-- Wardrobe cards replay their exact Eye motion while hovered, with a short
-- pause between cycles. Locked rewards use the same preview so players can see
-- what they are working toward before earning it.
function RPAL.AttachODYNEyeHoverPreview(owner,button,name)
    if not owner or not button or owner._odynEyeHoverPreviewAttached then return false end
    owner._odynEyeHoverPreviewAttached=true
    owner:EnableMouse(true)
    owner:HookScript("OnEnter",function(self)
        hoverPreviews[self]={button=button,name=name or button._odynEyeStyle or "Default",elapsed=0,interval=1.55}
        RPAL.PlayODYNEyeClickFX(button,name)
        driver:Show()
    end)
    owner:HookScript("OnLeave",function(self)
        local preview=hoverPreviews[self]
        hoverPreviews[self]=nil
        if preview and preview.button then Finish(preview.button) end
    end)
    owner:HookScript("OnHide",function(self)
        local preview=hoverPreviews[self]
        hoverPreviews[self]=nil
        if preview and preview.button then Finish(preview.button) end
    end)
    return true
end
