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

require 'fiber'
require 'monitor'
require 'contrast/agent/scope'

# This is the Scope component.
#
# It tracks /Contrast/ scope.  That is, "are we currently doing assess
# or protect stuff within a patched method?" -- this is how we avoid doing
# Contrast stuff on Contrast code.
#
# Separately from this component, there is also require scope, which is an
# optimization on how we implement patching to `require`.
module Contrast
  module Components
    module Scope # :nodoc:
      MONITOR = Monitor.new
      EXECUTION_CONTEXT = {} # rubocop:disable Style/MutableConstant

      class Interface # :nodoc:
        include Contrast::Components::ComponentBase

        def initialize
          # This is probably redundant with #scope_for_current_ec's nil check.
          EXECUTION_CONTEXT[Fiber.current] = Contrast::Agent::Scope.new
        end

        # This returns the scope governing the current execution context.
        # Use this sparingly, preferring the instance & class methods to
        # access and query scope, rather than interacting with the scope
        # object directly.
        def scope_for_current_ec
          MONITOR.synchronize do
            return EXECUTION_CONTEXT[Fiber.current] ||= Contrast::Agent::Scope.new
          end
        end
      end

      module InstanceMethods # :nodoc:
        # For each instance method on a scope, define a forwarder
        # to the scope on the current execution context's scope.

        def scope_for_current_ec
          MONITOR.synchronize do
            return EXECUTION_CONTEXT[Fiber.current] ||= Contrast::Agent::Scope.new
          end
        end

        def enter_contrast_scope!
          scope_for_current_ec.enter_contrast_scope!
        end

        def enter_deserialization_scope!
          scope_for_current_ec.enter_deserialization_scope!
        end

        def enter_split_scope!
          scope_for_current_ec.enter_split_scope!
        end

        def enter_scope! name
          scope_for_current_ec.enter_scope! name
        end

        def exit_contrast_scope!
          scope_for_current_ec.exit_contrast_scope!
        end

        def exit_deserialization_scope!
          scope_for_current_ec.exit_deserialization_scope!
        end

        def exit_split_scope!
          scope_for_current_ec.exit_split_scope!
        end

        def exit_scope! name
          scope_for_current_ec.exit_scope! name
        end

        def in_contrast_scope?
          scope_for_current_ec.in_contrast_scope?
        end

        def in_deserialization_scope?
          scope_for_current_ec.in_deserialization_scope?
        end

        def in_split_scope?
          scope_for_current_ec.in_split_scope?
        end

        def split_scope_depth
          scope_for_current_ec.split_scope_depth
        end

        def in_scope? name
          scope_for_current_ec.in_scope? name
        end

        def with_contrast_scope
          scope_for_current_ec.enter_contrast_scope!
          yield
        ensure
          scope_for_current_ec.exit_contrast_scope!
        end

        def with_deserialization_scope
          scope_for_current_ec.enter_deserialization_scope!
          yield
        ensure
          scope_for_current_ec.exit_deserialization_scope!
        end

        def with_split_scope
          scope_for_current_ec.enter_split_scope!
          yield
        ensure
          scope_for_current_ec.exit_split_scope!
        end

        # TODO: RUBY-572
        #
        # Current behavior is to no-op if we're not "in a request context".
        # Our C functions were previously checking to see if we had a scope, because
        # scope was tacked on to a request context -- so "we have a scope, therefore,
        # we have a request context."  We've decoupled scopes from request contexts,
        # so now it checks "do we have a request context."
        # RUBY-290 should remove all of that, including this method.
        def in_request_context?
          !!Contrast::Agent::REQUEST_TRACKER.current
        end
      end

      def self.sweep_dead_ecs
        # TODO: RUBY-571, #sweep_dead_ecs compensates for a lack of weak tables
        # 'ec' for execution context.  in this case, it's a Fiber.
        # Threads rely on Fibers, so two birds, one stone.
        MONITOR.synchronize do
          EXECUTION_CONTEXT.delete_if do |ec, _scope|
            !ec.alive?
          end
        end
      end

      ClassMethods = InstanceMethods

      COMPONENT_INTERFACE = Interface.new
    end
  end
end