module WashOut
  class Param
    attr_accessor :raw_name
    attr_accessor :name
    attr_accessor :map
    attr_accessor :type
    attr_accessor :multiplied
    attr_accessor :value
    attr_accessor :source_class
    attr_accessor :soap_config

    # Defines a WSDL parameter with name +name+ and type specifier +type+.
    # The type specifier format is described in #parse_def.
    def initialize(soap_config, name, type, multiplied = false)
      type ||= {}
      @soap_config = soap_config
      @name       = name.to_s
      @raw_name   = name.to_s
      @map        = {}
      @multiplied = multiplied

      if soap_config.camelize_wsdl.to_s == 'lower'
        @name = @name.camelize(:lower)
      elsif soap_config.camelize_wsdl
        @name = @name.camelize
      end

      if type.is_a?(Symbol)
        @type = type.to_s
      elsif type.is_a?(Class)
        @type         = 'struct'
        @map          = self.class.parse_def(soap_config, type.wash_out_param_map)
        @source_class = type
      else
        @type = 'struct'
        @map  = self.class.parse_def(soap_config, type)
      end
    end

    # Converts a generic externally derived Ruby value, such as String or
    # Hash, to a native Ruby object according to the definition of this type.
    def load(data, key)
      if !data.has_key? key
        raise WashOut::Dispatcher::SOAPError, "Required SOAP parameter '#{key}' is missing"
      end

      data = data[key]
      data = [data] if @multiplied && !data.is_a?(Array)

      if struct?
        data ||= {}
        if @multiplied
          data.map do |x|
            map_struct x do |param, dat, elem|
              param.load(dat, elem)
            end
          end
        else
          map_struct data do |param, dat, elem|
            param.load(dat, elem)
          end
        end
      else
        operation = case type
          when 'string';       :to_s
          when 'integer';      :to_i
          when 'double';       :to_f
          when 'boolean';      lambda{|dat| dat === "0" ? false : !!dat}
          when 'date';         :to_date
          when 'datetime';     :to_datetime
          when 'time';         :to_time
          when 'base64Binary'; lambda{|dat| Base64.decode64(dat)}
          else raise RuntimeError, "Invalid WashOut simple type: #{type}"
        end

        begin
          if data.nil?
            data
          elsif @multiplied
            return data.map{|x| x.send(operation)} if operation.is_a?(Symbol)
            return data.map{|x| operation.call(x)} if operation.is_a?(Proc)
          elsif operation.is_a? Symbol
            data.send(operation)
          else
            operation.call(data)
          end
        rescue
          raise WashOut::Dispatcher::SOAPError, "Invalid SOAP parameter '#{key}' format"
        end
      end
    end

    # Checks if this Param defines a complex type.
    def struct?
      type == 'struct'
    end

    def classified?
      !source_class.nil?
    end

    def basic_type
      return name unless classified?
      return source_class.wash_out_param_name(@soap_config)
    end

    def xsd_type
      return 'int' if type.to_s == 'integer'
      return 'dateTime' if type.to_s == 'datetime'
      return type
    end

    # Returns a WSDL namespaced identifier for this type.
    def namespaced_type
      struct? ? "tns:#{basic_type}" : "xsd:#{xsd_type}"
    end

    # Parses a +definition+. The format of the definition is best described
    # by the following BNF-like grammar.
    #
    #   simple_type := :string | :integer | :double | :boolean
    #   nested_type := type_hash | simple_type | WashOut::Param instance
    #   type_hash   := { :parameter_name => nested_type, ... }
    #   definition  := [ WashOut::Param, ... ] |
    #                  type_hash |
    #                  simple_type
    #
    # If a simple type is passed as the +definition+, a single Param is returned
    # with the +name+ set to "value".
    # If a WashOut::Param instance is passed as a +nested_type+, the corresponding
    # +:parameter_name+ is ignored.
    #
    # This function returns an array of WashOut::Param objects.
    def self.parse_def(soap_config, definition)
      raise RuntimeError, "[] should not be used in your params. Use nil if you want to mark empty set." if definition == []
      return [] if definition == nil

      if definition.is_a?(Class) && definition.ancestors.include?(WashOut::Type)
        definition = definition.wash_out_param_map
      end

      if [Array, Symbol].include?(definition.class)
        definition = { :value => definition }
      end

      if definition.is_a? Hash
        definition.map do |name, opt|
          if opt.is_a? WashOut::Param
            opt
          elsif opt.is_a? Array
            WashOut::Param.new(soap_config, name, opt[0], true)
          else
            WashOut::Param.new(soap_config, name, opt)
          end
        end
      else
        raise RuntimeError, "Wrong definition: #{definition.inspect}"
      end
    end

    def flat_copy
      copy = self.class.new(@soap_config, @name, @type.to_sym, @multiplied)
      copy.raw_name = raw_name
      copy.source_class = source_class
      copy
    end

    def attribute?
      name[0] == "@"
    end

    def attr_name
      raise 'Not attribute' unless attribute?
      name[1..-1]
    end

    private

    # Used to load an entire structure.
    def map_struct(data)
      unless data.is_a?(Hash)
        raise WashOut::Dispatcher::SOAPError, "SOAP message structure is broken"
      end

      data   = data.with_indifferent_access
      struct = {}.with_indifferent_access

      # RUBY18 Enumerable#each_with_object is better, but 1.9 only.
      @map.map do |param|
        if data.has_key? param.raw_name
          param_name = param.attribute? ? param.attr_name : param.raw_name
          struct[param_name] = yield param, data, param.raw_name
        end
      end

      struct
    end
  end
end
