-- primitive for reading files from a file system (or, later, network)
-- returns an object or nil on error
-- read lines from the object using .read() with args similar to file:read()
-- read() will indicate end of file by returning nil.
function start_reading(fs, filename)
  local infile = io.open(filename)
  if infile == nil then return nil end
  return {
    read = coroutine.wrap(function(format)
      while true do
        if format == nil then format = '*l' end
        format = coroutine.yield(infile:read(format))
      end
    end),
  }
end

-- fake file object with the same 'shape' as that returned by start_reading
function fake_file_stream(s)
  local i = 1
  local max = string.len(s)
  return {
    read = function(format)
      if i > max then
        return nil
      end
      if type(format) == 'number' then
        local result = s:sub(i, i+format-1)
        i = i+format
        return result
      elseif format == '*a' then
        local result = s:sub(i)
        i = max+1
        return result
      elseif format == '*l' then
        local start = i
        while i <= max do
          if s:sub(i, i) == '\n' then
            break
          end
          i = i+1
        end
        local result = s:sub(start, i)
        i = i+1
        return result
      elseif format == '*n' then
        error('fake file streams: *n not yet supported')
      end
    end,
  }
end

function test_fake_file_system()
  local s = fake_file_stream('abcdefgh\nijk\nlmn')
  check_eq(s.read(1), 'a', 'fake_file_system: 1 char')
  check_eq(s.read(1), 'b', 'fake_file_system: 1 more char')
  check_eq(s.read(3), 'cde', 'fake_file_system: multiple chars')
  check_eq(s.read('*l'), 'fgh\n', 'fake_file_system: line')
  check_eq(s.read('*a'), 'ijk\nlmn', 'fake_file_system: all')
end

-- primitive for writing files to a file system (or, later, network)
-- returns an object or nil on error
-- write to the object using .write()
-- indicate you're done writing by calling .close()
-- file will not be externally visible until .close()
function start_writing(fs, filename)
  if filename == nil then
    error('start_writing requires two arguments: a file-system (nil for real disk) and a filename')
  end
  local initial_filename = temporary_filename_in_same_volume(filename)
  local outfile = io.open(initial_filename, 'w')
  if outfile == nil then return nil end
  return {
    write = coroutine.wrap(function(x)
      while true do
        x = coroutine.yield(outfile:write(x))
      end
    end),
    close = function()
      outfile:close()
      os.rename(initial_filename, filename)
    end,
  }
end

function temporary_filename_in_same_volume(filename)
  -- opening in same directory will hopefully keep it on the same volume,
  -- so that a future rename works
  local i = 1
  while true do
    temporary_filename = 'teliva_tmp_'..filename..'_'..i
    if io.open(temporary_filename) == nil then
      -- file doesn't exist yet; create a placeholder and return it
      local handle = io.open(temporary_filename, 'w')
      if handle == nil then
        -- HERE: if an app doesn't have permissions, attempts to write to disk eventually get here
        error("this is unexpected; I can't create temporary files..")
      end
      handle:close()
      return temporary_filename
    end
    i = i+1
  end
end
