module React
  class Validator
    
    def self.build(&block)
      self.new.build(&block)
    end
    
    def build(&block)
      instance_eval(&block)
      self
    end

    def initialize
      @rules = {children: {required: false}}
    end

    def requires(prop_name, options = {})
      rule = options
      options[:required] = true
      @rules[prop_name] = options
    end

    def optional(prop_name, options = {})
      rule = options
      options[:required] = false
      @rules[prop_name] = options
    end
    
    def all_others(prop_name)
      @all_others = {}
    end
    
    def collect_all_others(params)
      Hash[params.collect { |prop_name, value| [prop_name, value] if @rules[prop_name] == nil}.compact]
    end
    
    def type_check(errors, error_prefix, object, klass, nil_allowed)
      return if !object and nil_allowed
      is_native = !object.respond_to?(:is_a?) rescue true
      if is_native or !object.is_a? klass
        unless klass.respond_to? :_react_param_conversion and klass._react_param_conversion(object, :validate_only)
          errors << "#{error_prefix} could not be converted to #{klass}"
        end
      end
    end
  
    def validate(props)
      errors = []
      
      if @all_others
        props.each do |prop_name, value|
          @all_others[prop_name] = value if @rules[prop_name] == nil
        end
      else   
        props.keys.each do |prop_name|
          errors <<  "Provided prop `#{prop_name}` not specified in spec"  if @rules[prop_name] == nil
        end
      end

      props = props.select {|key| @rules.keys.include?(key) }

      # requires or not
      (@rules.keys - props.keys).each do |prop_name|
        errors << "Required prop `#{prop_name}` was not specified" if @rules[prop_name][:required]
      end
      # type checking
      props.each do |prop_name, value|
        if klass = @rules[prop_name][:type]
          is_klass_array = klass.is_a?(Array) and klass.length > 0 rescue nil
          if is_klass_array
            value_is_array_like = value.respond_to?(:each_with_index) rescue nil
            if value_is_array_like
              value.each_with_index { |ele, i| type_check(errors, "Provided prop `#{prop_name}`[#{i}]", ele, klass[0], @rules[prop_name][:allow_nil]) }
            else
              errors << "Provided prop `#{prop_name}` was not an Array"
            end
          else
            type_check(errors, "Provided prop `#{prop_name}`", value, klass, @rules[prop_name][:allow_nil])
          end
        end
      end

      # values
      props.each do |prop_name, value|
        if values = @rules[prop_name][:values]
          errors << "Value `#{value}` for prop `#{prop_name}` is not an allowed value" unless values.include?(value)
        end
      end
      
      errors
    end

    def default_props
      @rules
      .select {|key, value| value.keys.include?("default") }
      .inject({}) {|memo, (k,v)| memo[k] = v[:default]; memo}
    end
  end
end