##
# IO

class IOError < StandardError; end
class EOFError < IOError; end

class IO
  SEEK_SET = 0
  SEEK_CUR = 1
  SEEK_END = 2

  BUF_SIZE = 4096

  def self.open(*args, &block)
    io = self.new(*args)

    return io unless block

    begin
      yield io
    ensure
      begin
        io.close unless io.closed?
      rescue StandardError
      end
    end
  end

  def self.popen(command, mode = 'r', opts={}, &block)
    if !self.respond_to?(:_popen)
      raise NotImplementedError, "popen is not supported on this platform"
    end
    io = self._popen(command, mode, opts)
    return io unless block

    begin
      yield io
    ensure
      begin
        io.close unless io.closed?
      rescue IOError
        # nothing
      end
    end
  end

  def self.pipe(&block)
    if !self.respond_to?(:_pipe)
      raise NotImplementedError, "pipe is not supported on this platform"
    end
    if block
      begin
        r, w = IO._pipe
        yield r, w
      ensure
        r.close unless r.closed?
        w.close unless w.closed?
      end
    else
      IO._pipe
    end
  end

  def self.read(path, length=nil, offset=nil, opt=nil)
    if not opt.nil?        # 4 arguments
      offset ||= 0
    elsif not offset.nil?  # 3 arguments
      if offset.is_a? Hash
        opt = offset
        offset = 0
      else
        opt = {}
      end
    elsif not length.nil?  # 2 arguments
      if length.is_a? Hash
        opt = length
        offset = 0
        length = nil
      else
        offset = 0
        opt = {}
      end
    else                   # only 1 argument
      opt = {}
      offset = 0
      length = nil
    end

    str = ""
    fd = -1
    io = nil
    begin
      if path[0] == "|"
        io = IO.popen(path[1..-1], (opt[:mode] || "r"))
      else
        fd = IO.sysopen(path)
        io = IO.open(fd, opt[:mode] || "r")
      end
      io.seek(offset) if offset > 0
      str = io.read(length)
    ensure
      if io
        io.close
      elsif fd != -1
        IO._sysclose(fd)
      end
    end
    str
  end

  def flush
    # mruby-io always writes immediately (no output buffer).
    raise IOError, "closed stream" if self.closed?
    self
  end

  def write(string)
    str = string.is_a?(String) ? string : string.to_s
    return str.size unless str.size > 0

    len = syswrite(str)
    if len != -1
      @pos += len
      return len
    end

    raise IOError
  end

  def <<(str)
    write(str)
    self
  end

  def eof?
    _check_readable
    begin
      buf = _read_buf
      return buf.size == 0
    rescue EOFError
      return true
    end
  end
  alias_method :eof, :eof?

  def pos
    raise IOError if closed?
    @pos
  end
  alias_method :tell, :pos

  def pos=(i)
    seek(i, SEEK_SET)
  end

  def rewind
    seek(0, SEEK_SET)
  end

  def seek(i, whence = SEEK_SET)
    raise IOError if closed?
    @pos = sysseek(i, whence)
    @buf = ''
    0
  end

  def _read_buf
    return @buf if @buf && @buf.size > 0
    @buf = sysread(BUF_SIZE)
  end

  def ungetc(substr)
    raise TypeError.new "expect String, got #{substr.class}" unless substr.is_a?(String)
    @pos -= substr.size
    if @buf.empty?
      @buf = substr.dup
    else
      @buf = substr + @buf
    end
    nil
  end

  def read(length = nil)
    unless length.nil?
      unless length.is_a? Fixnum
        raise TypeError.new "can't convert #{length.class} into Integer"
      end
      if length < 0
        raise ArgumentError.new "negative length: #{length} given"
      end
      if length == 0
        return ""   # easy case
      end
    end

    array = []
    start_pos = @pos
    while 1
      begin
        _read_buf
      rescue EOFError => e
        array = nil if array.empty? and (not length.nil?) and length != 0
        break
      end

      if length && (@pos - start_pos + @buf.size) >= length
        len = length - (@pos - start_pos)
        array.push @buf[0, len]
        @pos += len
        @buf = @buf[len, @buf.size - len]
        break
      else
        array.push @buf
        @pos += @buf.size
        @buf = ''
      end
    end

    array && array.join
  end

  def readline(arg = $/, limit = nil)
    case arg
    when String
      rs = arg
    when Fixnum
      rs = $/
      limit = arg
    else
      raise ArgumentError
    end

    if rs.nil?
      return read
    end

    if rs == ""
      rs = $/ + $/
    end

    array = []
    start_pos = @pos
    while 1
      begin
        _read_buf
      rescue EOFError => e
        array = nil if array.empty?
        break
      end

      if limit && (@pos - start_pos + @buf.size) >= limit
        len = limit - (@pos - start_pos)
        array.push @buf[0, len]
        @pos += len
        @buf = @buf[len, @buf.size - len]
        break
      elsif idx = @buf.index(rs)
        len = idx + rs.size
        array.push @buf[0, len]
        @pos += len
        @buf = @buf[len, @buf.size - len]
        break
      else
        array.push @buf
        @pos += @buf.size
        @buf = ''
      end
    end

    raise EOFError.new "end of file reached" if array.nil?

    array.join
  end

  def gets(*args)
    begin
      readline(*args)
    rescue EOFError => e
      nil
    end
  end

  def readchar
    _read_buf
    c = @buf[0]
    @buf = @buf[1, @buf.size]
    @pos += 1
    c
  end

  def getc
    begin
      readchar
    rescue EOFError => e
      nil
    end
  end

  # 15.2.20.5.3
  def each(&block)
    while line = self.gets
      block.call(line)
    end
    self
  end

  # 15.2.20.5.4
  def each_byte(&block)
    while char = self.getc
      block.call(char)
    end
    self
  end

  # 15.2.20.5.5
  alias each_line each

  alias each_char each_byte

  def readlines
    ary = []
    while (line = gets)
      ary << line
    end
    ary
  end

  def puts(*args)
    i = 0
    len = args.size
    while i < len
      s = args[i].to_s
      write s
      write "\n" if (s[-1] != "\n")
      i += 1
    end
    write "\n" if len == 0
    nil
  end

  def print(*args)
    i = 0
    len = args.size
    while i < len
      write args[i].to_s
      i += 1
    end
  end

  def printf(*args)
    write sprintf(*args)
    nil
  end

  alias_method :to_i, :fileno
  alias_method :tty?, :isatty
end

STDIN  = IO.open(0, "r")
STDOUT = IO.open(1, "w")
STDERR = IO.open(2, "w")

$stdin  = STDIN
$stdout = STDOUT
$stderr = STDERR

module Kernel
  def print(*args)
    $stdout.print(*args)
  end

  def puts(*args)
    $stdout.puts(*args)
  end

  def printf(*args)
    $stdout.printf(*args)
  end

  def gets(*args)
    $stdin.gets(*args)
  end

  def getc(*args)
    $stdin.getc(*args)
  end
end
