--[[

    A generic pool to contain "active" and "free" objects.  Active objects
    are typically objects which:
        1. Have a relatively high construction cost
        2. Are not lightweight enough to create many of them at once
        3. Tend to be reused as dynamic elements of a larger container.

    The pool should "rapidly" reach a high-water mark of contained objects
    which should flow between active and free states on a regular basis.

    Ideal uses of the ZO_ObjectPool would be to contain objects such as:
        1. Scrolling combat text
        2. Tracked quests
        3. Buff icons

    The pools are not intended to be used to track a dynamic set of 
    contained objects whose membership grows to a predetermined size.
    As such, do NOT use the pool to track:
        1. Inventory slots
        2. Action buttons (unless creating something like AutoBar)

    A common usage pattern is instantiating templated controls.  To facilitate this
    without bloating your own code you should use ZO_ObjectPool_CreateControl which has
    been written here as a convenience.  It creates a control named "template"..id where
    id is an arbitrary value that will not conflict with other generated id's.

    If your system depends on having well-known names for controls, you should not use the
    convenience function.
--]]

ZO_ObjectPool = ZO_InitializingObject:Subclass()

function ZO_ObjectPool:Initialize(factoryFunctionOrObjectClass, resetFunction, usePooledObjectWrapper)
    assert(factoryFunctionOrObjectClass ~= nil)

    self.m_Active = {}
    self.m_Free = {}

    self:SetUsePooledObjectWrapper(usePooledObjectWrapper)
    self:SetFactory(factoryFunctionOrObjectClass)
    self:SetResetFunction(resetFunction)
    
    self.m_NextFree = 1 -- Just in case the user would like the pool to generate object keys.
    self.m_NextControlId = 0 -- Just in case the user would like the pool to generate id-based control suffixes
end

-- Indicates whether pooled objects should have the ZO_PooledObject interface applied to them,
-- allowing the use of object:ReleaseObject() to release them back to the object pool.
function ZO_ObjectPool:GetUsePooledObjectWrapper()
    return self.m_UsePooledObjectWrapper
end

-- Defines whether pooled objects should implement the ZO_PooledObject interface.
function ZO_ObjectPool:SetUsePooledObjectWrapper(usePooledObjectWrapper)
    self.m_UsePooledObjectWrapper = usePooledObjectWrapper
end

-- Defines the primary factory behavior
function ZO_ObjectPool:SetFactory(factoryFunctionOrObjectClass)
    if type(factoryFunctionOrObjectClass) == "function" then
        self.m_Factory = factoryFunctionOrObjectClass -- Signature: function(ZO_ObjectPool, objectKey)
    else
        self.m_Factory = function(pool, objectKey) return factoryFunctionOrObjectClass:New(pool, objectKey) end
    end
end

-- Defines the primary reset behavior
function ZO_ObjectPool:SetResetFunction(resetFunction)
    self.m_Reset = resetFunction -- Signature: function(objectBeingReset, ZO_ObjectPool)
end

function ZO_ObjectPool:GetNextFree()
    local freeKey, object = next(self.m_Free)
    if freeKey == nil or object == nil then
        local nextPotentialFree = self.m_NextFree
        self.m_NextFree = self.m_NextFree + 1
        return nextPotentialFree, nil
    end

    return freeKey, object
end

function ZO_ObjectPool:GetNextControlId()
    self.m_NextControlId = self.m_NextControlId + 1
    return self.m_NextControlId
end

function ZO_ObjectPool:GetTotalObjectCount()
    return self:GetActiveObjectCount() + self:GetFreeObjectCount()
end

function ZO_ObjectPool:GetActiveObjectCount()
    return NonContiguousCount(self.m_Active)
end

function ZO_ObjectPool:HasActiveObjects()
    return next(self.m_Active) ~= nil
end

function ZO_ObjectPool:GetActiveObjects()
    return self.m_Active
end

function ZO_ObjectPool:GetActiveObject(objectKey)
    return self.m_Active[objectKey]
end

function ZO_ObjectPool:ActiveObjectIterator(filterFunctions)
    return ZO_FilteredNonContiguousTableIterator(self.m_Active, filterFunctions)
end

function ZO_ObjectPool:GetFreeObjectCount()
    return NonContiguousCount(self.m_Free)
end

function ZO_ObjectPool:ActiveAndFreeObjectIterator(filterFunctions)
    local objects = {}
    ZO_CombineNonContiguousTables(objects, self.m_Active, self.m_Free)
    return ZO_FilteredNonContiguousTableIterator(objects, filterFunctions)
end

function ZO_ObjectPool:SetCustomAcquireBehavior(customAcquireBehavior)
    self.customAcquireBehavior = customAcquireBehavior -- Signature: function(objectBeingAcquired, objectKey)
end

-- Optional supplementary behavior that can be run after the factory has created the object
-- Often used in conjunction with extension pool classes (e.g.: ZO_ControlPool) when you want all the default factory behavior, plus something extra
function ZO_ObjectPool:SetCustomFactoryBehavior(customFactoryBehavior)
    self.customFactoryBehavior = customFactoryBehavior -- Signature: function(objectBeingCreated, objectKey, ZO_ObjectPool)
end

-- Optional supplementary behavior that can be run after the primary reset function has been run on the object
-- Often used in conjunction with extension pool classes (e.g.: ZO_ControlPool) when you want all the default reset behavior, plus something extra
function ZO_ObjectPool:SetCustomResetBehavior(customResetBehavior)
    self.customResetBehavior = customResetBehavior -- Signature: function(objectBeingReset)
end

function ZO_ObjectPool:AcquireObject(objectKey)
    -- If the object referred to by this key is already
    -- active there is very little work to do...just return it.
    local object = objectKey and self.m_Active[objectKey] or nil
    if object then
        return object, objectKey
    end
    
    -- If we know the key that we want, use that object first, otherwise just return the first object from the free pool
    -- A nil objectKey means that the caller doesn't care about tracking unique keys for these objects, or that the keys
    -- the system uses can't directly be used to look up the data.  Just manage them with pool-generated id's
    if objectKey == nil then
        objectKey, object = self:GetNextFree()
    else
        object = self.m_Free[objectKey]
    end

    --
    -- If the object is valid it was reclaimed from the free list, otherwise it needs to be created.
    -- Creation uses the m_Factory member which receives this pool as its only argument.
    -- Either way, after this, object must be non-nil
    --
    if object then
        self.m_Free[objectKey] = nil
    else        
        object = self:CreateObject(objectKey)
    end
           
    self.m_Active[objectKey] = object

    if self.customAcquireBehavior then
        self.customAcquireBehavior(object, objectKey)
    end
        
    return object, objectKey
end

function ZO_ObjectPool:CreateObject(objectKey)
    local object = self:m_Factory(objectKey)

    if self:GetUsePooledObjectWrapper() then
        -- Apply the ZO_PooledObject interface to the new object instance
        -- to allow the use of the :ReleaseObject() convenience method.
        ZO_PooledObject.OnPooledObjectFactory(object, self, objectKey)
    end

    if self.customFactoryBehavior then
        self.customFactoryBehavior(object, objectKey, self)
    end

    return object
end

function ZO_ObjectPool:ReleaseObject(objectKey)
    local object = self.m_Active[objectKey]
    
    if object then
        self:ResetObject(object)
        
        self.m_Active[objectKey] = nil
        self.m_Free[objectKey] = object
    end
end

function ZO_ObjectPool:ReleaseAllObjects()
    for k, v in pairs(self.m_Active) do
        self:ResetObject(v)
        
        self.m_Free[k] = v
    end
    
    ZO_ClearTable(self.m_Active)
end

function ZO_ObjectPool:ResetObject(object)
    if self.m_Reset then
        self.m_Reset(object, self)
    end

    if self.customResetBehavior then
        self.customResetBehavior(object)
    end
end

function ZO_ObjectPool:DestroyFreeObject(objectKey, destroyFunction)
    local object = self.m_Free[objectKey]
    destroyFunction(object)
    self.m_Free[objectKey] = nil
end

function ZO_ObjectPool:DestroyAllFreeObjects(destroyFunction)
    for _, object in pairs(self.m_Free) do
        destroyFunction(object)
    end
    ZO_ClearTable(self.m_Free)
end

function ZO_ObjectPool_CreateControl(templateName, objectPool, parentControl)
    return CreateControlFromVirtual(templateName, parentControl, templateName, objectPool:GetNextControlId())
end

function ZO_ObjectPool_CreateNamedControl(name, templateName, objectPool, parentControl)
    return CreateControlFromVirtual(name, parentControl, templateName, objectPool:GetNextControlId())
end

function ZO_ObjectPool_DefaultResetObject(object)
    object:Reset()
end

function ZO_ObjectPool_DefaultResetControl(control)
    control:SetHidden(true)
end

--[[

  ZO_PooledObject Abstract Class

  Helper class that allows pooled objects to be released more conveniently
  through the use of an added :ReleaseObject() method.

  To automatically apply this interface to objects in an object pool:

    local USE_POOLED_OBJECT_WRAPPER = true
    local myObjectPool = ZO_ObjectPool:New(myClass, ZO_ObjectPool_DefaultResetObject, USE_POOLED_OBJECT_WRAPPER)

  or

    local myControlPool = ZO_ControlPool:New("MyVirtualControl", myParentControl)
    myControlPool:SetUsePooledObjectWrapper(true)

--]]

ZO_PooledObject_Mixin = {}

function ZO_PooledObject_Mixin:SetPoolAndKey(pool, poolKey)
    self.pool = pool
    self.poolKey = poolKey
end

function ZO_PooledObject_Mixin:ReleaseObject()
    self.pool:ReleaseObject(self.poolKey)
end

ZO_PooledObject = ZO_InitializingObject:Subclass()
zo_mixin(ZO_PooledObject, ZO_PooledObject_Mixin)

-- Static method
-- Adds object pool support to an existing object.
function ZO_PooledObject.OnPooledObjectFactory(object, pool, key)
    zo_mixin(object, ZO_PooledObject_Mixin)
    object:SetPoolAndKey(pool, key)
end