-- shamelessly stolen from plan9k

buffer = {}

function buffer.new(mode, stream) -- string table -- table -- create a new buffer in mode *mode* backed by stream object *stream*
 local result = {
  mode = {},
  stream = stream,
  bufferRead = "",
  bufferWrite = "",
  bufferSize = math.max(512, math.min(8 * 1024, computer.freeMemory() / 8)),
  bufferMode = "full",
  readTimeout = math.huge
 }
 mode = mode or "r"
 for i = 1, unicode.len(mode) do
  result.mode[unicode.sub(mode, i, i)] = true
 end
 local metatable = {
  __index = buffer,
  __metatable = "file"
 }
 return setmetatable(result, metatable)
end

local function badFileDescriptor()
 return nil, "bad file descriptor"
end

function buffer:close()
 if self.mode.w or self.mode.a then
  self:flush()
 end
 self.closed = true
 return self.stream:close()
end

function buffer:flush()
 local result, reason = self.stream:write(self.bufferWrite)
 if result then
  self.bufferWrite = ""
 else
  if reason then
   return nil, reason
  else
   return badFileDescriptor()
  end
 end

 return self
end

function buffer:lines(...)
 local args = table.pack(...)
 return function()
  local result = table.pack(self:read(table.unpack(args, 1, args.n)))
  if not result[1] and result[2] then
   error(result[2])
  end
  return table.unpack(result, 1, result.n)
 end
end

function buffer:read(...)
 local timeout = computer.uptime() + self.readTimeout

 local function readChunk()
  if computer.uptime() > timeout then
   error("timeout")
  end
  local result, reason = self.stream:read(self.bufferSize)
  if result then
   self.bufferRead = self.bufferRead .. result
   return self
  else -- error or eof
   return nil, reason
  end
 end

 local function readBytesOrChars(n)
  n = math.max(n, 0)
  local len, sub
  if self.mode.b then
   len = rawlen
   sub = string.sub
  else
   len = unicode.len
   sub = unicode.sub
  end
  local buffer = ""
  repeat
   if len(self.bufferRead) == 0 then
    local result, reason = readChunk()
    if not result then
     if reason then
      return nil, reason
     else -- eof
      return #buffer > 0 and buffer or nil
     end
    end
   end
   local left = n - len(buffer)
   buffer = buffer .. sub(self.bufferRead, 1, left)
   self.bufferRead = sub(self.bufferRead, left + 1)
  until len(buffer) == n
  
  return buffer
 end

 local function readNumber()
  local len, sub
  if self.mode.b then
   len = rawlen
   sub = string.sub
  else
   len = unicode.len
   sub = unicode.sub
  end
  local buffer = ""
  local first = true
  local decimal = false
  local last = false
  local hex = false
  local pat = "^[0-9]+"
  local minbuf = 3 -- "+0x" (sign + hexadecimal tag)
  -- this function is used to read trailing numbers (1e2, 0x1p2, etc)
  local function readnum(checksign)
   local _buffer = ""
   local sign = ""
   while true do
    if len(self.bufferRead) == 0 then
     local result, reason = readChunk()
     if not result then
      if reason then
       return nil, reason
      else -- eof
       return #_buffer > 0 and (sign .. _buffer) or nil
      end
     end
    end
    if checksign then
     local _sign = sub(self.bufferRead, 1, 1)
     if _sign == "+" or _sign == "-" then
      -- "eat" the sign (Rio Lua behaviour)
      sign = sub(self.bufferRead, 1, 1)
      self.bufferRead = sub(self.bufferRead, 2)
     end
     checksign = false
    else
     local x,y = string.find(self.bufferRead, pat)
     if not x then
      break
     else
      _buffer = _buffer .. sub(self.bufferRead, 1, y)
      self.bufferRead = sub(self.bufferRead, y + 1)
     end
    end
   end
   return #_buffer > 0 and (sign .. _buffer) or nil
  end
  while true do
   if len(self.bufferRead) == 0 or len(self.bufferRead) < minbuf then
    local result, reason = readChunk()
    if not result then
     if reason then
      return nil, reason
     else -- eof
      return #buffer > 0 and tonumber(buffer) or nil
     end
    end
   end
   -- these ifs are here so we run the buffer check above
   if first then
    local sign = sub(self.bufferRead, 1, 1)
    if sign == "+" or sign == "-" then
     -- "eat" the sign (Rio Lua behaviour)
     buffer = buffer .. sub(self.bufferRead, 1, 1)
     self.bufferRead = sub(self.bufferRead, 2)
    end
    local hextag = sub(self.bufferRead, 1, 2)
    if hextag == "0x" or hextag == "0X" then
     pat = "^[0-9A-Fa-f]+"
     -- "eat" the 0x, see https://gist.github.com/SoniEx2/570a363d81b743353151
     buffer = buffer .. sub(self.bufferRead, 1, 2)
     self.bufferRead = sub(self.bufferRead, 3)
     hex = true
    end
    minbuf = 0
    first = false
   elseif decimal then
    local sep = sub(self.bufferRead, 1, 1)
    if sep == "." then
     buffer = buffer .. sep
     self.bufferRead = sub(self.bufferRead, 2)
     local temp = readnum(false) -- no sign
     if temp then
      buffer = buffer .. temp
     end
    end
    if not tonumber(buffer) then break end
    decimal = false
    last = true
    minbuf = 1
   elseif last then
    local tag = sub(self.bufferRead, 1, 1)
    if hex and (tag == "p" or tag == "P") then
     local temp = sub(self.bufferRead, 1, 1)
     self.bufferRead = sub(self.bufferRead, 2)
     local temp2 = readnum(true) -- this eats the next sign if any
     if temp2 then
      buffer = buffer .. temp .. temp2
     end
    elseif tag == "e" or tag == "E" then
     local temp = sub(self.bufferRead, 1, 1)
     self.bufferRead = sub(self.bufferRead, 2)
     local temp2 = readnum(true) -- this eats the next sign if any
     if temp2 then
      buffer = buffer .. temp .. temp2
     end
    end
    break
   else
    local x,y = string.find(self.bufferRead, pat)
    if not x then
     minbuf = 1
     decimal = true
    else
     buffer = buffer .. sub(self.bufferRead, 1, y)
     self.bufferRead = sub(self.bufferRead, y + 1)
    end
   end
  end
  return tonumber(buffer)
 end

 local function readLine(chop)
  if not self.mode.t then
   local start = 1
   while true do
    local l = self.bufferRead:find("\n", start, true)
    if l then
     local result = self.bufferRead:sub(1, l + (chop and -1 or 0))
     self.bufferRead = self.bufferRead:sub(l + 1)
     return result
    else
     start = #self.bufferRead
     local result, reason = readChunk()
     if not result then
      if reason then
       return nil, reason
      else -- eof
       local result = #self.bufferRead > 0 and self.bufferRead or nil
       self.bufferRead = ""
       return result
      end
     end
    end
   end
  else
   -- ?????
   io.write("\27[s\27[8m")
   local pos, buffer, hIndex = 1, "", 0
   self.history = self.history or {}
   local function redraw()
    io.write(string.format("\27[u%s \27[u\27[%iC",buffer,(#buffer-pos)+1))
   end
   while true do
    char = readBytesOrChars(1)
    if char == "\27" then
     if readBytesOrChars(1) == "[" then
      local args = {""}
      repeat
       char = readBytesOrChars(1)
       if char:match("%d") then
        args[#args] = args[#args]..char
       else
        args[#args] = tonumber(args[#args])
        args[#args+1] = ""
       end
      until not char:match("[%d;]")
      if char == "C" then -- right
       if pos > 1 then
        pos = pos - 1
       end
      elseif char == "D" then -- left
       if pos <= #buffer then
        pos = pos + 1
       end
      elseif char == "A" then -- up
       hIndex = hIndex + 1
       io.write("\27[u"..(" "):rep(buffer:len()+1))
       buffer = self.history[1+#self.history-hIndex] or buffer
       pos = 1
      elseif char == "B" then -- down
       hIndex = hIndex - 1
       io.write("\27[u"..(" "):rep(buffer:len()+1))
       if hIndex == 0 then
        hIndex = hIndex - 1
        buffer = ""
       end
       buffer = self.history[1+#self.history-hIndex] or buffer
       pos = 1
      end
      hIndex = math.max(math.min(hIndex,#self.history),0)
     end
    elseif char == "\8" then
     if #buffer > 0 and pos <= #buffer then
      buffer = buffer:sub(1, (#buffer - pos)) .. buffer:sub((#buffer - pos) + 2)
     end
    elseif char == "\13" or char == "\10" or char == "\n" then
     io.write("\n")
     self.history[#self.history+1] = buffer
     if #self.history > (self.maxhistory or 16) then table.remove(self.history,1) end
     if chop then buffer = buffer .. "\n" end
     return buffer
    else
     buffer = buffer:sub(1, (#buffer - pos) + 1) .. char .. buffer:sub((#buffer - pos) + 2)
    end
    redraw()
   end
  end
 end

 local function readAll()
  repeat
   local result, reason = readChunk()
   if not result and reason then
    return nil, reason
   end
  until not result -- eof
  local result = self.bufferRead
  self.bufferRead = ""
  return result
 end

 local function read(n, format)
  if type(format) == "number" then
   return readBytesOrChars(format)
  else
   if type(format) ~= "string" or unicode.sub(format, 1, 1) ~= "*" then
    error("bad argument #" .. n .. " (invalid option)")
   end
   format = unicode.sub(format, 2, 2)
   if format == "n" then
    return readNumber()
   elseif format == "l" then
    return readLine(true)
   elseif format == "L" then
    return readLine(false)
   elseif format == "a" then
    return readAll()
   else
    error("bad argument #" .. n .. " (invalid format)")
   end
  end
 end

 if self.mode.w or self.mode.a then
  self:flush()
 end

 local results = {}
 local formats = table.pack(...)
 if formats.n == 0 then
  return readLine(true)
 end
 for i = 1, formats.n do
  local result, reason = read(i, formats[i])
  if result then
   results[i] = result
  elseif reason then
   return nil, reason
  end
 end
 return table.unpack(results, 1, formats.n)
end

function buffer:seek(whence, offset)
 whence = tostring(whence or "cur")
 assert(whence == "set" or whence == "cur" or whence == "end",
  "bad argument #1 (set, cur or end expected, got " .. whence .. ")")
 offset = offset or 0
 checkArg(2, offset, "number")
 assert(math.floor(offset) == offset, "bad argument #2 (not an integer)")

 if self.mode.w or self.mode.a then
  self:flush()
 elseif whence == "cur" then
  offset = offset - #self.bufferRead
 end
 local result, reason = self.stream:seek(whence, offset)
 if result then
  self.bufferRead = ""
  return result
 else
  return nil, reason
 end
end

function buffer:setvbuf(mode, size)
 mode = mode or self.bufferMode
 size = size or self.bufferSize

 assert(mode == "no" or mode == "full" or mode == "line",
  "bad argument #1 (no, full or line expected, got " .. tostring(mode) .. ")")
 assert(mode == "no" or type(size) == "number",
  "bad argument #2 (number expected, got " .. type(size) .. ")")

 self.bufferMode = mode
 self.bufferSize = size

 return self.bufferMode, self.bufferSize
end

function buffer:getTimeout()
 return self.readTimeout
end

function buffer:setTimeout(value)
 self.readTimeout = tonumber(value)
end

function buffer:write(...)
 if self.closed then
  return badFileDescriptor()
 end
 local args = table.pack(...)
 for i = 1, args.n do
  if type(args[i]) == "number" then
   args[i] = tostring(args[i])
  end
  checkArg(i, args[i], "string")
 end

 for i = 1, args.n do
  local arg = args[i]
  local result, reason

  if self.bufferMode == "full" then
   if self.bufferSize - #self.bufferWrite < #arg then
    result, reason = self:flush()
    if not result then
     return nil, reason
    end
   end
   if #arg > self.bufferSize then
    result, reason = self.stream:write(arg)
   else
    self.bufferWrite = self.bufferWrite .. arg
    result = self
   end

  elseif self.bufferMode == "line" then
   local l
   repeat
    local idx = arg:find("\n", (l or 0) + 1, true)
    if idx then
     l = idx
    end
   until not idx
   if l or #arg > self.bufferSize then
    result, reason = self:flush()
    if not result then
     return nil, reason
    end
   end
   if l then
    result, reason = self.stream:write(arg:sub(1, l))
    if not result then
     return nil, reason
    end
    arg = arg:sub(l + 1)
   end
   if #arg > self.bufferSize then
    result, reason = self.stream:write(arg)
   else
    self.bufferWrite = self.bufferWrite .. arg
    result = self
   end

  else -- self.bufferMode == "no"
   result, reason = self.stream:write(arg)
  end

  if not result then
   return nil, reason
  end
 end

 return self
end