module ActionWebService # :nodoc:
  module API # :nodoc:
    # A web service API class specifies the methods that will be available for
    # invocation for an API. It also contains metadata such as the method type
    # signature hints.
    #
    # It is not intended to be instantiated.
    #
    # It is attached to web service implementation classes like
    # ActionWebService::Base and ActionController::Base derivatives by using
    # <tt>container.web_service_api</tt>, where <tt>container</tt> is an
    # ActionController::Base or a ActionWebService::Base.
    #
    # See ActionWebService::Container::Direct::ClassMethods for an example
    # of use.
    class Base
      # Whether to transform the public API method names into camel-cased names 
      class_inheritable_option :inflect_names, true

      # By default only HTTP POST requests are processed
      class_inheritable_option :allowed_http_methods, [ :post ]
       
      # Whether to allow ActiveRecord::Base models in <tt>:expects</tt>.
      # The default is +false+; you should be aware of the security implications
      # of allowing this, and ensure that you don't allow remote callers to
      # easily overwrite data they should not have access to.
      class_inheritable_option :allow_active_record_expects, false

      # If present, the name of a method to call when the remote caller
      # tried to call a nonexistent method. Semantically equivalent to
      # +method_missing+.
      class_inheritable_option :default_api_method

      # Disallow instantiation
      private_class_method :new, :allocate
      
      class << self
        include ActionWebService::SignatureTypes

        # API methods have a +name+, which must be the Ruby method name to use when
        # performing the invocation on the web service object.
        #
        # The signatures for the method input parameters and return value can
        # by specified in +options+.
        #
        # A signature is an array of one or more parameter specifiers. 
        # A parameter specifier can be one of the following:
        #
        # * A symbol or string representing one of the Action Web Service base types.
        #   See ActionWebService::SignatureTypes for a canonical list of the base types.
        # * The Class object of the parameter type
        # * A single-element Array containing one of the two preceding items. This
        #   will cause Action Web Service to treat the parameter at that position
        #   as an array containing only values of the given type.
        # * A Hash containing as key the name of the parameter, and as value
        #   one of the three preceding items
        # 
        # If no method input parameter or method return value signatures are given,
        # the method is assumed to take no parameters and/or return no values of
        # interest, and any values that are received by the server will be
        # discarded and ignored.
        #
        # Valid options:
        # [<tt>:expects</tt>]             Signature for the method input parameters
        # [<tt>:returns</tt>]             Signature for the method return value
        # [<tt>:expects_and_returns</tt>] Signature for both input parameters and return value
        def api_method(name, options={})
          unless options.is_a?(Hash)
            raise(ActionWebServiceError, "Expected a Hash for options")
          end
          validate_options([:expects, :returns, :expects_and_returns], options.keys)
          if options[:expects_and_returns]
            expects = options[:expects_and_returns]
            returns = options[:expects_and_returns]
          else
            expects = options[:expects]
            returns = options[:returns]
          end
          expects = canonical_signature(expects)
          returns = canonical_signature(returns)
          if expects
            expects.each do |type|
              type = type.element_type if type.is_a?(ArrayType)
              if type.type_class.ancestors.include?(ActiveRecord::Base) && !allow_active_record_expects
                raise(ActionWebServiceError, "ActiveRecord model classes not allowed in :expects")
              end
            end
          end
          name = name.to_sym
          public_name = public_api_method_name(name)
          method = Method.new(name, public_name, expects, returns)
          write_inheritable_hash("api_methods", name => method)
          write_inheritable_hash("api_public_method_names", public_name => name)
        end

        # Whether the given method name is a service method on this API
        #
        #   class ProjectsApi < ActionWebService::API::Base
        #     api_method :getCount, :returns => [:int]
        #   end
        #
        #   ProjectsApi.has_api_method?('GetCount')   #=> false
        #   ProjectsApi.has_api_method?(:getCount)    #=> true
        def has_api_method?(name)
          api_methods.has_key?(name)
        end
  
        # Whether the given public method name has a corresponding service method
        # on this API
        #
        #   class ProjectsApi < ActionWebService::API::Base
        #     api_method :getCount, :returns => [:int]
        #   end
        #
        #   ProjectsApi.has_api_method?(:getCount)    #=> false
        #   ProjectsApi.has_api_method?('GetCount')   #=> true
        def has_public_api_method?(public_name)
          api_public_method_names.has_key?(public_name)
        end
  
        # The corresponding public method name for the given service method name
        #
        #   ProjectsApi.public_api_method_name('GetCount')  #=> "GetCount"
        #   ProjectsApi.public_api_method_name(:getCount)   #=> "GetCount"
        def public_api_method_name(name)
          if inflect_names
            name.to_s.camelize
          else
            name.to_s
          end
        end
  
        # The corresponding service method name for the given public method name
        #
        #   class ProjectsApi < ActionWebService::API::Base
        #     api_method :getCount, :returns => [:int]
        #   end
        #        
        #   ProjectsApi.api_method_name('GetCount') #=> :getCount
        def api_method_name(public_name)
          api_public_method_names[public_name]
        end
  
        # A Hash containing all service methods on this API, and their
        # associated metadata.
        #
        #   class ProjectsApi < ActionWebService::API::Base
        #     api_method :getCount,          :returns => [:int]
        #     api_method :getCompletedCount, :returns => [:int]
        #   end
        #
        #   ProjectsApi.api_methods #=> 
        #     {:getCount=>#<ActionWebService::API::Method:0x24379d8 ...>,
        #      :getCompletedCount=>#<ActionWebService::API::Method:0x2437794 ...>}
        #   ProjectsApi.api_methods[:getCount].public_name #=> "GetCount"
        def api_methods
          read_inheritable_attribute("api_methods") || {}
        end

        # The Method instance for the given public API method name, if any
        #
        #   class ProjectsApi < ActionWebService::API::Base
        #     api_method :getCount,          :returns => [:int]
        #     api_method :getCompletedCount, :returns => [:int]
        #   end
        #
        #   ProjectsApi.public_api_method_instance('GetCount')  #=> <#<ActionWebService::API::Method:0x24379d8 ...>
        #   ProjectsApi.public_api_method_instance(:getCount)   #=> nil
        def public_api_method_instance(public_method_name)
          api_method_instance(api_method_name(public_method_name))
        end

        # The Method instance for the given API method name, if any
        #
        #   class ProjectsApi < ActionWebService::API::Base
        #     api_method :getCount,          :returns => [:int]
        #     api_method :getCompletedCount, :returns => [:int]
        #   end
        #
        #   ProjectsApi.api_method_instance(:getCount) #=> <ActionWebService::API::Method:0x24379d8 ...>
        #   ProjectsApi.api_method_instance('GetCount') #=> <ActionWebService::API::Method:0x24379d8 ...>
        def api_method_instance(method_name)
          api_methods[method_name]
        end

        # The Method instance for the default API method, if any
        def default_api_method_instance
          return nil unless name = default_api_method
          instance = read_inheritable_attribute("default_api_method_instance")
          if instance && instance.name == name
            return instance
          end
          instance = Method.new(name, public_api_method_name(name), nil, nil)
          write_inheritable_attribute("default_api_method_instance", instance)
          instance
        end

        private
          def api_public_method_names
            read_inheritable_attribute("api_public_method_names") || {}
          end
  
          def validate_options(valid_option_keys, supplied_option_keys)
            unknown_option_keys = supplied_option_keys - valid_option_keys
            unless unknown_option_keys.empty?
              raise(ActionWebServiceError, "Unknown options: #{unknown_option_keys}")
            end
          end
      end
    end

    # Represents an API method and its associated metadata, and provides functionality
    # to assist in commonly performed API method tasks.
    class Method
      attr :name
      attr :public_name
      attr :expects
      attr :returns

      def initialize(name, public_name, expects, returns)
        @name = name
        @public_name = public_name
        @expects = expects
        @returns = returns
        @caster = ActionWebService::Casting::BaseCaster.new(self)
      end
      
      # The list of parameter names for this method
      def param_names
        return [] unless @expects
        @expects.map{ |type| type.name }
      end

      # Casts a set of Ruby values into the expected Ruby values
      def cast_expects(params)
        @caster.cast_expects(params)
      end

      # Cast a Ruby return value into the expected Ruby value
      def cast_returns(return_value)
        @caster.cast_returns(return_value)
      end

      # Returns the index of the first expected parameter
      # with the given name
      def expects_index_of(param_name)
        return -1 if @expects.nil?
        (0..(@expects.length-1)).each do |i|
          return i if @expects[i].name.to_s == param_name.to_s
        end
        -1
      end

      # Returns a hash keyed by parameter name for the given
      # parameter list
      def expects_to_hash(params)
        return {} if @expects.nil?
        h = {}
        @expects.zip(params){ |type, param| h[type.name] = param }
        h
      end

      # Backwards compatibility with previous API
      def [](sig_type)
        case sig_type
        when :expects
          @expects.map{|x| compat_signature_entry(x)}
        when :returns
          @returns.map{|x| compat_signature_entry(x)}
        end
      end

      # String representation of this method
      def to_s
        fqn = ""
        fqn << (@returns ? (@returns[0].human_name(false) + " ") : "void ")
        fqn << "#{@public_name}("
        fqn << @expects.map{ |p| p.human_name }.join(", ") if @expects
        fqn << ")"
        fqn
      end

      private
        def compat_signature_entry(entry)
          if entry.array?
            [compat_signature_entry(entry.element_type)]
          else
            if entry.spec.is_a?(Hash)
              {entry.spec.keys.first => entry.type_class}
            else
              entry.type_class
            end
          end
        end
    end
  end
end
