#--
# Copyright (c) 2007-2012 Nick Sieger.
# See the file README.txt included with the distribution for
# software license details.
#++

# Concatenate together multiple IO objects into a single, composite IO object
# for purposes of reading as a single stream.
#
# @example
#     crio = CompositeReadIO.new(StringIO.new('one'),
#                                StringIO.new('two'),
#                                StringIO.new('three'))
#     puts crio.read # => "onetwothree"
class CompositeReadIO
  # Create a new composite-read IO from the arguments, all of which should
  # respond to #read in a manner consistent with IO.
  def initialize(*ios)
    @ios = ios.flatten
    @index = 0
  end

  # Read from IOs in order until `length` bytes have been received.
  def read(length = nil, outbuf = nil)
    got_result = false
    outbuf = outbuf ? outbuf.replace("") : ""

    while io = current_io
      if result = io.read(length)
        got_result ||= !result.nil?
        result.force_encoding("BINARY") if result.respond_to?(:force_encoding)
        outbuf << result
        length -= result.length if length
        break if length == 0
      end
      advance_io
    end
    (!got_result && length) ? nil : outbuf
  end

  def rewind
    @ios.each { |io| io.rewind }
    @index = 0
  end

  private

  def current_io
    @ios[@index]
  end

  def advance_io
    @index += 1
  end
end

# Convenience methods for dealing with files and IO that are to be uploaded.
class UploadIO
  attr_reader :content_type, :original_filename, :local_path, :io, :opts

  # Create an upload IO suitable for including in the params hash of a
  # Net::HTTP::Post::Multipart.
  #
  # Can take two forms. The first accepts a filename and content type, and
  # opens the file for reading (to be closed by finalizer).
  #
  # The second accepts an already-open IO, but also requires a third argument,
  # the filename from which it was opened (particularly useful/recommended if
  # uploading directly from a form in a framework, which often save the file to
  # an arbitrarily named RackMultipart file in /tmp).
  #
  # @example
  #     UploadIO.new("file.txt", "text/plain")
  #     UploadIO.new(file_io, "text/plain", "file.txt")
  def initialize(filename_or_io, content_type, filename = nil, opts = {})
    io = filename_or_io
    local_path = ""
    if io.respond_to? :read
      # in Ruby 1.9.2, StringIOs no longer respond to path
      # (since they respond to :length, so we don't need their local path, see parts.rb:41)
      local_path = filename_or_io.respond_to?(:path) ? filename_or_io.path : "local.path"
    else
      io = File.open(filename_or_io)
      local_path = filename_or_io
    end
    filename ||= local_path

    @content_type = content_type
    @original_filename = File.basename(filename)
    @local_path = local_path
    @io = io
    @opts = opts
  end

  def self.convert!(io, content_type, original_filename, local_path)
    raise ArgumentError, "convert! has been removed. You must now wrap IOs " \
                         "using:\nUploadIO.new(filename_or_io, content_type, " \
                         "filename=nil)\nPlease update your code."
  end

  def method_missing(*args)
    @io.send(*args)
  end

  def respond_to?(meth, include_all = false)
    @io.respond_to?(meth, include_all) || super(meth, include_all)
  end
end
