module Concurrent
  module Edge

    # @!macro [attach] atomic_markable_reference
    #
    #   An atomic reference which maintains an object reference along with a mark bit
    #   that can be updated atomically.
    #
    #   @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicMarkableReference.html
    #     java.util.concurrent.atomic.AtomicMarkableReference
    class AtomicMarkableReference < ::Concurrent::Synchronization::Object

      private(*attr_atomic(:reference))

      # @!macro [attach] atomic_markable_reference_method_initialize
      def initialize(value = nil, mark = false)
        super()
        self.reference = ImmutableArray[value, mark]
      end

      # @!macro [attach] atomic_markable_reference_method_compare_and_set
      #
      #   Atomically sets the value and mark to the given updated value and
      #   mark given both:
      #     - the current value == the expected value &&
      #     - the current mark == the expected mark
      #
      #   @param [Object] expected_val the expected value
      #   @param [Object] new_val the new value
      #   @param [Boolean] expected_mark the expected mark
      #   @param [Boolean] new_mark the new mark
      #
      #   @return [Boolean] `true` if successful. A `false` return indicates
      #   that the actual value was not equal to the expected value or the
      #   actual mark was not equal to the expected mark
      def compare_and_set(expected_val, new_val, expected_mark, new_mark)
        # Memoize a valid reference to the current AtomicReference for
        # later comparison.
        current = reference
        curr_val, curr_mark = current

        # Ensure that that the expected marks match.
        return false unless expected_mark == curr_mark

        if expected_val.is_a? Numeric
          # If the object is a numeric, we need to ensure we are comparing
          # the numerical values
          return false unless expected_val == curr_val
        else
          # Otherwise, we need to ensure we are comparing the object identity.
          # Theoretically, this could be incorrect if a user monkey-patched
          # `Object#equal?`, but they should know that they are playing with
          # fire at that point.
          return false unless expected_val.equal? curr_val
        end

        prospect = ImmutableArray[new_val, new_mark]

        compare_and_set_reference current, prospect
      end
      alias_method :compare_and_swap, :compare_and_set

      # @!macro [attach] atomic_markable_reference_method_get
      #
      #   Gets the current reference and marked values.
      #
      #   @return [ImmutableArray] the current reference and marked values
      def get
        reference
      end

      # @!macro [attach] atomic_markable_reference_method_value
      #
      #   Gets the current value of the reference
      #
      #   @return [Object] the current value of the reference
      def value
        reference[0]
      end

      # @!macro [attach] atomic_markable_reference_method_mark
      #
      #   Gets the current marked value
      #
      #   @return [Boolean] the current marked value
      def mark
        reference[1]
      end
      alias_method :marked?, :mark

      # @!macro [attach] atomic_markable_reference_method_set
      #
      #   _Unconditionally_ sets to the given value of both the reference and
      #   the mark.
      #
      #   @param [Object] new_val the new value
      #   @param [Boolean] new_mark the new mark
      #
      #   @return [ImmutableArray] both the new value and the new mark
      def set(new_val, new_mark)
        self.reference = ImmutableArray[new_val, new_mark]
      end

      # @!macro [attach] atomic_markable_reference_method_update
      #
      # Pass the current value and marked state to the given block, replacing it
      # with the block's results. May retry if the value changes during the
      # block's execution.
      #
      # @yield [Object] Calculate a new value and marked state for the atomic
      #   reference using given (old) value and (old) marked
      # @yieldparam [Object] old_val the starting value of the atomic reference
      # @yieldparam [Boolean] old_mark the starting state of marked
      #
      # @return [ImmutableArray] the new value and new mark
      def update
        loop do
          old_val, old_mark = reference
          new_val, new_mark = yield old_val, old_mark

          if compare_and_set old_val, new_val, old_mark, new_mark
            return ImmutableArray[new_val, new_mark]
          end
        end
      end

      # @!macro [attach] atomic_markable_reference_method_try_update!
      #
      # Pass the current value to the given block, replacing it
      # with the block's result. Raise an exception if the update
      # fails.
      #
      # @yield [Object] Calculate a new value and marked state for the atomic
      #   reference using given (old) value and (old) marked
      # @yieldparam [Object] old_val the starting value of the atomic reference
      # @yieldparam [Boolean] old_mark the starting state of marked
      #
      # @return [ImmutableArray] the new value and marked state
      #
      # @raise [Concurrent::ConcurrentUpdateError] if the update fails
      def try_update!
        old_val, old_mark = reference
        new_val, new_mark = yield old_val, old_mark

        unless compare_and_set old_val, new_val, old_mark, new_mark
          fail ::Concurrent::ConcurrentUpdateError,
               'AtomicMarkableReference: Update failed due to race condition.',
               'Note: If you would like to guarantee an update, please use ' \
               'the `AtomicMarkableReference#update` method.'
        end

        ImmutableArray[new_val, new_mark]
      end

      # @!macro [attach] atomic_markable_reference_method_try_update
      #
      # Pass the current value to the given block, replacing it with the
      # block's result. Simply return nil if update fails.
      #
      # @yield [Object] Calculate a new value and marked state for the atomic
      #   reference using given (old) value and (old) marked
      # @yieldparam [Object] old_val the starting value of the atomic reference
      # @yieldparam [Boolean] old_mark the starting state of marked
      #
      # @return [ImmutableArray] the new value and marked state, or nil if
      # the update failed
      def try_update
        old_val, old_mark = reference
        new_val, new_mark = yield old_val, old_mark

        return unless compare_and_set old_val, new_val, old_mark, new_mark

        ImmutableArray[new_val, new_mark]
      end

      # Internal/private ImmutableArray for representing pairs
      class ImmutableArray < ::Array
        def self.new(*args)
          super(*args).freeze
        end
      end
    end
  end
end
