local fs = require("filesystem")
local text = require("text")

local api = {}

local function new_node(proxy)
  local node = {proxy=proxy}
  if not proxy or not proxy.list then
    node.children = {}
  end
  return node
end

local function array_read(array, separator)
  separator = separator or " "
  local builder = {}
  for _,value in ipairs(array) do
    table.insert(builder, tostring(value))
  end
  return table.concat(builder, separator)
end

local function child_iterator(node)
  -- a node can either list or have children, but not both (see add_child)
  -- a node can be a file, which has a proxy, but no children
  local listed = {}
  if node then
    if node.proxy and node.proxy.list then
      -- list should return a table, not another iterator
      -- the elements in the list are not nodes, but proxies
      -- we have to wrap each entry with a virtual node (a node that is not in a child-parent tree)
      -- list can be a function that returns a table, or the table already
      local list = node.proxy.list
      listed = type(list) == "table" and list or list()
    elseif node.children then
      listed = node.children
    end
  end
  local availables = {}
  for name, item in pairs(listed) do
    if name:len() > 0 then
      if not item.proxy then item = new_node(item) end
      if not item.proxy.isAvailable or item.proxy.isAvailable() then
        availables[name] = item
      end
    end
  end
  return pairs(availables)
end

local function get_child(node, name)
  for child_name, child in child_iterator(node) do
    if child_name == name then
      return child
    end
  end
end

local function add_child(node, name, proxy)
  if not node or node.proxy and node.proxy.list then
    return nil, "cannot add child to listing proxy"
  end

  local child = new_node(proxy)
  node.children[name] = child
  return child
end

local function findNode(path, bCreate)
  local segments = fs.segments(path)
  local node = api.root
  while #segments > 0 do
    local name = table.remove(segments, 1)
    local next = get_child(node, name)
    if not next then
      if bCreate then
        if not add_child(node, name) then
          return nil, "cannot create child node"
        end
      else
        return nil, "no such file or directory"
      end
    end
    node = next or get_child(node, name)
  end
  return node
end

-- devfs api

api.root = new_node()

function api.create(path, proxy)
  checkArg(1, path, "string")
  checkArg(2, proxy, "table", "nil")
  local pwd = fs.path(path)
  local name = fs.name(path)
  if not name then return nil, "invalid devfs path" end
  local pnode, why = findNode(pwd, true)
  if not pnode then
    return nil, why
  end

  if get_child(pnode, name) then
    return nil, "file or directory exists"
  end

  return add_child(pnode, name, proxy)
end

-- the filesystem object as seen from the system mount interface
api.proxy = {}

-- forward declare injector
local inject_dynamic_pairs
local function dynamic_list(path, fsnode)
  local nodes, links, dirs = {}, {}, {}
  local node = findNode(path)
  if node then
    for name,cnode in child_iterator(node) do
      if cnode.proxy and cnode.proxy.link then
        links[name] = cnode.proxy.link
      elseif cnode.proxy and cnode.proxy.list then
        local child = {name=name,parent=fsnode}
        local child_path = path .. "/" .. name
        inject_dynamic_pairs(child, child_path, true)
        dirs[name] = child
      else
        nodes[name] = cnode
      end
    end
  end
  return nodes, links, dirs
end

inject_dynamic_pairs = function(fsnode, path, bStoreUse)
  if getmetatable(fsnode) then return end
  fsnode.children = nil
  fsnode.links = nil
  setmetatable(fsnode,
  {
    __index = function(tbl, key)
      local bLinks = key == "links"
      local bChildren = key == "children"
      if not bLinks and not bChildren then return end
      local _, links, dirs = dynamic_list(path, tbl)
      if bStoreUse then
        tbl.children = dirs
        tbl.links = links
      end
      return bLinks and links or dirs
    end
  })
end

local label_lib = dofile("/lib/core/device_labeling.lua")
label_lib.loadRules()
api.getDeviceLabel = label_lib.getDeviceLabel
api.setDeviceLabel = label_lib.setDeviceLabel

local registered = false
function api.register(public_proxy)
  if registered then return end
  registered = true

  local start_path = "/lib/core/devfs/"
  for starter in fs.list(start_path) do
    local full_path = start_path .. starter
    local _,matched = starter:gsub("%.lua$","")
    if matched > 0 then
      local data = dofile(full_path)
      for name, entry in pairs(data) do
        api.create(name, entry)
      end
    end
  end

  if rawget(public_proxy, "fsnode") then
    inject_dynamic_pairs(public_proxy.fsnode, "")
  end
end

function api.proxy.list(path)
  local result = {}
  for name in pairs(dynamic_list(path, false, false)) do
    table.insert(result, name)
  end
  return result
end

function api.proxy.isDirectory(path)
  local node = findNode(path)
  return node and node.proxy and node.proxy.list
end

function api.proxy.size(path)
  checkArg(1, path, "string")
  local node = findNode(path)
  if not node or not node.proxy then
    return 0
  end

  local proxy = node.proxy
  if proxy.list then return 0 end
  if proxy.size then return proxy.size() end
  if proxy.open then return 0 end
  if proxy.read then return proxy.read():len() end
  if proxy[1] ~= nil then return array_read(proxy):len() end
  return 0
end

function api.proxy.lastModified()
  return 0
end

function api.proxy.exists(path)
  checkArg(1, path, "string")
  return not not findNode(path)
end

function api.getDevice(path)
  checkArg(1, path, "string")
  local device
  local reason = "no such device"
  local real, why = fs.realPath(require("shell").resolve(path))
  if not real then return nil, why end
  if fs.exists(real) then
    -- we don't have a good way of knowing where dev is mounted still
    -- similar hack in api.proxy.open
    real = fs.path(real) .. (fs.name(real) or "")
    local part, subbed = real:gsub("^/dev/", "")
    if subbed > 0 and part:len() > 0 then
      local node = findNode(part)
      if node and node.proxy then
        -- must be a special device node
        device = node.proxy.device
      end
      if not device then
        reason = "not a device"
      end
    else
      device, reason = fs.get(real)
    end
  end
  return device, reason
end

function api.proxy.open(path, mode)
  checkArg(1, path, "string")
  checkArg(2, mode, "string", "nil")

  mode = mode or "r"
  local bRead = mode:match("[ra]")
  local bWrite = mode:match("[wa]")

  if not bRead and not bWrite then
    return nil, "invalid mode"
  end

  local node, why = findNode(path)
  if not node then
    return nil, why
  elseif not node.proxy or node.proxy.list then
    return nil, "is a directory"
  end

  local proxy = node.proxy

  -- in case someone tries to open a link directly, refer them back to fs
  -- this is an unfortunate pathing hack due to optimizations for memory
  if proxy.link then
    return fs.open("/dev/"..path, mode)
  end

  -- special (but common) simple readonly cases
  if proxy[1] ~= nil then -- contains special readonly value
    local array = proxy
    proxy.read = function()return array_read(array) end
  end

  if proxy.open then
    return proxy.open(mode)
  end

  if bRead and not proxy.read then
    return nil, "cannot open for read"
  elseif bWrite and not proxy.write then
    return nil, "cannot open for write"
  end

  local txtRead = bRead and proxy.read()

  if bWrite then
    return text.internal.writer(proxy.write, mode, txtRead)
  end

  return text.internal.reader(txtRead, mode)
end

-- as long as the fsnode hack is used, fs.isLink is not needed here
-- function api.proxy.isLink(path) end

local function checked_invoke(handle, method, ...)
  checkArg(1, handle, "table")
  checkArg(2, method, "string")
  checkArg(3, handle[method], "function", "table", "nil")
  local m = handle[method]
  if not m then
    return nil, "bad file handle"
  elseif type(m) == "table" then
    local mm = getmetatable(m)
    assert(mm and mm.__call, string.format("FILE handle [%s] method defined, but is not callable", tostring(method)))
  end
  return m(handle, ...)
end

function api.proxy.read(h, ...)
  return checked_invoke(h, "read", ...)
end

function api.proxy.close(h, ...)
  return checked_invoke(h, "close", ...)
end

function api.proxy.write(h, ...)
  return checked_invoke(h, "write", ...)
end

function api.proxy.seek(h, ...)
  return checked_invoke(h, "seek", ...)
end

function api.proxy.remove()
  return nil, "cannot remove file or directory"
end

function api.proxy.makeDirectory()
  return nil, "use create in the devfs api"
end

function api.proxy.setLabel()
  return nil, "cannot set label on devfs"
end

return api
