--[[ Copyright (c) 2010 Peter "Corsix" Cawley

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. --]]

local LFS = require "lfs"
local pathsep = package.config:sub(1, 1)
local part_pattern = "[^" .. pathsep .. "]+"
local ISO_FS = require "ISO_FS"

--! Layer for abstracting away differences in file systems
class "FileSystem"

---@type FileSystem
local FileSystem = _G["FileSystem"]

function FileSystem:FileSystem()
end

local function normalise(str)
  return str:upper():gsub("_", "-")
end

function FileSystem:_enumerate()
  self.sub_dirs = {}
  self.files = {}
  for item in lfs.dir(self.physical_path) do
    local path = self.physical_path .. pathsep .. item
    if lfs.attributes(path, "mode") == "directory" then
      self.sub_dirs[normalise(item)] = {physical_path = path}
    else
      self.files[normalise(item)] = path
    end
  end
end

function FileSystem:setRoot(physical_path)
  if physical_path:match"%.[iI][sS][oO]$" or physical_path:match"%.[iI][sS][oO]9660$" then
    self.provider = ISO_FS()
    self.provider:setPathSeparator(pathsep)
    local file, err = io.open(physical_path, "rb")
    if not file then
      return nil, err
    end
    return self.provider:setRoot(file)
  else
    self.provider = nil
  end
  if physical_path:sub(-1) == pathsep then
    -- Trim off the trailing separator (lfs doesn't like querying the mode of a
    -- directory with a trailing slash on win32)
    physical_path = physical_path:sub(1, -2)
  end
  if lfs.attributes(physical_path, "mode") ~= "directory"  then
    return nil, "Specified path ('" .. physical_path .. "') is not a directory"
  end
  self.physical_path = physical_path
  self:_enumerate()
  return true
end

function FileSystem:listFiles(virtual_path, ...)
  if ... then
    virtual_path = table.concat({virtual_path, ...}, pathsep)
  end
  if self.provider then
    return self.provider:listFiles(virtual_path)
  elseif not self.sub_dirs then
    return nil, "Filesystem layer not initialised"
  end
  for part in virtual_path:gmatch(part_pattern) do
    local part_u = normalise(part)
    if self.sub_dirs[part_u] then
      self = self.sub_dirs[part_u]
      if not self.files then
        FileSystem._enumerate(self)
      end
    elseif self.files[part_u] then
      return nil, ("Attempt to access file '%s' as if it were a directory while trying to enumerate '%s'"):format(part, virtual_path)
    else
      return nil, ("Unable to find '%s' while trying to enumerate '%s'"):format(part, virtual_path)
    end
  end
  return self.files
end

function FileSystem:readContents(virtual_path, ...)
  local file, err = self:getFilePath(virtual_path, ...)
  if not file then
    return file, err
  end
  local f, e = io.open(file, "rb")
  if not f then
    return nil, e
  end
  local data = f:read"*a"
  f:close()
  return data
end

function FileSystem:getFilePath(virtual_path, ...)
  if ... then
    virtual_path = table.concat({virtual_path, ...}, pathsep)
  end
  if self.provider then
    return self.provider:readContents(virtual_path)
  elseif not self.sub_dirs then
    return nil, "Filesystem layer not initialised"
  end
  local is_file = false
  for part in virtual_path:gmatch(part_pattern) do
    if is_file then
      return nil, ("Attempt to access file '%s' as if it were a directory while trying to read '%s'"):format(part, virtual_path)
    end
    local part_u = normalise(part)
    if self.sub_dirs[part_u] then
      self = self.sub_dirs[part_u]
      if not self.files then
        FileSystem._enumerate(self)
      end
    elseif self.files[part_u] then
      self = self.files[part_u]
      is_file = true
    else
      return nil, ("Unable to find '%s' while trying to read '%s'"):format(part, virtual_path)
    end
  end
  if not is_file then
    return nil, ("Attempt to access directory '%s' as if it were a file"):format(virtual_path)
  end
  return self
end
