require 'r10k/settings/helpers'
require 'r10k/util/setopts'

module R10K
  module Settings

    # Define a single setting and additional attributes like descriptions,
    # default values, and validation.
    class Definition
      require 'r10k/settings/uri_definition'
      require 'r10k/settings/enum_definition'

      include R10K::Settings::Helpers
      include R10K::Util::Setopts

      # @!attribute [r] name
      #   @return [String] The name of this setting
      attr_reader :name

      # @!attribute [r] value
      #   @return [Object] An explicitly set value. This should only be used if
      #     an optional default value should not be used; otherwise use {#resolve}.
      attr_reader :value

      # @!attribute [r] desc
      #   @return [String] An optional documentation string for this setting.
      attr_reader :desc

      # @param name [Symbol] The name of the setting for this definition.
      # @param opts [Hash] Additional options for this definition to control
      #   validation, normalization, and the like.
      #
      # @option opts [Proc, Object] :default An optional proc or object for
      #   this setting. If no value has been set and the default is a Proc then
      #   it will be called and the result will be returned, otherwise if the
      #   value is not set the default value itself is returned.
      #
      # @options opts [Proc] :validate An optional proc that can be used to
      #   validate an assigned value. Default values are not assigned.
      #
      # @options opts [Proc] :normalize An optional proc that can be used to
      #   normalize an explicitly assigned value.
      def initialize(name, opts = {})
        @name = name
        setopts(opts, allowed_initialize_opts)
      end

      # Assign new values, perform validation checks, and return the final
      # values for this collection
      def evaluate(newvalue)
        assign(newvalue)
        validate
        resolve
      end

      # Store an explicit value for this definition
      #
      # If a :normalize hook has been given then it will be called with the
      # new value and the returned value will be stored.
      #
      # @param newvalue [Object] The value to store for this setting
      # @return [void]
      def assign(newvalue)
        if @normalize
          @value = @normalize.call(newvalue)
        else
          @value = newvalue
        end
      end

      # Call any validation hooks for this definition.
      #
      # The :validate hook will be called if the hook has been set and an
      # explicit value has been assigned to this definition. Validation
      # failures should be indicated by the :validate hook raising an exception.
      #
      # @raise [Exception] An exception class indicating that validation failed.
      # @return [nil]
      def validate
        if @value && @validate
          @validate.call(@value)
        end
        nil
      end

      # Compute the final value of this setting. If a value has not been
      # assigned the default value will be used.
      #
      # @return [Object] The final value of this definition.
      def resolve
        if !@value.nil?
          @value
        elsif !@default.nil?
          if @default == :inherit
            # walk all the way up to root, starting with grandparent
            ancestor = parent

            while ancestor = ancestor.parent
              return ancestor[@name].resolve if ancestor.respond_to?(:[]) && ancestor[@name]
            end
          elsif @default.is_a?(Proc)
            @default.call
          else
            @default
          end
        end
      end

      private

      # Subclasses may define additional params that are accepted at
      # initialization; they should override this method to add any
      # additional fields that should be respected.
      def allowed_initialize_opts
        {
          :desc      => true,
          :default   => true,
          :validate  => true,
          :normalize => true,
        }
      end
    end
  end
end
