# Copyright (c) 2021 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

module Contrast
  module Agent
    # Scope lets us disable Contrast for certain code calls. We need to do this so
    # that we don't propagate through our own code.
    #
    # Think logging: If you have
    # something like "The source was '" + source + "'", and source is tracked,
    # you'll trigger propagation with the + method. This in turn would cause
    # propagation if you log there "The target ''" + target + "' was propagated'"
    # Which would then cause another propagation with the '+' method, forever.
    #
    # Instead, we should say "If I'm already doing Contrast things, don't track
    # this"
    class Scope
      SCOPE_LIST = %i[contrast deserialization split].cs__freeze

      def initialize
        @contrast_scope = 0
        @deserialization_scope = 0
        @split_scope = 0
      end

      def in_contrast_scope?
        @contrast_scope.positive?
      end

      def in_deserialization_scope?
        @deserialization_scope.positive?
      end

      def in_split_scope?
        @split_scope.positive?
      end

      def enter_contrast_scope!
        @contrast_scope += 1
      end

      def enter_deserialization_scope!
        @deserialization_scope += 1
      end

      def enter_split_scope!
        @split_scope += 1
      end

      def split_scope_depth
        @split_scope
      end

      # Scope Exits...
      # by design, can go below zero.
      # every exit/enter pair (regardless of series)
      # should cancel each other out.
      #
      # so we prefer this sequence:
      #   scope =  0
      #   exit  = -1
      #   enter =  0
      #   enter =  1
      #   exit  =  0
      #   scope =  0
      #
      # over this sequence:
      #   scope =  0
      #   exit  =  0
      #   enter =  1
      #   enter =  2
      #   exit  =  1
      #   scope =  1
      def exit_contrast_scope!
        @contrast_scope -= 1
      end

      def exit_deserialization_scope!
        @deserialization_scope -= 1
      end

      def exit_split_scope!
        @split_scope -= 1
      end

      def with_contrast_scope
        enter_contrast_scope!
        yield
      ensure
        exit_contrast_scope!
      end

      def with_deserialization_scope
        enter_deserialization_scope!
        yield
      ensure
        exit_deserialization_scope!
      end

      def with_split_scope
        enter_split_scope!
        yield
      ensure
        exit_split_scope!
      end

      # Dynamic versions of the above.
      # These are equivalent, but they're slower and riskier.
      # Prefer the static methods if you know what scope you need at the call site.
      def in_scope? name
        cs__class.ensure_valid_scope! name
        call = with_contrast_scope { :"in_#{ name }_scope?" }
        send(call)
      end

      def enter_scope! name
        cs__class.ensure_valid_scope! name
        call = with_contrast_scope { :"enter_#{ name }_scope!" }
        send(call)
      end

      def exit_scope! name
        cs__class.ensure_valid_scope! name
        call = with_contrast_scope { :"exit_#{ name }_scope!" }
        send(call)
      end

      class << self
        def valid_scope? scope_sym
          Contrast::Agent::Scope::SCOPE_LIST.include? scope_sym
        end

        def ensure_valid_scope! scope_sym
          unless valid_scope? scope_sym # rubocop:disable Style/GuardClause
            with_contrast_scope do
              raise NoMethodError, "Scope '#{ scope_sym.inspect }' is not registered as a scope."
            end
          end
        end
      end
    end
  end
end