local zo_clamp = zo_clamp
local zo_lerp = zo_lerp

--Particle

ZO_Particle = ZO_Object:Subclass()

function ZO_Particle:New(...)
    local obj = ZO_Object.New(self)
    obj:Initialize(...)
    return obj
end

function ZO_Particle:Initialize()
    self.parameters = {}
end

function ZO_Particle:GetKey()
    return self.key
end

function ZO_Particle:SetKey(key)
    self.key = key
end

function ZO_Particle:GetTextureControl()
    return self.textureControl
end

function ZO_Particle:GetParameter(name)
    return self.parameters[name]
end

function ZO_Particle:SetParameter(name, value)
    self.parameters[name] = value
end

function ZO_Particle:ResetParameters()
    ZO_ClearTable(self.parameters)
end

--startTimeS can be in the past
function ZO_Particle:Start(parentControl, startTimeS, nowS)
    local parameters = self.parameters
    self.textureControl = PARTICLE_SYSTEM_MANAGER:AcquireTexture()

    self.textureControl:SetParent(parentControl)
    self.startTimeS = startTimeS
    local durationS = parameters["DurationS"]
    if durationS then
        self.endTimeS = self.startTimeS + durationS
    end
    self.textureControl:SetTexture(parameters["Texture"])
    local blendMode = parameters["BlendMode"]
    self.textureControl:SetBlendMode(blendMode or TEX_BLEND_MODE_ALPHA)

    --AnimationTimeline based animations

    self:AddAnimationsOnStart(durationS)

    self.animationTimelines = PARTICLE_SYSTEM_MANAGER:FinishBuildingAnimationTimelines()
    if self.animationTimelines then
        local elapsedTimeS = self:GetElapsedTime(nowS)
        for _, animationTimeline in ipairs(self.animationTimelines) do
            animationTimeline:PlayFromStart(elapsedTimeS * 1000)
        end
    end

    --Update Loop based animations

    self.hasOffsetXInterpolation, self.offsetX = self:InitializeEasedLerpParameter("StartOffsetX", "EndOffsetX", 0)
    self.hasOffsetYInterpolation, self.offsetY = self:InitializeEasedLerpParameter("StartOffsetY", "EndOffsetY", 0)
    self.hasOffsetZInterpolation, self.offsetZ = self:InitializeEasedLerpParameter("StartOffsetZ", "EndOffsetZ", 0)
end

function ZO_Particle:AddAnimationsOnStart(durationS)
    local parameters = self.parameters

    local DEFAULT_PLAYBACK_INFO = nil
    local DEFAULT_DURATION_S = durationS
    local DEFAULT_OFFSET_S = 0

    --Alpha
    local alpha
    local hasAlphaInterpolation, alpha = self:InitializeEasedLerpParameter("StartAlpha", "EndAlpha", 1)
    if hasAlphaInterpolation then
        local alphaAnimation = PARTICLE_SYSTEM_MANAGER:GetAnimation(self.textureControl, DEFAULT_PLAYBACK_INFO, ANIMATION_ALPHA, parameters["AlphaEasing"], DEFAULT_DURATION_S, DEFAULT_OFFSET_S)
        alphaAnimation:SetAlphaValues(parameters["StartAlpha"], parameters["EndAlpha"])
    end
    self.textureControl:SetAlpha(alpha)

    --Color
    local hasColorRInterpolation, colorR = self:InitializeEasedLerpParameter("StartColorR", "EndColorR", 1)
    local hasColorGInterpolation, colorG = self:InitializeEasedLerpParameter("StartColorG", "EndColorG", 1)
    local hasColorBInterpolation, colorB = self:InitializeEasedLerpParameter("StartColorB", "EndColorB", 1)
    if hasColorRInterpolation or hasColorGInterpolation or hasColorBInterpolation then
        local colorAnimation = PARTICLE_SYSTEM_MANAGER:GetAnimation(self.textureControl, DEFAULT_PLAYBACK_INFO, ANIMATION_COLOR, parameters["ColorEasing"], DEFAULT_DURATION_S, DEFAULT_OFFSET_S)
        colorAnimation:SetColorValues(parameters["StartColorR"], parameters["StartColorG"], parameters["StartColorB"], 1, parameters["EndColorR"], parameters["StartColorG"], parameters["StartColorB"], 1)
        colorAnimation:SetApplyAlpha(false)
    end
    self.textureControl:SetColor(colorR, colorG, colorB, alpha)

    --Flip Book
    local flipBookCellsWide = parameters["FlipBookCellsWide"]
    if flipBookCellsWide then
        local flipBookCellsHigh = parameters["FlipBookCellsHigh"]
        local NO_EASING_FUNCTION = nil
        local textureAnimation = PARTICLE_SYSTEM_MANAGER:GetAnimation(self.textureControl, parameters["FlipBookPlaybackInfo"], ANIMATION_TEXTURE, NO_EASING_FUNCTION, parameters["FlipBookDurationS"] or DEFAULT_DURATION_S, DEFAULT_OFFSET_S)
        textureAnimation:SetImageData(flipBookCellsWide, flipBookCellsHigh)
    else
        self.textureControl:SetTextureCoords(0, 1, 0, 1)
    end

    --Rotation
    local startRotationRadians = parameters["StartRotationRadians"]
    if startRotationRadians then
        local endRotationRadians = parameters["EndRotationRadians"]
        if not endRotationRadians then
            local rotationSpeedRadians = parameters["RotationSpeedRadians"]
            if rotationSpeedRadians then
                local totalTimeAnimatingS = DEFAULT_DURATION_S
                endRotationRadians = startRotationRadians + totalTimeAnimatingS * rotationSpeedRadians
            end
        end
        if endRotationRadians then
            local NO_EASING_FUNCTION = nil
            local textureRotationAnimation = PARTICLE_SYSTEM_MANAGER:GetAnimation(self.textureControl, DEFAULT_PLAYBACK_INFO, ANIMATION_TEXTUREROTATE, NO_EASING_FUNCTION, DEFAULT_DURATION_S, DEFAULT_OFFSET_S)
            textureRotationAnimation:SetRotationValues(startRotationRadians, endRotationRadians)
        end
    else
        startRotationRadians = 0
    end
    self.textureControl:SetTextureRotation(startRotationRadians)
end

function ZO_Particle:Stop()
    if self.textureControl then
        PARTICLE_SYSTEM_MANAGER:ReleaseTexture(self.textureControl)
        self.textureControl = nil
    end
    if self.animationTimelines then
        PARTICLE_SYSTEM_MANAGER:ReleaseAnimationTimelines(self.animationTimelines)
        self.animationTimelines = nil
    end
end

function ZO_Particle:GetProgress(timeS)
    return zo_clamp((timeS - self.startTimeS) / (self.endTimeS - self.startTimeS), 0, 1)
end

function ZO_Particle:GetElapsedTime(timeS)
    return zo_clamp(timeS - self.startTimeS, 0, self.endTimeS - self.startTimeS)
end

function ZO_Particle:IsDone(timeS)
    return timeS > self.endTimeS
end

function ZO_Particle:InitializeEasedLerpParameter(startName, endName, defaultValue)
    local parameters = self.parameters
    local hasInterpolation = false
    local initialValue = defaultValue
    if parameters[startName] ~= nil then
        initialValue = parameters[startName]
        if parameters[endName] ~= nil then
            hasInterpolation = true
        end
    end
    return hasInterpolation, initialValue
end

function ZO_Particle:ComputedEasedLerpParameter(startName, endName, easingName, defaultValue, progress)
    local parameters = self.parameters
    local startValue = parameters[startName]
    local endValue = parameters[endName]
    local value = defaultValue
    if startValue and endValue then
        local easingFunction = parameters[easingName]
        if easingFunction then
            progress = easingFunction(progress)
        end
        value = zo_lerp(startValue, endValue, progress)
    end
    return value
end

function ZO_Particle:OnUpdate(timeS)
    local progress = self:GetProgress(timeS)
    local parameters = self.parameters
    
    if self.hasOffsetXInterpolation then
        self.offsetX = self:ComputedEasedLerpParameter("StartOffsetX", "EndOffsetX", "OffsetXEasing", 0, progress)
    end
    if self.hasOffsetYInterpolation then
        self.offsetY = self:ComputedEasedLerpParameter("StartOffsetY", "EndOffsetY", "OffsetYEasing", 0, progress)
    end
    if self.hasOffsetZInterpolation then
        self.offsetZ = self:ComputedEasedLerpParameter("StartOffsetZ", "EndOffsetZ", "OffsetZEasing", 0, progress)
    end
end

function ZO_Particle:GetDimensionsFromParameters()
    local parameters = self.parameters
    local size = parameters["Size"]
    local width = parameters["Width"] or size
    local height = parameters["Height"] or size
    return width, height
end

--Scene Graph Particle

ZO_SceneGraphParticle = ZO_Particle:Subclass()

function ZO_SceneGraphParticle:New(...)
    return ZO_Particle.New(self, ...)
end

function ZO_SceneGraphParticle:Initialize()
    ZO_Particle.Initialize(self)
end

function ZO_SceneGraphParticle:SetParentNode(parentNode)
    self.parentNode = parentNode
end

--startTimeS can be in the past
function ZO_SceneGraphParticle:Start(parentControl, startTimeS, nowS)
    ZO_Particle.Start(self, parentControl, startTimeS, nowS)
    self.parentNode:AddTexture(self.textureControl, 0, 0, 0)

    --Scene graph nodes make use of the control scale for their own purposes so these have to calculate it into the size
    self.widthFromParamters, self.heightFromParameters = self:GetDimensionsFromParameters()
    local scale
    self.hasScaleInterpolation, scale = self:InitializeEasedLerpParameter("StartScale", "EndScale", 1)
    self.textureControl:SetDimensions(self.widthFromParamters * scale, self.heightFromParameters * scale)
end

function ZO_SceneGraphParticle:OnUpdate(timeS)
    ZO_Particle.OnUpdate(self, timeS)
    
    --Scene graph nodes make use of the control scale for their own purposes so these have to calculate it into the size
    if self.hasScaleInterpolation then        
        local progress = self:GetProgress(timeS)
        local scale = self:ComputedEasedLerpParameter("StartScale", "EndScale", "ScaleEasing", 1, progress)
        self.textureControl:SetDimensions(self.widthFromParamters * scale, self.heightFromParameters * scale)
    end
end

function ZO_SceneGraphParticle:Stop()
    if self.textureControl then
        self.parentNode:RemoveTexture(self.textureControl)
    end
    ZO_Particle.Stop(self)
end

--Expects that x is right and y is up
function ZO_SceneGraphParticle:SetPosition(x, y, z)
    if z == nil then
        z = 0
    end
    self.parentNode:SetControlPosition(self.textureControl, x + self.offsetX, -(y + self.offsetY), z + self.offsetZ)
end

--Control Particle

ZO_ControlParticle = ZO_Particle:Subclass()

function ZO_ControlParticle:New(...)
    return ZO_Particle.New(self, ...)
end

--startTimeS can be in the past
function ZO_ControlParticle:Start(parentControl, startTimeS, nowS)
    ZO_Particle.Start(self, parentControl, startTimeS, nowS)
    local parameters = self.parameters
    self.anchorPoint = parameters["AnchorPoint"] or CENTER
    self.anchorRelativePoint = parameters["AnchorRelativePoint"] or CENTER
    local drawLevel = parameters["DrawLevel"] or 0
    self.textureControl:SetDrawLevel(drawLevel)
    local drawLayer = parameters["DrawLayer"] or DL_BACKGROUND
    self.textureControl:SetDrawLayer(drawLayer)
    local width, height = self:GetDimensionsFromParameters()
    self.textureControl:SetDimensions(width, height)
end

function ZO_ControlParticle:AddAnimationsOnStart(durationS)
    ZO_Particle.AddAnimationsOnStart(self, durationS)

    local parameters = self.parameters
    local hasScaleInterpolation, scale = self:InitializeEasedLerpParameter("StartScale", "EndScale", 1)
    self.textureControl:SetScale(scale)
    if hasScaleInterpolation then
        local DEFAULT_PLAYBACK_INFO = nil
        local DEFAULT_DURATION_S = durationS
        local DEFAULT_OFFSET_S = 0

        local scaleAnimation = PARTICLE_SYSTEM_MANAGER:GetAnimation(self.textureControl, DEFAULT_PLAYBACK_INFO, ANIMATION_SCALE, parameters["ScaleEasing"], DEFAULT_DURATION_S, DEFAULT_OFFSET_S)
        scaleAnimation:SetScaleValues(parameters["StartScale"], parameters["EndScale"])
    end
end

--Expects that x is right and y is down
function ZO_ControlParticle:SetPosition(x, y, z)
    self.textureControl:SetAnchor(self.anchorPoint, nil, self.anchorRelativePoint, x + self.offsetX, y + self.offsetY)
end