local fs = require("filesystem")
local shell = require("shell")
local tty = require("tty")
local unicode = require("unicode")
local tx = require("transforms")
local text = require("text")

local dirsArg, ops = shell.parse(...)

if ops.help then
  print([[Usage: ls [OPTION]... [FILE]...
  -a, --all                  do not ignore entries starting with .
      --full-time            with -l, print time in full iso format
  -h, --human-readable       with -l and/or -s, print human readable sizes
      --si                   likewise, but use powers of 1000 not 1024
  -l                         use a long listing format
  -r, --reverse              reverse order while sorting
  -R, --recursive            list subdirectories recursively
  -S                         sort by file size
  -t                         sort by modification time, newest first
  -X                         sort alphabetically by entry extension
  -1                         list one file per line
  -p                         append / indicator to directories
  -M                         display Microsoft-style file and directory
                             count after listing
      --no-color             Do not colorize the output (default colorized)
      --help                 display this help and exit
For more info run: man ls]])
  return 0
end

if #dirsArg == 0 then
  table.insert(dirsArg, ".")
end

local ec = 0
local fOut = tty.isAvailable() and io.output().tty
local function perr(msg) io.stderr:write(msg,"\n") ec = 2 end
local set_color = function() end
local function colorize() return end
if fOut and not ops["no-color"] then
  local LSC = tx.foreach(text.split(os.getenv("LS_COLORS") or "", {":"}, true), function(e)
    local parts = text.split(e, {"="}, true)
    return parts[2], parts[1]
  end)
  colorize = function(info)
    return
      info.isLink and LSC.ln or
      info.isDir and LSC.di or
      LSC['*'..info.ext] or
      LSC.fi
  end
  set_color=function(c)
    io.write(string.char(0x1b), "[", c or "", "m")
  end
end
local msft={reports=0,proxies={}}
function msft.report(files, dirs, used, proxy)
  local free = proxy.spaceTotal() - proxy.spaceUsed()
  set_color()
  local pattern = "%5i File(s) %s bytes\n%5i Dir(s)  %11s bytes free\n"
  io.write(string.format(pattern, files, tostring(used), dirs, tostring(free)))
end
function msft.tail(names)
  local fsproxy = fs.get(names.path)
  if not fsproxy then
    return
  end
  local totalSize, totalFiles, totalDirs = 0, 0, 0
  for i=1,#names do
    local info = names[i]
    if info.isDir then
      totalDirs = totalDirs + 1
    else
      totalFiles = totalFiles + 1
    end
    totalSize = totalSize + info.size
  end
  msft.report(totalFiles, totalDirs, totalSize, fsproxy)
  local ps = msft.proxies
  ps[fsproxy] = ps[fsproxy] or {files=0,dirs=0,used=0}
  local p = ps[fsproxy]
  p.files = p.files + totalFiles
  p.dirs = p.dirs + totalDirs
  p.used = p.used + totalSize
  msft.reports = msft.reports + 1
end
function msft.final()
  if msft.reports < 2 then return end
  local groups = {}
  for proxy,report in pairs(msft.proxies) do
    table.insert(groups, {proxy=proxy,report=report})
  end
  set_color()
  print("Total Files Listed:")
  for _,pair in ipairs(groups) do
    local proxy, report = pair.proxy, pair.report
    if #groups>1 then
      print("As pertaining to: "..proxy.address)
    end
    msft.report(report.files, report.dirs, report.used, proxy)
  end
end

if not ops.M then
  msft.tail=function()end
  msft.final=function()end
end

local function nod(n)
  return n and (tostring(n):gsub("(%.[0-9]+)0+$","%1")) or "0"
end

local function formatSize(size)
  if not ops.h and not ops['human-readable'] and not ops.si then
    return tostring(size)
  end
  local sizes = {"", "K", "M", "G"}
  local unit = 1
  local power = ops.si and 1000 or 1024
  while size > power and unit < #sizes do
    unit = unit + 1
    size = size / power
  end
  return nod(math.floor(size*10)/10)..sizes[unit]
end

local function pad(txt)
  txt = tostring(txt)
  return #txt >= 2 and txt or "0"..txt
end

local function formatDate(epochms)
  --local day_names={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"}
  local month_names={"January","February","March","April","May","June","July","August","September","October","November","December"}
  if epochms == 0 then return "" end
  local d = os.date("*t", epochms)
  local day, hour, min, sec = nod(d.day), pad(nod(d.hour)), pad(nod(d.min)), pad(nod(d.sec))
  if ops["full-time"] then
    return string.format("%s-%s-%s %s:%s:%s ", d.year, pad(nod(d.month)), pad(day), hour, min, sec)
  else
    return string.format("%s %+2s %+2s:%+2s ", month_names[d.month]:sub(1,3), day, hour, pad(min))
  end
end

local function filter(names)
  if ops.a then
    return names
  end
  local set = { path = names.path }
  for _, info in ipairs(names) do
    if fs.name(info.name):sub(1, 1) ~= "." then
      set[#set + 1] = info
    end
  end
  return set
end

local function sort(names)
  local once = false
  local function ni(v)
    for i=1,#names do
      local info = names[i]
      if info.key == v.key then
        return i
      end
    end
  end
  local function sorter(key)
    once = true
    table.sort(names, function(a, b)
      local ast = names[ni(a)]
      local bst = names[ni(b)]
      return ast[key] > bst[key]
    end)
  end
  if ops.t then sorter("time") end
  if ops.X then sorter("ext") end
  if ops.S then sorter("size") end
  local rev = ops.r or ops.reverse
  if not once then sorter("sort_name") rev=not rev end
  if rev then
    for i=1,#names/2 do
      names[i], names[#names - i + 1] = names[#names - i + 1], names[i]
    end
  end
  return names
end

local function dig(names, dirs, dir)
  if ops.R then
    local di = 1
    for i=1,#names do
      local info = names[i]
      if info.isDir then
        local path = dir..(dir:sub(-1) == "/" and "" or "/")
        table.insert(dirs, di, path..info.name)
        di = di + 1
      end
    end
  end
  return names
end

local first_display = true
local function display(names)
  local mt={}
  local lines = setmetatable({}, mt)
  if ops.l then
    lines.n = #names
    local max_size_width = 1
    local max_date_width = 0
    for i=1,lines.n do
      local info = names[i]
      max_size_width = math.max(max_size_width, formatSize(info.size):len())
      max_date_width = math.max(max_date_width, formatDate(info.time):len())
    end
    mt.__index = function(_, index)
      local info = names[index]
      local file_type = info.isLink and 'l' or info.isDir and 'd' or 'f'
      local link_target = info.isLink and string.format(" -> %s", info.link:gsub("/+$", "") .. (info.isDir and "/" or "")) or ""
      local write_mode = info.fs.isReadOnly() and '-' or 'w'
      local size = formatSize(info.size)
      local modDate = formatDate(info.time)
      local format = "%s-r%s %+"..tostring(max_size_width).."s %"..tostring(max_date_width).."s"
      local meta = string.format(format, file_type, write_mode, size, modDate)
      local item = info.name..link_target
      return {{name = meta}, {color = colorize(info), name = item}}
    end
  elseif ops["1"] or not fOut then
    lines.n = #names
    mt.__index = function(_, index)
      local info = names[index]
      return {{color = colorize(info), name = info.name}}
    end
  else -- columns
    local num_columns, items_per_column, width = 0, 0, tty.getViewport() - 1
    local function real(x, y)
      local index = y + ((x-1) * items_per_column)
      return index <= #names and index or nil
    end
    local function max_name(column_index)
      local max = 0 -- return the width of the max element in column_index
      for r=1,items_per_column do
        local ri = real(column_index, r)
        if not ri then break end
        local info = names[ri]
        max = math.max(max, unicode.wlen(info.name))
      end
      return max
    end
    local function measure()
      local total = 0
      for column_index=1,num_columns do
        total = total + max_name(column_index) + (column_index > 1 and 2 or 0)
      end
      return total
    end
    while items_per_column<#names do
      items_per_column = items_per_column + 1
      num_columns = math.ceil(#names/items_per_column)
      if measure() < width then
        break
      end
    end
    lines.n = items_per_column
    mt.__index=function(_, line_index)
      return setmetatable({},{
        __len=function()return num_columns end,
        __index=function(_, column_index)
          local ri = real(column_index, line_index)
          if not ri then return end
          local info = names[ri]
          local name = info.name
          return {color = colorize(info), name = name .. string.rep(' ', max_name(column_index) - unicode.wlen(name) + (column_index < num_columns and 2 or 0))}
        end,
      })
    end
  end
  for line_index=1,lines.n do
    local line = lines[line_index]
    for element_index=1,#line do
      local e = line[element_index]
      if not e then break end
      first_display = false
      set_color(e.color)
      io.write(e.name)
    end
    set_color()
    print()
  end
  msft.tail(names)
end
local header = function() end
if #dirsArg > 1 or ops.R then
  header = function(path)
    if not first_display then print() end
    set_color()
    io.write(path,":\n")
  end
end

local function stat(path, name)
  local info = {}
  info.key = name
  info.path = name:sub(1, 1) == "/" and "" or path
  info.full_path = fs.concat(info.path, name)
  info.isDir = fs.isDirectory(info.full_path)
  info.name = name:gsub("/+$", "") .. (ops.p and info.isDir and "/" or "")
  info.sort_name = info.name:gsub("^%.","")
  info.isLink, info.link = fs.isLink(info.full_path)
  info.size = info.isLink and 0 or fs.size(info.full_path)
  info.time = fs.lastModified(info.full_path)
  info.fs = fs.get(info.full_path)
  info.ext = info.name:match("(%.[^.]+)$") or ""
  return info
end

local function displayDirList(dirs)
  while #dirs > 0 do
    local dir = table.remove(dirs, 1)
    header(dir)
    local path = shell.resolve(dir)
    local list, reason = fs.list(path)
    if not list then
      perr(reason)
    else
      local names = { path = path }
      for name in list do
        names[#names + 1] = stat(path, name)
      end
      display(dig(sort(filter(names)), dirs, dir))
    end
  end
end
local dir_set, file_set = {}, {path=shell.getWorkingDirectory()}
for _,dir in ipairs(dirsArg) do
  local path = shell.resolve(dir)
  local real, why = fs.realPath(path)
  local access_msg = "cannot access " .. tostring(path) .. ": "
  if not real then
    perr(access_msg .. why)
  elseif not fs.exists(path) then
    perr(access_msg .. "No such file or directory")
  elseif fs.isDirectory(path) then
    table.insert(dir_set, dir)
  else -- file or link
    table.insert(file_set, stat(dir, dir))
  end
end

io.output():setvbuf("line")

local ok, msg = pcall(function()
  if #file_set > 0 then display(sort(file_set)) end
  displayDirList(dir_set)
  msft.final()
end)

io.output():flush()
io.output():setvbuf("no")

assert(ok, msg)

return ec

