# MacRuby implementation of stringio.
#
# This file is covered by the Ruby license. See COPYING for more details.
#
# Copyright (C) 2012, The MacRuby Team. All rights reserved.
# Copyright (C) 2009-2011, Apple Inc. All rights reserved.

class StringIO

  attr_reader :string, :pos

  #   strio.lineno    -> integer
  #
  # Returns the current line number in *strio*. The stringio must be
  # opened for reading. +lineno+ counts the number of times  +gets+ is
  # called, rather than the number of newlines  encountered. The two
  # values will differ if +gets+ is  called with a separator other than
  # newline.  See also the  <code>$.</code> variable.
  #
  #
  #   strio.lineno = integer    -> integer
  #
  # Manually sets the current line number to the given value.
  # <code>$.</code> is updated only on the next read.
  #
  attr_accessor :lineno

  include Enumerable

  #    StringIO.open(string=""[, mode]) {|strio| ...}
  #
  # Equivalent to StringIO.new except that when it is called with a block, it
  # yields with the new instance and closes it, and returns the result which
  # returned from the block.
  #
  def self.open(*args)
    obj = new(*args)

    if block_given?
      begin
        yield obj
      ensure
        obj.close
        obj.instance_variable_set(:@string, nil)
        obj
      end
    else
      obj
    end
  end

  # StringIO.new(string=""[, mode])
  #
  # Creates new StringIO instance from with _string_ and _mode_.
  #
  def initialize(string = String.new, mode = nil)
    @string = string.to_str
    @pos = 0
    @lineno = 0
    define_mode(mode)

    raise Errno::EACCES if (@writable && string.frozen?)
    self
  end

  def initialize_copy(from)
    from = from.to_strio
    self.taint if from.tainted?

    @string   = from.instance_variable_get(:@string).dup
    # mode
    @append   = from.instance_variable_get(:@append)
    @readable = from.instance_variable_get(:@readable)
    @writable = from.instance_variable_get(:@writable)

    @pos = from.instance_variable_get(:@pos)
    @lineno = from.instance_variable_get(:@lineno)

    self
  end

  #   strio.reopen(other_StrIO)     -> strio
  #   strio.reopen(string, mode)    -> strio
  #
  # Reinitializes *strio* with the given <i>other_StrIO</i> or _string_
  # and _mode_ (see StringIO#new).
  #
  def reopen(str=nil, mode=nil)
    if str == nil && mode == nil
      mode = 'w+'
    elsif !str.kind_of?(String) && mode == nil
      raise TypeError unless str.respond_to?(:to_strio)
      return initialize_copy(str)
    else
      raise TypeError unless str.respond_to?(:to_str)
      @string = str.to_str
    end

    define_mode(mode)
    @pos = 0
    @lineno = 0

    self
  end

  #   strio.string = string  -> string
  #
  # Changes underlying String object, the subject of IO.
  #
  def string=(str)
    @string = str.to_str
    @pos = 0
    @lineno = 0
  end

  #   strio.rewind    -> 0
  #
  # Positions *strio* to the beginning of input, resetting
  # +lineno+ to zero.
  #
  def rewind
    @pos = 0
    @lineno = 0
  end

  #   strio.read([length [, buffer]])    -> string, buffer, or nil
  #
  # See IO#read.
  #
  def read(length = nil, buffer = String.new)
    raise IOError, "not opened for reading" unless @readable
    raise TypeError unless buffer.respond_to?(:to_str)
    buffer = buffer.to_str

    if length.nil?
      return buffer.replace("") if self.eof?
      buffer.replace(@string[@pos..-1])
      @pos = @string.size
    else
      raise TypeError unless length.respond_to?(:to_int)
      length = length.to_int
      raise ArgumentError if length < 0

      if length == 0
        buffer.replace("")
      else
        if self.eof?
          buffer.replace("")
          return nil
        end
        buffer.replace(@string[@pos, length])
        @pos += buffer.length
      end
      buffer.force_encoding('BINARY')
   end


    buffer
  end

  #   strio.sysread(integer[, outbuf])    -> string
  #
  # Similar to #read, but raises +EOFError+ at end of string instead of
  # returning +nil+, as well as IO#sysread does.
  def sysread(length = nil, buffer = String.new)
    val = read(length, buffer)
    ( buffer.clear && raise(IO::EOFError, "end of file reached")) if val == nil
    val
  end
  alias_method :readpartial, :sysread
  alias_method :read_nonblock, :sysread

  #   strio.readbyte   -> fixnum
  #
  # See IO#readbyte.
  #
  def readbyte
    raise(IO::EOFError, "end of file reached") if eof?
    getbyte
  end

  #   strio.seek(amount, whence=SEEK_SET) -> 0
  #
  # Seeks to a given offset _amount_ in the stream according to
  # the value of _whence_ (see IO#seek).
  #
  def seek(offset, whence = ::IO::SEEK_SET)
    raise(IOError, "closed stream") if closed?
    raise TypeError unless offset.respond_to?(:to_int)
    offset = offset.to_int

    case whence
    when ::IO::SEEK_CUR
      # Seeks to offset plus current position
      offset += @pos
    when ::IO::SEEK_END
      # Seeks to offet plus end of stream (usually offset is a negative value)
      offset += @string.size
    when ::IO::SEEK_SET, nil
      # Seeks to the absolute location given by offset
    else
      raise Errno::EINVAL, "invalid whence"
    end

    raise Errno::EINVAL if (offset < 0)
    @pos = offset

    0
  end

  #   strio.pos = integer    -> integer
  #
  # Seeks to the given position (in bytes) in *strio*.
  #
  def pos=(pos)
    raise Errno::EINVAL if pos < 0
    @pos = pos
  end

  #   strio.closed?    -> true or false
  #
  # Returns +true+ if *strio* is completely closed, +false+ otherwise.
  #
  def closed?
    !@readable && !@writable
  end

  #   strio.close  -> nil
  #
  # Closes strio.  The *strio* is unavailable for any further data
  # operations; an +IOError+ is raised if such an attempt is made.
  #
  def close
    raise(IOError, "closed stream") if closed?
    @readable = @writable = nil
  end

  #   strio.closed_read?    -> true or false
  #
  # Returns +true+ if *strio* is not readable, +false+ otherwise.
  #
  def closed_read?
    !@readable
  end

  #   strio.close_read    -> nil
  #
  # Closes the read end of a StringIO.  Will raise an +IOError+ if the
  # *strio* is not readable.
  #
  def close_read
    raise(IOError, "closing non-duplex IO for reading") unless @readable
    @readable = nil
  end

  #   strio.closed_write?    -> true or false
  #
  # Returns +true+ if *strio* is not writable, +false+ otherwise.
  #
  def closed_write?
    !@writable
  end

  #   strio.eof     -> true or false
  #   strio.eof?    -> true or false
  #
  # Returns true if *strio* is at end of file. The stringio must be
  # opened for reading or an +IOError+ will be raised.
  #
  def eof?
    raise(IOError, "not opened for reading") unless @readable
    @pos >= @string.length
  end
  alias_method :eof, :eof?

  def binmode
    self
  end

  def fcntl
    raise NotImplementedError, "StringIO#fcntl is not implemented"
  end

  def flush
    self
  end

  def fsync
    0
  end

  # strio.pid -> nil
  #
  def pid
    nil
  end

  #   strio.sync    -> true
  #
  # Returns +true+ always.
  #
  def sync
    true
  end

  #    strio.sync = boolean -> boolean
  #
  def sync=(value)
    value
  end

  def tell
    @pos
  end

  # strio.fileno -> nil
  #
  def fileno
    nil
  end

  #   strio.isatty -> nil
  #   strio.tty? -> nil
  def isatty
    false
  end
  alias_method :tty?, :isatty

  def length
    @string.length
  end
  alias_method :size, :length

  #   strio.getc   -> string or nil
  #
  # Gets the next character from io.
  # Returns nil if called at end of file
  def getc
    raise(IOError, "not opened for reading") unless @readable
    return nil if eof?
    result = @string[@pos]
    @pos += 1
    result
  end

  #   strio.ungetc(string)   -> nil
  #
  # Pushes back one character (passed as a parameter) onto *strio*
  # such that a subsequent buffered read will return it.  Pushing back
  # behind the beginning of the buffer string is not possible.  Nothing
  # will be done if such an attempt is made.
  # In other case, there is no limitation for multiple pushbacks.
  #
  def ungetc(chars)
    raise(IOError, "not opened for reading") unless @readable
    return nil if chars == nil

    chars = chars.chr if chars.kind_of?(Fixnum)
    raise TypeError unless chars.respond_to?(:to_str)
    chars = chars.to_str

    if @pos == 0
      @string = chars + @string
    elsif @pos > 0
      @pos -= 1
      @string[@pos] = chars
    end

    nil
  end

  #   strio.ungetbyte(fixnum)   -> nil
  #
  # See IO#ungetbyte
  #
  def ungetbyte(bytes)
    raise(IOError, "not opened for reading") unless @readable
    return nil if bytes == nil

    bytes = bytes.chr if bytes.kind_of?(Fixnum)
    raise TypeError unless bytes.respond_to?(:to_str)
    bytes = bytes.to_str

    if @pos == 0
      @string = bytes + @string
    elsif @pos > 0
      @pos -= 1
      @string[@pos] = bytes
    end

    nil
  end

  #   strio.readchar   -> fixnum
  #
  # See IO#readchar.
  #
  def readchar
    raise(IO::EOFError, "end of file reached") if eof?
    getc
  end

  #   strio.each_char {|char| block }  -> strio
  #
  # See IO#each_char.
  #
  def each_char
    raise(IOError, "not opened for reading") unless @readable
    if block_given?
      @string.each_char{|c| yield(c)}
      self
    else
      @string.each_char
    end
  end
  alias_method :chars, :each_char

  #   strio.getbyte   -> fixnum or nil
  #
  # See IO#getbyte.
  def getbyte
    raise(IOError, "not opened for reading") unless @readable
    # Because we currently don't support bytes access
    # the following code isn't used
    # instead we are dealing with chars
    result = @string.bytes.to_a[@pos]
    @pos += 1 unless eof?
    result
    # getc
  end

  #   strio.each_byte {|byte| block }  -> strio
  #
  # See IO#each_byte.
  #
  def each_byte
    raise(IOError, "not opened for reading") unless @readable
    return self if (@pos > @string.length)
    if block_given?
      @string.each_byte{|b| @pos += 1; yield(b)}
      self
    else
      @string.each_byte
    end
  end
  alias_method :bytes, :each_byte


  #   strio.each(sep=$/) {|line| block }         -> strio
  #   strio.each(limit) {|line| block }          -> strio
  #   strio.each(sep, limit) {|line| block }     -> strio
  #   strio.each_line(sep=$/) {|line| block }    -> strio
  #   strio.each_line(limit) {|line| block }     -> strio
  #   strio.each_line(sep,limit) {|line| block } -> strio
  #
  # See IO#each.
  #
  def each(sep=$/, limit=nil)
    if block_given?
      raise(IOError, "not opened for reading") unless @readable
      sep, limit = getline_args(sep, limit)
      if limit == 0
        raise ArgumentError, "invalid limit: 0 for each and family"
      end
      while line = getline(sep, limit)
        yield(line)
      end
      self
    else
      to_enum(:each, sep, limit)
    end
  end
  alias_method :each_line, :each
  alias_method :lines, :each

  #   strio.gets(sep=$/)     -> string or nil
  #   strio.gets(limit)      -> string or nil
  #   strio.gets(sep, limit) -> string or nil
  #
  # See IO#gets.
  #
  def gets(sep=$/, limit=nil)
    sep, limit = getline_args(sep, limit)
    $_ = getline(sep, limit)
  end

  #   strio.readline(sep=$/)     -> string
  #   strio.readline(limit)      -> string or nil
  #   strio.readline(sep, limit) -> string or nil
  #
  # See IO#readline.
  def readline(sep=$/, limit=nil)
    raise(IO::EOFError, 'end of file reached') if eof?
    sep, limit = getline_args(sep, limit)
    $_ = getline(sep, limit)
  end

  #   strio.readlines(sep=$/)    ->   array
  #   strio.readlines(limit)     ->   array
  #   strio.readlines(sep,limit) ->   array
  #
  # See IO#readlines.
  #
  def readlines(sep=$/, limit=nil)
    raise IOError, "not opened for reading" unless @readable
    ary = []
    sep, limit = getline_args(sep, limit)
    if limit == 0
      raise ArgumentError, "invalid limit: 0 for readlines"
    end
    while line = getline(sep, limit)
      ary << line
    end
    ary
  end


  #   strio.write(string)    -> integer
  #   strio.syswrite(string) -> integer
  #
  # Appends the given string to the underlying buffer string of *strio*.
  # The stream must be opened for writing.  If the argument is not a
  # string, it will be converted to a string using <code>to_s</code>.
  # Returns the number of bytes written.  See IO#write.
  #
  def write(str)
    raise(IOError, "not opened for writing") unless @writable
    raise(IOError, "not modifiable string") if @string.frozen?

    str = str.to_s
    return 0 if str.empty?

    if @append || (@pos >= @string.length)
      # add padding in case it's needed
      str = str.rjust((@pos + str.length) - @string.length, "\000") if (@pos > @string.length)
      enc1, enc2 = str.encoding, @string.encoding
      if enc1 != enc2
        str = str.dup.force_encoding(enc2)
      end
      @string << str
      @pos = @string.length
    else
      @string[@pos, str.length] = str
      @pos += str.length
      @string.taint if str.tainted?
    end

    str.length
  end
  alias_method :syswrite, :write
  alias_method :write_nonblock, :write

  #   strio << obj     -> strio
  #
  # See IO#<<.
  #
  def <<(str)
    self.write(str)
    self
  end


  def close_write
    raise(IOError, "closing non-duplex IO for writing") unless @writable
    @writable = nil
  end

  #   strio.truncate(integer)    -> 0
  #
  # Truncates the buffer string to at most _integer_ bytes. The *strio*
  # must be opened for writing.
  #
  def truncate(len)
    raise(IOError, "closing non-duplex IO for writing") unless @writable
    raise(TypeError) unless len.respond_to?(:to_int)
    length = len.to_int
    raise(Errno::EINVAL, "negative length") if (length < 0)
    if length < @string.size
      @string[length .. @string.size] = ""
    else
      @string = @string.ljust(length, "\000")
    end
    # send back what was passed, not our :to_int version
    len
  end

  #   strio.puts(obj, ...)    -> nil
  #
  #  Writes the given objects to <em>strio</em> as with
  #  <code>IO#print</code>. Writes a record separator (typically a
  #  newline) after any that do not already end with a newline sequence.
  #  If called with an array argument, writes each element on a new line.
  #  If called without arguments, outputs a single record separator.
  #
  #     io.puts("this", "is", "a", "test")
  #
  #  <em>produces:</em>
  #
  #     this
  #     is
  #     a
  #     test
  #
  def puts(*args)
    if args.empty?
      write("\n")
    else
      args.each do |arg|
        if arg == nil
          line = "nil"
        else
          begin
            arg = arg.to_ary
            recur = false
            arg.each do |a|
              if arg == a
                recur = true
                break
              else
                puts a
              end
            end
            next unless recur
            line = '[...]'
          rescue
            line = arg.to_s
          end
        end

        write(line)
        write("\n") if line[-1] != ?\n
      end
    end

    nil
  end


  #   strio.putc(obj)    -> obj
  #
  #  If <i>obj</i> is <code>Numeric</code>, write the character whose
  #  code is <i>obj</i>, otherwise write the first character of the
  #  string representation of  <i>obj</i> to <em>strio</em>.
  #
  def putc(obj)
    raise(IOError, "not opened for writing") unless @writable

    if obj.is_a?(String)
      char = obj[0]
    else
      raise TypeError unless obj.respond_to?(:to_int)
      char = obj.to_int % 256
    end

    if @append || @pos == @string.length
      @string << char
      @pos = @string.length
    elsif @pos > @string.length
      @string = @string.ljust(@pos, "\000")
      @string << char
      @pos = @string.length
    else
      @string[@pos] = ("" << char)
      @pos += 1
    end

    obj
  end


  #     strio.print()             => nil
  #     strio.print(obj, ...)     => nil
  #
  #  Writes the given object(s) to <em>strio</em>. The stream must be
  #  opened for writing. If the output record separator (<code>$\\</code>)
  #  is not <code>nil</code>, it will be appended to the output. If no
  #  arguments are given, prints <code>$_</code>. Objects that aren't
  #  strings will be converted by calling their <code>to_s</code> method.
  #  With no argument, prints the contents of the variable <code>$_</code>.
  #  Returns <code>nil</code>.
  #
  #     io.print("This is ", 100, " percent.\n")
  #
  #  <em>produces:</em>
  #
  #     This is 100 percent.
  #
  def print(*args)
    raise IOError, "not opened for writing" unless @writable
    args << $_ if args.empty?
    args.map! { |x| (x == nil) ? "nil" : x }
    write((args << $\).flatten.join)
    nil
  end

  #     printf(strio, string [, obj ... ] )    => nil
  #
  #  Equivalent to:
  #     strio.write(sprintf(string, obj, ...)
  #
  def printf(*args)
    raise IOError, "not opened for writing" unless @writable

    if args.size > 1
      write(args.shift % args)
    else
      write(args.first)
    end

    nil
  end

  def external_encoding
    @string ? @string.encoding : nil
  end

  def internal_encoding
    nil
  end

  def set_encoding(enc)
    @string = @string.encode(enc)
    self
  end

  protected

    # meant to be overwritten by developers
    def to_strio
      self
    end

    def define_mode(mode=nil)
      if mode == nil
        # default modes
        @string.frozen? ? set_mode_from_string("r") : set_mode_from_string("r+")
      elsif mode.is_a?(Integer)
        set_mode_from_integer(mode)
      else
        mode = mode.to_str
        set_mode_from_string(mode)
      end
    end

    def set_mode_from_string(mode)
      @readable = @writable = @append = false

      case mode
      when "r", "rb"
        @readable = true
      when "r+", "rb+"
        raise(Errno::EACCES) if @string.frozen?
        @readable = true
        @writable = true
      when "w", "wb"
        @string.frozen? ? raise(Errno::EACCES) : @string.replace("")
        @writable = true
      when "w+", "wb+"
        @string.frozen? ? raise(Errno::EACCES) : @string.replace("")
        @readable = true
        @writable = true
      when "a", "ab"
        raise(Errno::EACCES) if @string.frozen?
        @writable = true
        @append = true
      when "a+", "ab+"
        raise(Errno::EACCES) if @string.frozen?
        @readable = true
        @writable = true
        @append = true
      end
    end

    def set_mode_from_integer(mode)
      @readable = @writable = @append = false

      case mode & (IO::RDONLY | IO::WRONLY | IO::RDWR)
      when IO::RDONLY
        @readable = true
        @writable = false
      when IO::WRONLY
        @readable = false
        @writable = true
        raise(Errno::EACCES) if @string.frozen?
      when IO::RDWR
        @readable = true
        @writable = true
        raise(Errno::EACCES) if @string.frozen?
      end

      @append = true if (mode & IO::APPEND) != 0
      raise(Errno::EACCES) if @append && @string.frozen?
      @string.replace("") if (mode & IO::TRUNC) != 0
    end

    def getline(sep = $/, lim = nil)
      raise(IOError, "not opened for reading") unless @readable
      return nil if eof?

      offset = limit = -1
      if lim != nil
        limit  = lim - 1
        offset = @pos + limit
      end

      if lim != nil && lim == 0
        line = ""
      elsif sep == nil
        line = @string[@pos .. offset]
      elsif sep.empty?
        while @string[@pos] == ?\n
          @pos += 1
          limit -= 1
        end

        if stop = @string.index("\n\n", @pos)
          stop += 1
          line = @string[@pos .. stop]
          if lim != nil && line[limit, 2] != "\n\n"
            line = line[0 .. limit]
          end
        else
          line = @string[@pos .. offset]
        end
      else
        if stop = @string.index(sep, @pos)
          line = @string[@pos .. (stop + sep.size - 1)]
        else
          line = @string[@pos .. offset]
        end
      end
      @pos += line.size
      @lineno += 1

      line
    end

    def getline_args(sep = $/, lim = nil)
      if lim == nil && !sep.kind_of?(String)
        if !sep.respond_to?(:to_str)
          lim = sep
          sep = nil
        end
      end

      if lim != nil
        raise TypeError unless lim.respond_to?(:to_int)
        lim = lim.to_int
      end
      sep = sep.to_str unless (sep == nil)

      return sep, lim
    end

end
