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

require 'delegate'
require 'contrast/extension/module'
require 'contrast/utils/object_share'

module Contrast
  # This is the base module for our components classes. It is intended to
  # facilitate the translation of the Common Configuration settings to usable
  # Ruby methods. Any class under this namespace should be required here,
  # providing a single point of require for this functionality.
  module Components
    # Include this into your classes and modules,
    # and use 'access_component' to define constants that will allow
    # interaction with other components.
    module Interface
      def self.included klass
        # Upon inclusion, ComponentInterfaces extends the including with
        # these two interfaces.
        # Interface provides a class-level method 'access_component'
        # that regulates per-class access to agent state.
        # (It's a glorified `include MyComponent`).
        klass.extend Contrast::Components::ComponentReceiverClassInterface
      end
    end

    # All component access is gated through delegators.
    #
    # One delegator is used by the calling class,
    # so we can tweak outgoing calls.
    #
    # The second delegator is used by the receiving component,
    # so we can tweak incoming calls.
    #
    # We use __setobj__ to decide which component implementation to use.
    # This is intended to provide flexibility in design and
    # simplicity in testing.
    class ComponentDelegator < SimpleDelegator
      # intentionally left blank
    end

    # All components should inherit from this,
    # whether Interfaces, InstanceMethods or ClassMethods.
    module ComponentBase
      def self.included klass
        klass.extend  Methods
        klass.include Methods
      end

      module Methods # :nodoc:
        # use this to determine if the configuration value is literally boolean
        # false or some form of the word `false`, regardless of case. It should
        # be used for those values which default to `true` as they should only
        # treat a value explicitly set to `false` as such.
        #
        # @param config_param [Boolean,String] the value to check
        # @return [Boolean] should the value be treated as `false`
        def false? config_param
          return false if config_param == true
          return true if config_param == false
          return false unless config_param.cs__is_a?(String)

          Contrast::Utils::ObjectShare::FALSE.casecmp?(config_param)
        end

        # use this to determine if the configuration value is literally boolean
        # true or some form of the word `true`, regardless of case. It should
        # be used for those values which default to `false` as they should only
        # treat a value explicitly set to `true` as such.
        #
        # @param config_param [Boolean,String] the value to check
        # @return [Boolean] should the value be treated as `true`
        def true? config_param
          return false if config_param == false
          return true if config_param == true
          return false unless config_param.cs__is_a?(String)

          Contrast::Utils::ObjectShare::TRUE.casecmp?(config_param)
        end
      end
    end

    def self.component_const_name mod_name
      mod_name = mod_name.split('::').last
      @cache ||= {}
      @cache[mod_name] ||= mod_name. # CamelCaseName
          split(/(?=[A-Z])/)&.          # ['Camel', 'Case', 'Name']
          map(&:upcase)&.               # ['CAMEL', 'CASE', 'NAME']
          join('_')                     # 'CAMEL_CASE_NAME'
    end

    # Interface to allow for iteration over each of the configuration
    # components
    module ComponentReceiverClassInterface
      # Components are manually required at the end of
      # this file, and this constant is then frozen.
      # RUBY-535 to handle this better.
      COMPONENT_MAP = {} # rubocop:disable Style/MutableConstant

      # TODO: RUBY-535
      # This module is used via `extend`, so it can't access
      # constants we define here.
      def component_map
        COMPONENT_MAP
      end

      # .access_component
      #
      # to be used as:
      #
      # class Abc
      #   include Contrast::Components::Interface
      #   access_component :logging, :agent
      #
      #   def function
      #     if AGENT.disabled?
      #       0 / 3
      #     end
      #   rescue
      #     logger.error "this function did error"
      #   end
      # end
      #
      # `:logger` creates a #logger and .logger method
      # `:agent` provides an AGENT constant, analogous to a local singleton.
      #
      def access_component *component_set_syms
        @_access_component ||= {}

        component_set_syms.each do |sym|
          next if @_access_component[sym]

          if (mods = component_map[sym]) # rubocop:disable Style/GuardClause
            # We may support multiple components via one access request.
            mods.each do |m|
              name = Contrast::Components.component_const_name(m.name)
              cs__const_set(name, m::COMPONENT_INTERFACE) if m.cs__const_defined?(:COMPONENT_INTERFACE)
              include m::InstanceMethods               if m.cs__const_defined?(:InstanceMethods, false)
              extend  m::ClassMethods                  if m.cs__const_defined?(:ClassMethods, false)
            end

            @_access_component[sym] = true
          else
            raise NoMethodError, "#{ self } asked to access undefined component '#{ sym }'."
          end
        end
      end
    end
  end
end

# Components can depend on other components, but it should be a
# directed acyclic graph.

# Scope shouldn't depend on anything.
require 'contrast/components/scope'
Contrast::Components::ComponentReceiverClassInterface::COMPONENT_MAP[:scope] = [Contrast::Components::Scope]

# Config depends on Scope.
require 'contrast/components/config'
Contrast::Components::ComponentReceiverClassInterface::COMPONENT_MAP[:config] = [Contrast::Components::Config]

# Settings should not depend on anything but Config.
require 'contrast/components/settings'
Contrast::Components::ComponentReceiverClassInterface::COMPONENT_MAP[:settings] = [Contrast::Components::Settings]

require 'contrast/components/assess'
require 'contrast/components/protect'
require 'contrast/components/inventory'
Contrast::Components::ComponentReceiverClassInterface::COMPONENT_MAP[:analysis] = [
  Contrast::Components::Protect,
  Contrast::Components::Assess,
  Contrast::Components::Inventory
]

require 'contrast/components/logger'
Contrast::Components::ComponentReceiverClassInterface::COMPONENT_MAP[:logging] = [Contrast::Components::Logger]

require 'contrast/components/agent'
Contrast::Components::ComponentReceiverClassInterface::COMPONENT_MAP[:agent] = [Contrast::Components::Agent]

require 'contrast/components/contrast_service'
Contrast::Components::ComponentReceiverClassInterface::COMPONENT_MAP[:contrast_service] = [Contrast::Components::ContrastService]

require 'contrast/components/app_context'
Contrast::Components::ComponentReceiverClassInterface::COMPONENT_MAP[:app_context] = [Contrast::Components::AppContext]

require 'contrast/components/heap_dump'
Contrast::Components::ComponentReceiverClassInterface::COMPONENT_MAP[:heap_dump] = [Contrast::Components::HeapDump]

require 'contrast/components/sampling'
Contrast::Components::ComponentReceiverClassInterface::COMPONENT_MAP[:sampling] = [Contrast::Components::Sampling]

Contrast::Components::ComponentReceiverClassInterface::COMPONENT_MAP.cs__freeze