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

require 'forwardable'
require 'contrast/utils/object_share'

module Contrast
  module Config
    # This is the base for our configuration classes. It is intended to
    # facilitate the translation of the Common Configuration settings to usable
    # Ruby classes.
    class BaseConfiguration
      extend Forwardable

      BOOLEANS = [true, false].cs__freeze

      attr_reader :map

      alias_method :to_hash, :map
      def_delegators :@map, :empty?, :key?, :delete, :fetch, :[], :[]=, :each, :each_pair, :each_key, :each_value

      EMPTY_VALUE = :EMPTY_VALUE

      def initialize hsh = {}, keys = {}
        @map = {}
        traverse_config(hsh, keys)
      end

      def assign_value_to_path_array dot_path_array, value
        current_level = self
        dot_path_array[0...-1].each do |segment|
          current_level = current_level.send(segment) if current_level.cs__respond_to?(segment)
        end
        last_entry = dot_path_array[-1]
        if current_level.nil? == false && current_level.cs__respond_to?(last_entry)
          current_level.send("#{ last_entry }=", value)
        end
        nil
      end

      def nil?
        @map.empty?
      end

      private

      # Traverse the given entity to build out the configuration graph.
      #
      # The values will be either a hash, indicating internal nodes to
      # traverse, or a value to set or the EMPTY_VALUE symbol, indicating a
      # leaf node.
      #
      # The spec_keys are the Contrast defined keys used to access a given
      # configuration. Each child of this class maintains its own set of keys,
      # as well as Objects to which those keys map.
      def traverse_config values, spec_keys
        internal_nodes = values.cs__respond_to?(:has_key?)
        spec_keys.each_pair do |spec_key, spec_value|
          user_provided_value = internal_nodes ? value_from_key_config(spec_key, values) : EMPTY_VALUE
          define_methods(spec_key, spec_value, user_provided_value)
        end
      end

      def define_methods spec_key, spec_value, user_provided_value
        str_key = spec_key.to_s
        assign_config_value(str_key, spec_value, user_provided_value)
        define_getter(str_key)
        define_setter(str_key)
      end

      def assign_config_value str_key, spec_value, user_provided_value
        @map[str_key] = if spec_value.is_a?(Class) && spec_value <= Contrast::Config::BaseConfiguration
                          spec_value.new(user_provided_value)
                        elsif spec_value.is_a?(Contrast::Config::DefaultValue) && user_provided_value == EMPTY_VALUE
                          spec_value.value
                        elsif BOOLEANS.include?(user_provided_value)
                          user_provided_value.to_s
                        else
                          user_provided_value
                        end
      end

      def value_from_key_config key, config_hash
        return config_hash[key] if config_hash.key?(key)
        return config_hash.fetch(key.to_sym, EMPTY_VALUE) if key.is_a?(String)

        config_hash.fetch(key.to_s, EMPTY_VALUE)
      end

      def define_getter str_key
        define_singleton_method str_key.to_sym do
          @map[str_key] == EMPTY_VALUE ? nil : @map[str_key]
        end
      end

      def define_setter str_key
        define_singleton_method "#{ str_key }=".to_sym do |new_value|
          boolean_value = new_value == true
          boolean_value ||= new_value == false
          @map[str_key] = boolean_value ? new_value.to_s : new_value
        end
      end
    end
  end
end