# frozen_string_literal: true

module Puppet::Pops
module Types
# Keeps track of self recursion of conceptual 'this' and 'that' instances using two separate maps and
# a state. The class is used when tracking self recursion in two objects ('this' and 'that') simultaneously.
# A typical example of when this is needed is when testing if 'that' Puppet Type is assignable to 'this'
# Puppet Type since both types may contain self references.
#
# All comparisons are made using the `object_id` of the instance rather than the instance itself.
#
# Hash#compare_by_identity is intentionally not used since we only need to keep
# track of object ids we've seen before, based on object_id, so we don't store
# entire objects in memory.
#
# @api private
class RecursionGuard
  attr_reader :state

  NO_SELF_RECURSION = 0
  SELF_RECURSION_IN_THIS = 1
  SELF_RECURSION_IN_THAT = 2
  SELF_RECURSION_IN_BOTH = 3

  def initialize
    @state = NO_SELF_RECURSION
  end

  # Checks if recursion was detected for the given argument in the 'this' context
  # @param instance [Object] the instance to check
  # @return [Integer] the resulting state
  def recursive_this?(instance)
    instance_variable_defined?(:@recursive_this_map) && @recursive_this_map.has_key?(instance.object_id) # rubocop:disable Lint/HashCompareByIdentity
  end

  # Checks if recursion was detected for the given argument in the 'that' context
  # @param instance [Object] the instance to check
  # @return [Integer] the resulting state
  def recursive_that?(instance)
    instance_variable_defined?(:@recursive_that_map) && @recursive_that_map.has_key?(instance.object_id) # rubocop:disable Lint/HashCompareByIdentity
  end

  # Add the given argument as 'this' invoke the given block with the resulting state
  # @param instance [Object] the instance to add
  # @return [Object] the result of yielding
  def with_this(instance)
    if (@state & SELF_RECURSION_IN_THIS) == 0
      tc = this_count
      @state |= SELF_RECURSION_IN_THIS if this_put(instance)
      if tc < this_count
        # recursive state detected
        result = yield(@state)

        # pop state
        @state &= ~SELF_RECURSION_IN_THIS
        @this_map.delete(instance.object_id)
        return result
      end
    end
    yield(@state)
  end

  # Add the given argument as 'that' invoke the given block with the resulting state
  # @param instance [Object] the instance to add
  # @return [Object] the result of yielding
  def with_that(instance)
    if (@state & SELF_RECURSION_IN_THAT) == 0
      tc = that_count
      @state |= SELF_RECURSION_IN_THAT if that_put(instance)
      if tc < that_count
        # recursive state detected
        result = yield(@state)

        # pop state
        @state &= ~SELF_RECURSION_IN_THAT
        @that_map.delete(instance.object_id)
        return result
      end
    end
    yield(@state)
  end

  # Add the given argument as 'this' and return the resulting state
  # @param instance [Object] the instance to add
  # @return [Integer] the resulting state
  def add_this(instance)
    if (@state & SELF_RECURSION_IN_THIS) == 0
      @state |= SELF_RECURSION_IN_THIS if this_put(instance)
    end
    @state
  end

  # Add the given argument as 'that' and return the resulting state
  # @param instance [Object] the instance to add
  # @return [Integer] the resulting state
  def add_that(instance)
    if (@state & SELF_RECURSION_IN_THAT) == 0
      @state |= SELF_RECURSION_IN_THAT if that_put(instance)
    end
    @state
  end

  # @return the number of objects added to the `this` map
  def this_count
    instance_variable_defined?(:@this_map) ? @this_map.size : 0
  end

  # @return the number of objects added to the `that` map
  def that_count
    instance_variable_defined?(:@that_map) ? @that_map.size : 0
  end

  private

  def this_put(o)
    id = o.object_id
    @this_map ||= {}
    if @this_map.has_key?(id)
      @recursive_this_map ||= {}
      @recursive_this_map[id] = true
      true
    else
      @this_map[id] = true
      false
    end
  end

  def that_put(o)
    id = o.object_id
    @that_map ||= {}
    if @that_map.has_key?(id)
      @recursive_that_map ||= {}
      @recursive_that_map[id] = true
      true
    else
      @that_map[id] = true
      false
    end
  end
end
end
end
