# coding: UTF-8

require 'paperclip'
require 'aws-sdk'
require 'uri'

module Paperclip
  module Storage
    module Aws
      def self.extended base
        begin
          require 'aws-sdk'
        rescue LoadError => e
          e.message << " (You may need to install the aws-sdk gem)"
          raise e
        end unless defined?(AWS::Core)
        
        attr_accessor :s3_credentials, :s3_bucket, :s3_permissions, :s3_options, :s3_protocol, :s3_host_alias
        
        base.instance_eval do   
          self.setup_s3_credentials
          self.setup_s3_bucket                 
          self.setup_s3_permissions          
          self.setup_s3_protocol
          self.setup_s3_options
          self.setup_s3_host_alias
        end        
      end
      
      def s3
        @s3 ||= AWS::S3.new(
          :access_key_id => @s3_credentials[:access_key_id],
          :secret_access_key => @s3_credentials[:secret_access_key],
          :s3_endpoint => @s3_endpoint
        )        
      end
      
      def url(style=default_style, options={})
        if self.original_filename.nil? 
          # default_url = @default_url.is_a?(Proc) ? @default_url.call(self) : @default_url
          # return interpolate(default_url, style)          
          return super
        end
        
        if options[:expires].present? || options[:action].present?
          options.reverse_merge!({
            :expires => 60*60,
            :action => :read
          })          
          secure = ( self.choose_protocol(style, options) == 'https:' )
          return self.s3.buckets[@s3_bucket].objects[path(style).gsub(%r{^/}, "")].url_for(options[:action], {  :secure => secure, :expires => options[:expires] }).to_s
        else
          if @s3_host_alias.present?
            url = "#{choose_protocol(style, options)}//#{@s3_host_alias}/#{path(style).gsub(%r{^/}, "")}"
          else
            url = "#{choose_protocol(style, options)}//#{@s3_endpoint}/#{@s3_bucket}/#{path(style).gsub(%r{^/}, "")}"
          end    
          return URI.escape(url)
        end              
      end
      
      def bucket
        @bucket ||= self.s3.buckets[@s3_bucket]
      end
      
      def s3_bucket=(value)
        @bucket = nil
                
        @s3_bucket = value
        @s3_bucket = @s3_bucket.call(self) if @s3_bucket.is_a?(Proc)
      end
      
      def versions(style = default_style)
        begin
          return nil unless self.bucket.versioning_enabled?
          self.bucket.objects[path(style)].versions          
        rescue AWS::S3::Errors::Base => e
          log "ERROR: Error occured while fetching versions for `#{path(style)}`. Probably versions are disabled or suspended for `#{self.bucket.name}` bucket."
          return nil
        end
      end
            
      def exists?(style = default_style)
        if path(style).nil? || path(style).to_s.strip == ""
          return false
        end
        begin
          return self.bucket.objects[path(style)].exists?
        rescue AWS::S3::Errors::Base
          return false
        end
      end

      def choose_protocol(style, options={})
        protocol = if options[:protocol].present?
          options[:protocol].to_s
        else
          @s3_protocol.is_a?(Proc) ? @s3_protocol.call(style, self) : @s3_protocol
        end

        "#{protocol}:" if protocol.present?
      end

      # Returns representation of the data of the file assigned to the given
      # style, in the format most representative of the current storage.
      def to_file(style = default_style)
        return @queued_for_write[style] if @queued_for_write[style]
        filename = path(style)
        extname  = File.extname(filename)
        basename = File.basename(filename, extname)
        file = Tempfile.new([basename, extname])
        file.binmode
        file.write(self.bucket.objects[path(style)].read)
        file.rewind
        return file
      end

      def create_bucket
        self.s3.buckets.create(@s3_bucket)
      end

      def flush_writes #:nodoc:
        @queued_for_write.each do |style, file|
          begin
            log "Saving #{path(style)}"
            
            self.s3.buckets[@s3_bucket].objects[path(style)].write({
              :file => file.path,
              :acl => @s3_permissions[:style.to_sym] || @s3_permissions[:default],
              :content_type => file.content_type.to_s.strip
            }.reverse_merge(@s3_options))
          rescue AWS::S3::Errors::NoSuchBucket => e
            create_bucket
            retry
          rescue AWS::Errors::Base => e
            raise
          end
        end
        @queued_for_write = {}
      end

      def flush_deletes #:nodoc:
        @queued_for_delete.each do |path|
          begin
            log "Deleting #{path}"
            self.s3.buckets[@s3_bucket].objects[path].delete
          rescue AWS::Errors::Base => e
            raise
          end
        end
        @queued_for_delete = []
      end

      def copy_to_local_file(style, local_dest_path)
        log("copying #{path(style)} to local file #{local_dest_path}")
        local_file = ::File.open(local_dest_path, 'wb')

        file = self.s3.buckets[@s3_bucket].objects[self.path(style)]
        local_file.write(file.read)
        local_file.close
      rescue AWS::Errors::Base => e
        warn("#{e} - cannot copy #{path(style)} to local file #{local_dest_path}")
        false
      end
      
    # PRIVATE METHODS        
      def setup_s3_credentials
        if @options[:s3_credentials].present?
          @s3_credentials = self.parse_s3_credentials(@options[:s3_credentials]).stringify_keys
          env = Object.const_defined?(:Rails) ? Rails.env : nil
          
          @s3_credentials = (@s3_credentials[env] || @s3_credentials).symbolize_keys              
          @s3_endpoint = @s3_credentials[:endpoint] || 's3.amazonaws.com'
        else
          raise ArgumentError, "missing required :s3_credentials option"
        end
      end
      protected :setup_s3_credentials
      
      def parse_s3_credentials(creds)
        case creds
        when File
          YAML::load(ERB.new(File.read(creds.path)).result)
        when String, Pathname
          YAML::load(ERB.new(File.read(creds)).result)
        when Hash
          creds
        else
          raise ArgumentError, "Credentials are not a path, file, or hash."
        end
      end        
      protected :parse_s3_credentials
      
      def setup_s3_bucket
        if @options[:bucket].present?
          log "DEPRECATION WARNING: Passing  `:bucket` options to paperclip is deprecated. Please use `:s3_bucket` instead."
        end
        
        if @options[:s3_bucket].present? || @s3_credentials[:bucket].present? || @options[:bucket].present?
          self.s3_bucket = @options[:s3_bucket] || @s3_credentials[:bucket] || @options[:bucket]     
        else
          raise ArgumentError, "missing required :s3_bucket option"
        end
      end
      protected :setup_s3_bucket
      
      def setup_s3_permissions
        @s3_permissions = self.parse_s3_permissions(@options[:s3_permissions])
      end
      protected :setup_s3_permissions
      
      def parse_s3_permissions(permissions)
        if permissions.is_a?(Hash)
          permissions[:default] = permissions[:default] || :public_read
        else
          permissions = { :default => permissions || :public_read }
        end
        permissions                
      end
      protected :parse_s3_permissions
      
      def setup_s3_protocol
        @s3_protocol = @options[:s3_protocol] ||
          Proc.new do |style, attachment|
            permission  = (@s3_permissions[style.to_sym] || @s3_permissions[:default])
            permission  = permission.call(attachment, style) if permission.is_a?(Proc)
            (permission == :public_read) ? 'http' : 'https'
          end                  
      end
      protected :setup_s3_protocol
        
      def setup_s3_options
        @s3_options     = (@options[:s3_options] || {}).symbolize_keys
        
        # setup Amazon Server Side encryption
        @s3_options.reverse_merge!({
          :sse => false,
          :storage_class => :standard,
          :content_disposition => nil
        })
        @s3_options[:server_side_encryption] ||= @s3_options.delete(:sse)                            
      end
      protected :setup_s3_options
      
      
      def setup_s3_host_alias
        @s3_host_alias = @options[:s3_host_alias]
        @s3_host_alias = @s3_host_alias.call(self) if @s3_host_alias.is_a?(Proc)                            
      end
      protected :setup_s3_host_alias
    end
  end
end
