# Copyright (c) 2020 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
      # The following %i[] list is the authoritative list
      # of scopes.  If you define a new symbol here, you'll
      # get scope methods:
      #   %i[monkey] ->
      #     enter_monkey_scope!
      #     exit_monkey_scope!
      #     in_monkey_scope?
      #     with_monkey_scope { special_monkey_function }
      SCOPE_LIST = %i[contrast deserialization].cs__freeze

      iv_list = SCOPE_LIST.map { |name| :"@#{ name }_scope" }
      define_method 'initialize' do
        iv_list.each do |iv_sym|
          instance_variable_set(iv_sym, 0)
        end
      end

      SCOPE_LIST.each do |name|
        iv_sym = :"@#{ name }_scope"

        define_method "in_#{ name }_scope?" do
          instance_variable_get(iv_sym).positive?
        end

        enter_method_sym = :"enter_#{ name }_scope!"
        define_method enter_method_sym do
          level = instance_variable_get(iv_sym)
          instance_variable_set(iv_sym, level + 1)
        end

        exit_method_sym = :"exit_#{ name }_scope!"
        define_method exit_method_sym do
          # 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
          level = instance_variable_get(iv_sym)
          instance_variable_set(iv_sym, level - 1)
        end

        define_method "with_#{ name }_scope" do |*_args, &block|
          send enter_method_sym
          block.call
        ensure
          send exit_method_sym
        end
      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