# Copyright 2011 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
#     http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.

require 'aws/model'
require 'aws/cacheable'

module AWS

  # @private
  class Resource

    include Model
    include Cacheable

    # @private
    class NotFound < StandardError; end

    def initialize *args

      super

      # cache static attributes passed into options

      options = args.last.is_a?(Hash) ? args.last : {}
      options.each_pair do |opt_name,opt_value|
        if 
          self.class.attributes.has_key?(opt_name) and
          self.class.attributes[opt_name].static?
        then
          static_attributes[opt_name] = opt_value
        end
      end

    end

    # @return [String] Returns a simple string representation of this resource.
    def inspect

      identifiers = []
      resource_identifiers.each do |key, value|
        if attr = self.class.attributes.values.find{|a| a.get_as == key }
          identifiers << "#{attr.name}:#{value}"
        else
          identifiers << "#{key}:#{value}"
        end
      end

      "<#{self::class} #{identifiers.join(' ')}>"

    end

    # @return [Boolean] Returns true if the objects references the same
    #   AWS resource.
    def == other
      other.kind_of?(self.class) and 
        resource_identifiers == other.resource_identifiers
    end

    alias_method :eql?, :==

    # @private
    protected
    def get_resource attr_name
      raise NotImplementedError
    end

    # @private
    protected
    def update_resource attr, value
      raise NotImplementedError
    end

    # Overide this method is subclasses of Resource.  This method should 
    # return an array of identifying key/value pairs.
    #
    #   # @private
    #   protected
    #   def resource_identifiers
    #     [[:user_name, name]]
    #   end
    # 
    # @private
    protected
    def resource_identifiers
      raise NotImplementedError
    end

    # @protected
    protected
    def resource_options(additional = {})
      Hash[resource_identifiers].merge(additional)
    end

    # @private
    protected
    def local_cache_key
      resource_identifiers.collect{|name,value| value.to_s }.join(":")
    end

    # @private
    protected
    def static_attributes
      @static_attributes ||= {}
    end

    # @private
    protected
    def ruby_name
      @ruby_name ||= Inflection.ruby_name(self.class.name)
    end

    # @private
    public
    def attributes_from_response resp

      attributes = {}

      self.class.attribute_providers_for(resp.request_type).each do |provider|
        attributes.merge!(provider.attributes_from_response(self, resp))
      end

      # cache static attributes
      attributes.each do |attr_name,value|
        if self.class.attributes[attr_name].static?
          static_attributes[attr_name] = value
        end
      end

      attributes.empty? ? nil : attributes

    end

    # @private
    protected
    def cache_static_attributes request_type, resp_obj
      self.class.attribute_providers_for(request_type).each do |provider|
        attributes = provider.attributes_from_response_object(resp_obj)
        attributes.each_pair do |attr_name,value|
          if self.class.attributes[attr_name].static?
            static_attributes[attr_name] = value
          end
        end
      end
    end


    class << self

      # @private
      def new_from request_type, resp_obj, *args
        resource = new(*args)  
        resource.send(:cache_static_attributes, request_type, resp_obj)
        resource
      end

      # @private
      def attributes
        @attributes ||= Hash.new do |hash,attr_name|
          raise "uknown attribute #{attr_name}"
        end
      end

      # @private
      def attribute_providers
        @attribute_providers ||= []
      end

      # @private
      def attribute_providers_for request_type
        attribute_providers.select do |provider|
          provider.request_types.include?(request_type)
        end
      end

      # @private
      protected
      def attribute name, options = {}, &block
        attr = Attribute.new(name, options)
        attr.instance_eval(&block) if block_given?
        define_attribute_getter(attr)
        define_attribute_setter(attr) if attr.mutable?
        attributes[attr.name] = attr
      end

      # @private
      protected
      def mutable_attribute name, options = {}, &block
        attribute(name, options.merge(:mutable => true), &block)
      end

      # @private
      protected
      def define_attribute_getter attribute
        define_method(attribute.name) do
          
          return static_attributes[attribute.name] if 
            static_attributes.has_key?(attribute.name)

          begin
            retrieve_attribute(attribute) { get_resource(attribute) }
          rescue Cacheable::NoData => e
            name = ruby_name.tr("_", " ")
            raise NotFound, "unable to find the #{name}"
          end

        end
      end

      # @private
      protected
      def define_attribute_setter attribute
        setter = attribute.name.to_s.sub(/\?/, '') + '='
        define_method(setter) do |value|
          translated_value = attribute.translate_input_value(value)
          update_resource(attribute, translated_value)
          if attribute.static?
            static_attributes[attribute.name] = translated_value
          end
          value
        end
      end

      # @private
      protected
      def populates_from *request_types, &block
        provider = provider(*request_types)
        provider.find(&block)
        provider.provides(*attributes.keys)
        provider
      end

      # @private
      protected
      def provider *request_types, &block
        provider = AttributeProvider.new(self, request_types)
        if block_given?
          yield(provider)
        end
        attribute_providers << provider 
        provider
      end

    end

    # @private
    class Attribute

      def initialize name, options = {}
        @name = name
        @options = options
        @request_types = []
      end

      attr_reader :name

      attr_reader :request_types

      def get_as
        @get_as ||= (@options[:get_as] || @options[:as] || name)
      end

      def set_as
        @set_as ||= (@options[:set_as] || @options[:as] || name)
      end

      def mutable?
        @options[:mutable] == true
      end

      def static?
        @options[:static] == true
      end

      def translates_input &block
        @input_translator = block
      end

      def translates_output options = {}, &block
        @translates_nil = options[:nil]
        @output_translator = block
      end

      def translate_input_value value
        @input_translator ? @input_translator.call(value) : value
      end

      def translate_output_value value

        # by default nil values are not translated
        return nil if value.nil? and @translates_nil != true

        case
        when @options[:to_sym]  then value.tr('-','_').downcase.to_sym
        when @output_translator then @output_translator.call(value)
        else value
        end

      end

    end

    # @private
    class AttributeProvider

      def initialize klass, request_types
        @klass = klass
        @id = klass.attribute_providers.length
        @request_types = request_types
        @provides = {}
      end

      attr_reader :request_types

      def find &block
        @klass.send(:define_method, finder_method, &block)
      end

      def finder_method
        "find_in_response_#{@id}"
      end

      # Indicates that all of the the named attributes can be retrieved
      # from an appropriate response object.
      # 
      # @param [Symbol] attr_names A list of attributes provided
      # @param [Hash] options
      # @option options [Boolean] :value_wrapped (false) If true, then
      #   the value returned by the response object will also receive
      #   the message :value before it is translated and returned.
      # @option options [Symbol] :get_as Defaults to the method named
      #   by the attribute.  This is useful when you have two providers
      #   for the same attribute but their response object name
      #   them differently.
      def provides *attr_names
        options = attr_names.last.is_a?(Hash) ? attr_names.pop : {}
        attr_names.each do |attr_name|
          attr = @klass.attributes[attr_name]
          attr.request_types.push(*request_types)
          @provides[attr_name] = options
        end
      end

      def attributes_from_response resource, response
        if response_object = resource.send(finder_method, response)
          attributes_from_response_object(response_object)
        else
          {}
        end
      end

      def attributes_from_response_object resp_obj
        attributes = {}
        @provides.each do |attr_name, options|

          attr = @klass.attributes[attr_name]
          method = options[:get_as] || attr.get_as

          v = resp_obj.respond_to?(method) ? resp_obj.send(method) : nil
          v = v.value if v and options[:value_wrapped]
          v = attr.translate_output_value(v)

          attributes[attr_name] = v

        end
        attributes
      end

    end
  end
end