local fs = require "luacheck.fs"
local serializer = require "luacheck.serializer"
local sha1 = require "luacheck.vendor.sha1"
local utils = require "luacheck.utils"

local cache = {}

-- Check results can be cached inside a given cache directory.
-- Check result for a file is stored in `<cache_dir>/<SHA1(filename)>`.
-- Cache file format: <format_version>\n<filename>\n<serialized check result`.

-- Returns default cache directory or nothing.
function cache.get_default_dir()
   if utils.is_windows then
      local local_app_data_dir = os.getenv("LOCALAPPDATA")

      if not local_app_data_dir then
         local user_profile_dir = os.getenv("USERPROFILE")

         if user_profile_dir then
            local_app_data_dir = fs.join(user_profile_dir, "Local Settings", "Application Data")
         end
      end

      if local_app_data_dir then
         return fs.join(local_app_data_dir, "Luacheck", "Cache")
      end
   else
      local fh = assert(io.popen("uname -s"))
      local system = fh:read("*l")
      fh:close()

      if system == "Darwin" then
         local home_dir = os.getenv("HOME")

         if home_dir then
            return fs.join(home_dir, "Library", "Caches", "Luacheck")
         end
      else
         local config_home_dir = os.getenv("XDG_CACHE_HOME")

         if not config_home_dir then
            local home_dir = os.getenv("HOME")

            if home_dir then
               config_home_dir = fs.join(home_dir, ".cache")
            end
         end

         if config_home_dir then
            return fs.join(config_home_dir, "luacheck")
         end
      end
   end
   return fs.join(fs.get_current_dir(), ".luacheck_cache")
end

local format_version = "1"

local Cache = utils.class()

function Cache:__init(cache_directory)
   local ok, err = fs.make_dirs(cache_directory)

   if not ok then
      return nil, ("Couldn't initialize cache in %s: %s"):format(cache_directory, err)
   end

   self._dir = cache_directory
   self._current_dir = fs.get_current_dir()
end

-- Caches check result for a file. Returns true on success, nothing on error.
function Cache:put(filename, check_result)
   local normalized_filename = fs.normalize(fs.join(self._current_dir, filename))
   local cache_filename = fs.join(self._dir, sha1.sha1(normalized_filename))

   local fh = io.open(cache_filename, "wb")

   if not fh then
      return
   end

   local serialized_result = serializer.dump_check_result(check_result)
   fh:write(format_version, "\n", normalized_filename, "\n", serialized_result)
   fh:close()
   return true
end

-- Retrieves cached check result for a file.
-- Returns check result on cache hit, nothing on cache miss,
-- nil and true on malformed cache data.
function Cache:get(filename)
   local normalized_filename = fs.normalize(fs.join(self._current_dir, filename))
   local cache_filename = fs.join(self._dir, sha1.sha1(normalized_filename))

   local file_mtime = fs.get_mtime(filename)
   local cache_mtime = fs.get_mtime(cache_filename)

   if not file_mtime or not cache_mtime or file_mtime >= cache_mtime then
      return
   end

   local fh = io.open(cache_filename, "rb")

   if not fh then
      return
   end

   if fh:read() ~= format_version then
      fh:close()
      return
   end

   if fh:read() ~= normalized_filename then
      fh:close()
      return
   end

   local serialized_result = fh:read("*a")
   fh:close()

   if not serialized_result then
      return nil, true
   end

   local result = serializer.load_check_result(serialized_result)

   if not result then
      return nil, true
   end

   return result
end

function cache.new(cache_directory)
   return Cache(cache_directory)
end

return cache
