require 'net/ssh/loggable'

module Net; module SFTP; module Operations

  # A wrapper around an SFTP file handle, that exposes an IO-like interface
  # for interacting with the remote file. All operations are synchronous
  # (blocking), making this a very convenient way to deal with remote files.
  #
  # A wrapper is usually created via the Net::SFTP::Session#file factory:
  #
  #   file = sftp.file.open("/path/to/remote")
  #   puts file.gets
  #   file.close
  class File
    # A reference to the Net::SFTP::Session instance that drives this wrapper
    attr_reader :sftp

    # The SFTP file handle object that this object wraps
    attr_reader :handle

    # The current position within the remote file
    attr_reader :pos

    # Creates a new wrapper that encapsulates the given +handle+ (such as
    # would be returned by Net::SFTP::Session#open!). The +sftp+ parameter
    # must be the same Net::SFTP::Session instance that opened the file.
    def initialize(sftp, handle)
      @sftp     = sftp
      @handle   = handle
      @pos      = 0
      @real_pos = 0
      @real_eof = false
      @buffer   = ""
    end

    # Repositions the file pointer to the given offset (relative to the
    # start of the file). This will also reset the EOF flag.
    def pos=(offset)
      @real_pos = @pos = offset
      @buffer = ""
      @real_eof = false
    end

    # Closes the underlying file and sets the handle to +nil+. Subsequent
    # operations on this object will fail.
    def close
      sftp.close!(handle)
      @handle = nil
    end

    # Returns true if the end of the file has been encountered by a previous
    # read. Setting the current file position via #pos= will reset this
    # flag (useful if the file's contents have changed since the EOF was
    # encountered).
    def eof?
      @real_eof && @buffer.empty?
    end

    # Reads up to +n+ bytes of data from the stream. Fewer bytes will be
    # returned if EOF is encountered before the requested number of bytes
    # could be read. Without an argument (or with a nil argument) all data
    # to the end of the file will be read and returned.
    #
    # This will advance the file pointer (#pos).
    def read(n=nil)
      loop do
        break if n && @buffer.length >= n
        break unless fill
      end

      if n
        result, @buffer = @buffer[0,n], (@buffer[n..-1] || "")
      else
        result, @buffer = @buffer, ""
      end

      @pos += result.length
      return result
    end

    # Reads up to the next instance of +sep_string+ in the stream, and
    # returns the bytes read (including +sep_string+). If +sep_string+ is
    # omitted, it defaults to +$/+. If EOF is encountered before any data
    # could be read, #gets will return +nil+. If the first argument is an
    # integer, or optional second argument is given, the returning string
    # would not be longer than the given value in bytes.
    def gets(sep_or_limit=$/, limit=Float::INFINITY)
      if sep_or_limit.is_a? Integer
        sep_string = $/
        lim = sep_or_limit
      else
        sep_string = sep_or_limit
        lim = limit
      end

      delim = if sep_string && sep_string.length == 0
        "#{$/}#{$/}"
      else
        sep_string
      end

      loop do
        at = @buffer.index(delim) if delim
        if at
          offset = [at + delim.length, lim].min
          @pos += offset
          line, @buffer = @buffer[0,offset], @buffer[offset..-1]
          return line
        elsif lim < @buffer.length
          @pos += lim
          line, @buffer = @buffer[0,lim], @buffer[lim..-1]
          return line
        elsif !fill
          return nil if @buffer.empty?
          @pos += @buffer.length
          line, @buffer = @buffer, ""
          return line
        end
      end
    end

    # Same as #gets, but raises EOFError if EOF is encountered before any
    # data could be read.
    def readline(sep_or_limit=$/, limit=Float::INFINITY)
      line = gets(sep_or_limit, limit)
      raise EOFError if line.nil?
      return line
    end

    # Writes the given data to the stream, incrementing the file position and
    # returning the number of bytes written.
    def write(data)
      data = data.to_s
      sftp.write!(handle, @real_pos, data)
      @real_pos += data.length
      @pos = @real_pos
      data.length
    end

    # Writes each argument to the stream. If +$\+ is set, it will be written
    # after all arguments have been written.
    def print(*items)
      items.each { |item| write(item) }
      write($\) if $\
      nil
    end

    def size
      stat.size
    end

    # Resets position to beginning of file
    def rewind
      self.pos = 0
    end

    # Writes each argument to the stream, appending a newline to any item
    # that does not already end in a newline. Array arguments are flattened.
    def puts(*items)
      items.each do |item|
        if Array === item
          puts(*item)
        else
          write(item)
          write("\n") unless item[-1] == ?\n
        end
      end
      nil
    end

    # Performs an fstat operation on the handle and returns the attribute
    # object (Net::SFTP::Protocol::V01::Attributes, Net::SFTP::Protool::V04::Attributes,
    # or Net::SFTP::Protocol::V06::Attributes, depending on the SFTP protocol
    # version in use).
    def stat
      sftp.fstat!(handle)
    end

    private

      # Fills the buffer. Returns +true+ if it succeeded, and +false+ if
      # EOF was encountered before any data was read.
      def fill
        data = sftp.read!(handle, @real_pos, 8192)

        if data.nil?
          @real_eof = true
          return false
        else
          @real_pos += data.length
          @buffer << data
        end

        !@real_eof
      end
  end

end; end; end
