# 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/meta_utils'
require 'aws/inflection'
require 'rexml/text'

module AWS
  class S3

    # Common methods for AccessControlList and related objects.
    module ACLObject

      # @private
      def initialize(opts = {}); end

      # @private
      def body_xml; ""; end

      # @private
      def stag
        element_name
      end

      # @private
      def element_name
        self.class.name[/::([^:]*)$/, 1]
      end

      # Returns the XML representation of the object.  Generally
      # you'll want to call this on an AccessControlList object,
      # which will yield an XML representation of the ACL that you
      # can send to S3.
      def to_s
        if body_xml.empty?
          "<#{stag}/>"
        else
          "<#{stag}>#{body_xml}</#{element_name}>"
        end
      end

      # (see #to_s)
      def to_xml
        to_s
      end

      # Returns true if and only if this object is valid according
      # to S3's published ACL schema.  In particular, this will
      # check that all required attributes are provided and that
      # they are of the correct type.
      def valid?
        validate!
      rescue => e
        false
      else
        true
      end

      # Raises an exception unless this object is valid according to
      # S3's published ACL schema.
      # @see #valid?
      def validate!; end

      # @private
      def validate_input(name, value, context = nil)
        send("validate_#{name}_input!", value, context)
      end

      # @private
      module ClassMethods

        def string_attr(element_name, options = {})
          method_name = options[:method_name] ||
            Inflection.ruby_name(element_name)

          attr_accessor(method_name)
          setter_option(method_name)
          string_input_validator(method_name)
          validate_string(method_name) if options[:required]
          body_xml_string_content(element_name, method_name)
        end

        def object_attr(klass, options = {})
          base_name = klass.name[/::([^:]*)$/, 1]
          method_name = Inflection.ruby_name(base_name)
          cast = options[:cast] || Hash

          attr_reader(method_name)
          setter_option(method_name)
          object_setter(klass, method_name, cast)
          object_input_validator(klass, base_name, method_name, cast)
          validate_object(method_name) if options[:required]
          body_xml_content(method_name)
        end

        def object_list_attr(list_element, klass, options = {})
          base_name = klass.name[/::([^:]*)$/, 1]
          method_name = Inflection.ruby_name(options[:method_name].to_s || list_element)
          default_value = nil
          default_value = [] if options[:required]

          attr_reader(method_name)
          setter_option(method_name) { [] if options[:required] }
          object_list_setter(klass, method_name)
          object_list_input_validator(klass, base_name, method_name)
          validate_list(method_name)
          body_xml_list_content(list_element, method_name)
        end

        def setter_option(method_name)
          MetaUtils.class_extend_method(self, :initialize) do |*args|
            opts = args.last || {}
            instance_variable_set("@#{method_name}", yield) if block_given?
            key = method_name.to_sym

            if opts.has_key?(key)
              value = opts[key]
              validate_input(method_name, value, "for #{method_name} option")
              self.send("#{method_name}=", value)
            end
            super(opts)
          end
        end

        def string_input_validator(method_name)
          input_validator(method_name) do |value, context|
            raise ArgumentError.new("expected string"+context) unless
              value.respond_to?(:to_str)
          end
        end

        def object_input_validator(klass, base_name, method_name, cast)
          input_validator(method_name) do |value, context|
            if value.kind_of?(cast)
              klass.new(value).validate!
            else
              raise ArgumentError.new("expected #{base_name} object or hash"+context) unless
                value.nil? or value.kind_of? klass
            end
          end
        end

        def object_list_input_validator(klass, base_name, method_name)
          input_validator(method_name) do |value, context|
            raise ArgumentError.new("expected array"+context) unless value.kind_of?(Array)
            value.each do |member|
              if member.kind_of?(Hash)
                klass.new(member).validate!
              else
                raise ArgumentError.new("expected array#{context} "+
                                        "to contain #{base_name} objects "+
                                        "or hashes") unless
                  member.kind_of? klass
              end
            end
          end
        end

        def input_validator(method_name, &blk)
          validator = "__validator__#{blk.object_id}"
          MetaUtils.class_extend_method(self, validator, &blk)
          MetaUtils.class_extend_method(self, "validate_#{method_name}_input!") do |*args|
            (value, context) = args
            context = " "+context if context
            context ||= ""
            send(validator, value, context)
          end
        end

        def object_setter(klass, method_name, cast)
          define_method("#{method_name}=") do |value|
            validate_input(method_name, value)
            if value.kind_of?(cast)
              value = klass.new(value)
            end
            instance_variable_set("@#{method_name}", value)
          end
        end

        def object_list_setter(klass, method_name)
          define_method("#{method_name}=") do |value|
            validate_input(method_name, value)
            list = value.map do |member|
              if member.kind_of?(Hash)
                klass.new(member)
              else
                member
              end
            end
            instance_variable_set("@#{method_name}", list)
          end
        end

        def validate_string(method_name)
          MetaUtils.class_extend_method(self, :validate!) do
            super()
            raise "missing #{method_name}" unless send(method_name)
          end
        end

        def validate_object(method_name)
          MetaUtils.class_extend_method(self, :validate!) do
            super()
            raise "missing #{method_name}" unless send(method_name)
            send(method_name).validate!
          end
        end

        def validate_list(method_name)
          MetaUtils.class_extend_method(self, :validate!) do
            super()
            raise "missing #{method_name}" unless send(method_name)
            send(method_name).each { |member| member.validate! }
          end
        end

        def body_xml_string_content(element_name, method_name)
          add_xml_child(method_name) do |value|
            normalized = REXML::Text.normalize(value.to_s)
            "<#{element_name}>#{normalized}</#{element_name}>"
          end
        end

        def body_xml_content(method_name)
          add_xml_child(method_name) { |value| value.to_s }
        end

        def body_xml_list_content(list_element, method_name)
          add_xml_child(method_name) do |list|
            list_content = list.map { |member| member.to_s }.join
            if list_content.empty?
              "<#{list_element}/>"
            else
              "<#{list_element}>#{list_content}</#{list_element}>"
            end
          end
        end

        def add_xml_child(method_name)
          MetaUtils.class_extend_method(self, :body_xml) do
            xml = super()
            value = send(method_name)
            xml += yield(value) if value
            xml
          end
        end

      end

      def self.included(m)
        m.extend(ClassMethods)
      end

    end
  end
end