module Savon
  module Model

    def self.extended(base)
      base.setup
    end

    def setup
      class_operation_module
      instance_operation_module
    end

    # Accepts one or more SOAP operations and generates both class and instance methods named
    # after the given operations. Each generated method accepts an optional SOAP message Hash.
    def operations(*operations)
      operations.each do |operation|
        define_class_operation(operation)
        define_instance_operation(operation)
      end
    end

    def all_operations
      operations(*client.operations)
    end

    private

    # Defines a class-level SOAP operation.
    def define_class_operation(operation)
      class_operation_module.module_eval %{
        def #{operation.to_s.snakecase}(locals = {})
          client.call #{operation.inspect}, locals
        end
      }
    end

    # Defines an instance-level SOAP operation.
    def define_instance_operation(operation)
      instance_operation_module.module_eval %{
        def #{operation.to_s.snakecase}(locals = {})
          self.class.#{operation.to_s.snakecase} locals
        end
      }
    end

    # Class methods.
    def class_operation_module
      @class_operation_module ||= Module.new {

        def client(globals = {})
          @client ||= Savon::Client.new(globals)
        rescue InitializationError
          raise_initialization_error!
        end

        def global(option, *value)
          client.globals[option] = value
        end

        def raise_initialization_error!
          raise InitializationError,
            "Expected the model to be initialized with either a WSDL document or the SOAP endpoint and target namespace options.\n" \
            "Make sure to setup the model by calling the .client class method before calling the .global method.\n\n" \
            "client(wsdl: '/Users/me/project/service.wsdl')                              # to use a local WSDL document\n" \
            "client(wsdl: 'http://example.com?wsdl')                                     # to use a remote WSDL document\n" \
            "client(endpoint: 'http://example.com', namespace: 'http://v1.example.com')  # if you don't have a WSDL document"
        end

      }.tap { |mod| extend(mod) }
    end

    # Instance methods.
    def instance_operation_module
      @instance_operation_module ||= Module.new {

        def client
          self.class.client
        end

      }.tap { |mod| include(mod) }
    end

  end
end