# frozen_string_literal: true

require 'stringio'

# multipart-post gem
require 'composite_io'
require 'parts'

module Faraday
  # Multipart value used to POST a binary data from a file or
  #
  # @example
  #   payload = { file: Faraday::FilePart.new("file_name.ext", "content/type") }
  #   http.post("/upload", payload)
  #

  # @!method initialize(filename_or_io, content_type, filename = nil, opts = {})
  #
  #   @param filename_or_io [String, IO] Either a String filename to a local
  #     file or an open IO object.
  #   @param content_type [String] String content type of the file data.
  #   @param filename [String] Optional String filename, usually to add context
  #     to a given IO object.
  #   @param opts [Hash] Optional Hash of String key/value pairs to describethis
  #     this uploaded file. Expected Header keys include:
  #     * Content-Transfer-Encoding - Defaults to "binary"
  #     * Content-Disposition - Defaults to "form-data"
  #     * Content-Type - Defaults to the content_type argument.
  #     * Content-ID - Optional.
  #
  # @return [Faraday::FilePart]
  #
  # @!attribute [r] content_type
  # The uploaded binary data's content type.
  #
  # @return [String]
  #
  # @!attribute [r] original_filename
  # The base filename, taken either from the filename_or_io or filename
  # arguments in #initialize.
  #
  # @return [String]
  #
  # @!attribute [r] opts
  # Extra String key/value pairs to make up the header for this uploaded file.
  #
  # @return [Hash]
  #
  # @!attribute [r] io
  # The open IO object for the uploaded file.
  #
  # @return [IO]
  FilePart = ::UploadIO

  # Multipart value used to POST a file.
  #
  # @deprecated Use FilePart instead of this class. It behaves identically, with
  #   a matching name to ParamPart.
  UploadIO = ::UploadIO

  Parts = ::Parts

  # Similar to, but not compatible with CompositeReadIO provided by the
  # multipart-post gem.
  # https://github.com/nicksieger/multipart-post/blob/master/lib/composite_io.rb
  class CompositeReadIO
    def initialize(*parts)
      @parts = parts.flatten
      @ios = @parts.map(&:to_io)
      @index = 0
    end

    # @return [Integer] sum of the lengths of all the parts
    def length
      @parts.inject(0) { |sum, part| sum + part.length }
    end

    # Rewind each of the IOs and reset the index to 0.
    #
    # @return [void]
    def rewind
      @ios.each(&:rewind)
      @index = 0
    end

    # Read from IOs in order until `length` bytes have been received.
    #
    # @param length [Integer, nil]
    # @param outbuf [String, nil]
    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&.zero?
        end
        advance_io
      end
      !got_result && length ? nil : outbuf
    end

    # Close each of the IOs.
    #
    # @return [void]
    def close
      @ios.each(&:close)
    end

    def ensure_open_and_readable
      # Rubinius compatibility
    end

    private

    def current_io
      @ios[@index]
    end

    def advance_io
      @index += 1
    end
  end
end
