module Aws
  # @api private
  class ParamValidator

    include Seahorse::Model::Shapes

    EXPECTED_GOT = "expected %s to be %s, got value %s (class: %s) instead."

    # @param [Seahorse::Model::Shapes::ShapeRef] rules
    # @param [Hash] params
    # @return [void]
    def self.validate!(rules, params)
      new(rules).validate!(params)
    end

    # @param [Seahorse::Model::Shapes::ShapeRef] rules
    # @option options [Boolean] :validate_required (true)
    def initialize(rules, options = {})
      @rules = rules
      @validate_required = options[:validate_required] != false
    end

    # @param [Hash] params
    # @return [void]
    def validate!(params)
      errors = []
      structure(@rules, params, errors, 'params') if @rules
      raise ArgumentError, error_messages(errors) unless errors.empty?
    end

    private

    def structure(ref, values, errors, context)
      # ensure the value is hash like
      return unless correct_type?(ref, values, errors, context)

      shape = ref.shape

      # ensure required members are present
      if @validate_required
        shape.required.each do |member_name|
          if values[member_name].nil?
            param = "#{context}[#{member_name.inspect}]"
            errors << "missing required parameter #{param}"
          end
        end
      end

      # validate non-nil members
      values.each_pair do |name, value|
        unless value.nil?
          if shape.member?(name)
            member_ref = shape.member(name)
            shape(member_ref, value, errors, context + "[#{name.inspect}]")
          else
            errors << "unexpected value at #{context}[#{name.inspect}]"
          end
        end
      end
    end

    def list(ref, values, errors, context)
      # ensure the value is an array
      unless values.is_a?(Array)
        errors << expected_got(context, "an Array", values)
        return
      end

      # validate members
      member_ref = ref.shape.member
      values.each.with_index do |value, index|
        shape(member_ref, value, errors, context + "[#{index}]")
      end
    end

    def map(ref, values, errors, context)

      return unless correct_type?(ref, values, errors, context)

      key_ref = ref.shape.key
      value_ref = ref.shape.value

      values.each do |key, value|
        shape(key_ref, key, errors, "#{context} #{key.inspect} key")
        shape(value_ref, value, errors, context + "[#{key.inspect}]")
      end
    end

    def shape(ref, value, errors, context)
      case ref.shape
      when StructureShape then structure(ref, value, errors, context)
      when ListShape then list(ref, value, errors, context)
      when MapShape then map(ref, value, errors, context)
      when StringShape
        unless value.is_a?(String)
          errors << expected_got(context, "a String", value)
        end
      when IntegerShape
        unless value.is_a?(Integer)
          errors << expected_got(context, "an Integer", value)
        end
      when FloatShape
        unless value.is_a?(Float)
          errors << expected_got(context, "a Float", value)
        end
      when TimestampShape
        unless value.is_a?(Time)
          errors << expected_got(context, "a Time object", value)
        end
      when BooleanShape
        unless [true, false].include?(value)
          errors << expected_got(context, "true or false", value)
        end
      when BlobShape
        unless io_like?(value) or value.is_a?(String)
          errors << expected_got(context, "a String or IO object", value)
        end
      else
        raise "unhandled shape type: #{ref.shape.class.name}"
      end
    end

    def correct_type?(ref, value, errors, context)
      case value
      when Hash then true
      when ref[:struct_class] then true
      else
        errors << expected_got(context, "a hash", value)
        false
      end
    end

    def io_like?(value)
      value.respond_to?(:read) &&
      value.respond_to?(:rewind) &&
      value.respond_to?(:size)
    end

    def error_messages(errors)
      if errors.size == 1
        errors.first
      else
        prefix = "\n  - "
        "parameter validator found #{errors.size} errors:" +
          prefix + errors.join(prefix)
      end
    end

    def expected_got(context, expected, got)
      EXPECTED_GOT % [context, expected, got.inspect, got.class.name]
    end

  end
end
