Updater = { }

Updater.maxRetries = 5

local updaterWindow
local loadModulesFunction
local scheduledEvent
local httpOperationId = 0
local previousHttpTimeout

local function onLog(level, message, time)
  if level == LogError then
    Updater.error(message)
    g_logger.setOnLog(nil)
  end
end

local function initAppWindow()
  if g_resources.getLayout() == "mobile" then
    g_window.setMinimumSize({ width = 640, height = 360 })
  else
    g_window.setMinimumSize({ width = 800, height = 640 })
  end

  local size = { width = 1024, height = 600 }
  size = g_settings.getSize('window-size', size)
  g_window.resize(size)

  local displaySize = g_window.getDisplaySize()
  local defaultPos = { x = (displaySize.width - size.width)/2,
                       y = (displaySize.height - size.height)/2 }
  local pos = g_settings.getPoint('window-pos', defaultPos)
  pos.x = math.max(pos.x, 0)
  pos.y = math.max(pos.y, 0)
  g_window.move(pos)

  local maximized = g_settings.getBoolean('window-maximized', false)
  if maximized then g_window.maximize() end

  g_window.setTitle(g_app.getName())
  g_window.setIcon('/images/clienticon')

  if g_app.isMobile() then
    scheduleEvent(function()
      g_app.scale(5.0)
    end, 10)
  end
end

local function loadModules()
  if loadModulesFunction then
    local tmpLoadFunc = loadModulesFunction
    loadModulesFunction = nil
    tmpLoadFunc()
  end
end

local function normPath(path)
  if not path then return '' end
  path = path:gsub('\\', '/')
  if path:sub(1, 1) ~= '/' then
    path = '/' .. path
  end
  return path
end

local function hashesEqual(a, b)
  if not a or not b then return false end
  a = tostring(a):lower()
  b = tostring(b):lower()
  return a == b and a:len() > 0
end

local function fileExistsAny(path)
  path = normPath(path)
  return g_resources.fileExists(path) or g_resources.fileExists(path:sub(2))
end

local function localChecksum(path)
  local archive = g_resources.filesChecksums()
  if type(archive) == 'table' then
    if archive[path] then return archive[path] end
    if archive[normPath(path)] then return archive[normPath(path)] end
    local noSlash = path:gsub('^/', '')
    if archive[noSlash] then return archive[noSlash] end
  end
  local vfs = normPath(path)
  if g_resources.fileChecksum then
    if g_resources.fileExists(vfs) then
      return g_resources.fileChecksum(vfs)
    end
    if g_resources.fileExists(path) then
      return g_resources.fileChecksum(path)
    end
  end
  return nil
end

local function needsUpdate(path, remoteHash)
  local localHash = localChecksum(path)
  if hashesEqual(localHash, remoteHash) then
    return false
  end
  -- unpacked client may use another hash algorithm; skip re-download if file is already there
  if localHash and remoteHash and tostring(localHash):len() ~= tostring(remoteHash):len() and fileExistsAny(path) then
    return false
  end
  return true
end

local function ensureDirForFile(path)
  path = normPath(path)
  local dir = path:match('^(.*)/[^/]+$')
  if not dir or dir == '' or dir == '/' then return end
  local acc = ''
  for part in dir:gmatch('[^/]+') do
    acc = acc .. '/' .. part
    if not g_resources.directoryExists(acc) then
      pcall(function() g_resources.makeDir(acc) end)
    end
  end
end

local function applyUnpacked(files)
  for _, entry in ipairs(files) do
    local file = entry[1]
    if file and not file:find('%.exe$') then
      local candidates = {
        '/downloads' .. normPath(file),
        '/downloads/' .. file:gsub('^/', ''),
        normPath(file)
      }
      local content = nil
      for _, name in ipairs(candidates) do
        if g_resources.fileExists(name) then
          local ok, data = pcall(function() return g_resources.readFileContents(name) end)
          if ok and data and #data > 0 then
            content = data
            break
          end
        end
      end
      if content then
        ensureDirForFile(file)
        pcall(function() g_resources.writeFileContents(normPath(file), content) end)
      end
    end
  end
end

local function downloadFiles(url, files, index, retries, doneCallback)
  if not updaterWindow then return end
  local entry = files[index]
  if not entry then
    return doneCallback()
  end
  local file = entry[1]
  local file_checksum = entry[2]

  if retries > 0 then
    updaterWindow.downloadStatus:setText(tr("Downloading (%i retry):\n%s", retries, file))
  else
    updaterWindow.downloadStatus:setText(tr("Downloading:\n%s", file))
  end
  updaterWindow.downloadProgress:setPercent(0)
  updaterWindow.mainProgress:setPercent(math.floor(100 * index / #files))

  httpOperationId = HTTP.download(url .. file, file,
    function (downloaded, checksum, err)
      if not err and checksum and file_checksum and not hashesEqual(checksum, file_checksum) then
        if tostring(checksum):len() == tostring(file_checksum):len() then
          err = "Invalid checksum of: " .. file .. ".\nShould be " .. file_checksum .. ", is: " .. checksum
        else
          g_logger.warning("Updater checksum algorithm mismatch for " .. file .. ", accepting file")
        end
      end
      if err then
        if retries >= Updater.maxRetries then
          Updater.error("Can't download file: " .. file .. ".\nError: " .. err)
        else
          scheduledEvent = scheduleEvent(function()
            downloadFiles(url, files, index, retries + 1, doneCallback)
          end, 250)
        end
        return
      end
      downloadFiles(url, files, index + 1, 0, doneCallback)
    end,
    function (progress, speed)
      updaterWindow.downloadProgress:setPercent(progress)
      updaterWindow.downloadProgress:setText(speed .. " kbps")
    end)
end

local function updateFiles(data, keepCurrentFiles)
  if not updaterWindow then return end
  if type(data) ~= "table" then
    return Updater.error("Invalid data from updater api (not table)")
  end
  if type(data["error"]) == 'string' and data["error"]:len() > 0 then
    return Updater.error(data["error"])
  end
  if not data["files"] or type(data["url"]) ~= 'string' or data["url"]:len() < 4 then
    return Updater.error("Invalid data from updater api: " .. json.encode(data, 2))
  end
  if data["keepFiles"] then
    keepCurrentFiles = true
  end
  if not g_resources.isLoadedFromArchive() then
    keepCurrentFiles = true
  end

  local newFiles = false
  local finalFiles = {}
  local localFiles = g_resources.filesChecksums() or {}
  local toUpdate = {}

  for file, checksum in pairs(localFiles) do
    if keepCurrentFiles or string.find(file, "data/things") then
      table.insert(finalFiles, file)
    end
  end

  for file, checksum in pairs(data["files"]) do
    table.insert(finalFiles, file)
    if needsUpdate(file, checksum) then
      table.insert(toUpdate, {file, checksum})
      newFiles = true
    end
  end

  local binary = nil
  if type(data["binary"]) == "table" and data["binary"]["file"] and data["binary"]["file"]:len() > 1 then
    local selfChecksum = g_resources.selfChecksum()
    if selfChecksum:len() > 0 and not hashesEqual(selfChecksum, data["binary"]["checksum"]) then
      if tostring(selfChecksum):len() == tostring(data["binary"]["checksum"]):len() then
        binary = data["binary"]["file"]
        table.insert(toUpdate, {binary, data["binary"]["checksum"]})
      end
    end
  end

  if #toUpdate == 0 then
    updaterWindow.mainProgress:setPercent(100)
    scheduledEvent = scheduleEvent(Updater.abort, 20)
    return
  end

  local forceRestart = false
  local reloadModules = false
  local forceRestartPattern = {"init.lua", "corelib", "updater", "otmod"}
  for _, file in ipairs(toUpdate) do
    for __, pattern in ipairs(forceRestartPattern) do
      if string.find(file[1], pattern) then
        forceRestart = true
      end
      if not string.find(file[1], "data/things") then
        reloadModules = true
      end
    end
  end

  updaterWindow.status:setText(tr("Updating %i files", #toUpdate))
  updaterWindow.mainProgress:setPercent(0)
  updaterWindow.downloadProgress:setPercent(0)
  updaterWindow.downloadProgress:show()
  updaterWindow.downloadStatus:show()
  updaterWindow.changeUrlButton:hide()
  downloadFiles(data["url"], toUpdate, 1, 0, function()
    updaterWindow.status:setText(tr("Updating client (may take few seconds)"))
    updaterWindow.mainProgress:setPercent(100)
    updaterWindow.downloadProgress:hide()
    updaterWindow.downloadStatus:hide()
    scheduledEvent = scheduleEvent(function()
      local unpacked = not g_resources.isLoadedFromArchive()
      local restart = binary or (not loadModulesFunction and reloadModules) or forceRestart
      if newFiles then
        if unpacked then
          applyUnpacked(toUpdate)
          pcall(function() g_resources.updateData(finalFiles, false) end)
        else
          g_resources.updateData(finalFiles, not restart)
        end
      end
      if binary then
        g_resources.updateExecutable(binary)
      end
      if restart then
        g_app.restart()
      else
        if reloadModules then
          g_modules.reloadModules()
        end
        Updater.abort()
      end
    end, 100)
  end)
end

function Updater.init(loadModulesFunc)
  g_logger.setOnLog(onLog)
  loadModulesFunction = loadModulesFunc
  previousHttpTimeout = HTTP.timeout
  HTTP.timeout = 60
  initAppWindow()
  Updater.check()
end

function Updater.terminate()
  loadModulesFunction = nil
  Updater.abort()
end

function Updater.abort()
  HTTP.cancel(httpOperationId)
  removeEvent(scheduledEvent)
  if previousHttpTimeout then
    HTTP.timeout = previousHttpTimeout
    previousHttpTimeout = nil
  end
  if updaterWindow then
    updaterWindow:destroy()
    updaterWindow = nil
  end
  loadModules()
end

function Updater.check(args)
  if updaterWindow then return end

  updaterWindow = g_ui.displayUI('updater')
  updaterWindow:show()
  updaterWindow:focus()
  updaterWindow:raise()

  local updateData = nil
  local function progressUpdater(value)
    removeEvent(scheduledEvent)
    if value == 100 then
      return Updater.error(tr("Timeout"))
    end
    if updateData and (value > 60 or (not g_app.isMobile() or not ALLOW_CUSTOM_SERVERS or not loadModulesFunc)) then
      return updateFiles(updateData)
    end
    scheduledEvent = scheduleEvent(function() progressUpdater(value + 1) end, 50)
    updaterWindow.mainProgress:setPercent(value)
  end
  progressUpdater(0)

  httpOperationId = HTTP.postJSON(Services.updater, {
    version = APP_VERSION,
    build = g_app.getVersion(),
    os = g_app.getOs(),
    platform = g_window.getPlatformType(),
    args = args or {}
  }, function(data, err)
    if err then
      return Updater.error(err)
    end
    updateData = data
  end)
end

function Updater.error(message)
  removeEvent(scheduledEvent)
  if not updaterWindow then return end
  local text = tostring(message or ''):lower()
  if text:find('timeout') or text:find('connect') or text:find('resolv') or text:find('refused') or text:find('http') then
    g_logger.warning('Updater: ' .. tostring(message) .. ' - continuing to login')
    return Updater.abort()
  end
  displayErrorBox(tr("Updater Error"), message).onOk = function()
    Updater.abort()
  end
end

function Updater.changeUrl()
  removeEvent(scheduledEvent)
  modules.client_textedit.edit(Services.updater, {title="Enter updater url", width=300}, function(newUrl)
    if updaterWindow and newUrl:len() > 4 then
      Services.updater = newUrl
    end
    if updaterWindow then
      updaterWindow:destroy()
      updaterWindow = nil
    end
    Updater.check()
  end)
end
