#### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify
#### it under the terms of the GNU General Public License as published
#### the Free Software Foundation, either version 3 of the License, or
#### (at your option) any later version.
####
#### This program is distributed in the hope that it will be useful,
#### but WITHOUT ANY WARRANTY; without even the implied warranty of
#### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
#### GNU General Public License for more details.
####
#### You should have received a copy of the GNU General Public License
#### along with this program.If not, see<http:####www.gnu.org/licenses/.>

module RemiLib
  # The `ArrayPool` class provides functionality to allow an array of a desired
  # size to be 'rented' out so that buffers of the same size don't need to be
  # constantly re-created.
  #
  # These are not thread safe.  If you need to use array pools with multiple
  # threads, create one `ArrayPool` instance directly per thread.
  class ArrayPool(T)
    @arrays : Array(Array(T)) = [] of Array(T)
    @rented : Array(Bool) = [] of Bool

    # Creates a new `ArrayPool` instance.
    #
    # `defaultVal` is the default value assigned to each index when creating
    # _NEW_ arrays.  Existing arrays are not re-initialized with `defaultVal`
    # between rentings.
    def initialize(@defaultVal : T)
    end

    # Requests an `Array(T)` of `size` elements from the pool.  If an
    # array of that size already exists and is not yet rented, it is returned
    # and set as 'rented'.  Otherwise a new array of that size is created, set
    # as 'rented', and returned.
    #
    # IMPORTANT: No effort is made to clear out the contents of old arrays
    # before returning one.
    def rent(size) : Array(T)
      @arrays.size.times do |i|
        if !@rented.unsafe_fetch(i) && @arrays.unsafe_fetch(i).size == size
          # Found one to rent
          @rented.unsafe_put(i, true)
          return @arrays.unsafe_fetch(i)
        end
      end

      # None available, make a new array and rent it out.
      ret = Array.new(size, @defaultVal)
      @arrays << ret
      @rented << true
      ret
    end

    # Lets the array pool know that you are done using `array` so that it can
    # mark it as not rented.  After calling this, you should not use `array`
    # anymore without first re-renting it with `ArrayPool.rent`.
    def return(array)
      @arrays.size.times do |i|
        if @arrays.unsafe_fetch(i).same?(array)
          @rented.unsafe_put(i, false)
          return
        end
      end

      raise "Attempted to return a buffer that was not part of the array pool"
    end

    # Rents out an array of `size` elements, then yields it to the block.  The
    # block is wrapped in a `begin..ensure` that will automatically return the
    # array when the block exits.
    def withRentedArray(size, &)
      buf = rent(size)
      begin
        yield buf
      ensure
        self.return(buf)
      end
    end

    # Removes all arrays from the pool and marks them as not rented.
    def clear : Nil
      @arrays.clear
      @rented.clear
    end
  end
end
