require 'concurrent/hash'

module Dry
  module Configurable
    # @private
    class Config
      class << self
        # @private
        def [](settings)
          ::Class.new(Config) do
            @settings = settings
            singleton_class.send(:attr_reader, :settings)

            @lock = ::Mutex.new
            @config_defined = false
          end
        end

        # @private
        def define_accessors!
          @lock.synchronize do
            break if config_defined?

            settings.each do |setting|
              next if setting.reserved?

              define_method(setting.name) do
                @config[setting.name]
              end

              define_method("#{setting.name}=") do |value|
                raise FrozenConfig, 'Cannot modify frozen config' if frozen?
                @config[setting.name] = setting.processor.(value)
              end
            end

            @config_defined = true
          end
        end

        # @private
        def config_defined?
          @config_defined
        end
      end

      def initialize
        @config = ::Concurrent::Hash.new
        @lock = ::Mutex.new
        @defined = false
      end

      def defined?
        @defined
      end

      # @private
      def define!(parent_config = EMPTY_HASH)
        @lock.synchronize do
          break if self.defined?

          self.class.define_accessors!
          set_values!(parent_config)

          @defined = true
        end

        self
      end

      # @private
      def finalize!
        define!
        @config.freeze
        freeze
      end

      # Serialize config to a Hash
      #
      # @return [Hash]
      #
      # @api public
      def to_h
        @config.each_with_object({}) do |(key, value), hash|
          case value
          when Config
            hash[key] = value.to_h
          else
            hash[key] = value
          end
        end
      end
      alias to_hash to_h

      # Get config value by a key
      #
      # @param [String,Symbol] name
      #
      # @return Config value
      def [](name)
        setting = self.class.settings[name.to_sym]

        if setting.nil?
          raise_unknown_setting_error(name)
        elsif setting.reserved?
          @config[setting.name]
        else
          public_send(name)
        end
      end

      # Set config value.
      # Note that finalized configs cannot be changed.
      #
      # @param [String,Symbol] name
      # @param [Object] value
      def []=(name, value)
        setting = self.class.settings[name.to_sym]

        if setting.nil?
          raise_unknown_setting_error(name)
        elsif setting.reserved?
          @config[setting.name] = setting.processor.(value)
        else
          public_send("#{name}=", value)
        end
      end

      # Whether config has a key
      #
      # @param [Symbol] key
      # @return [Bool]
      def key?(name)
        self.class.settings.name?(name)
      end

      # Recursively update values from a hash
      #
      # @param [Hash] values to set
      # @return [Config]
      def update(values)
        values.each do |key, value|
          if self[key].is_a?(Config)
            self[key].update(value)
          else
            self[key] = value
          end
        end
        self
      end

      def dup
        if self.defined?
          self.class.new.define!(to_h)
        else
          self.class.new
        end
      end

      private

      # @private
      def set_values!(parent)
        self.class.settings.each do |setting|
          if parent.key?(setting.name) && !setting.node?
            @config[setting.name] = parent[setting.name]
          elsif setting.undefined?
            @config[setting.name] = nil
          elsif setting.node?
            value = setting.value.create_config
            value.define!(parent.fetch(setting.name, EMPTY_HASH))
            self[setting.name] = value
          else
            self[setting.name] = setting.value
          end
        end
      end

      # @private
      def raise_unknown_setting_error(name)
        ::Kernel.raise ArgumentError, "+#{name}+ is not a setting name"
      end
    end
  end
end
