local verbose = require "sga.verbose"
local ext = require "shellExtension"

SGA_DAEMON.INF = 2 ^ 1024
SGA_DAEMON.ERROR_CODE = -1
SGA_DAEMON.NO_VALUE = ""
SGA_DAEMON.NetCapacity = {
  ALL  = "ALL", 
  NO   = "NO", 
  CALC = "CALC"
}

--...................... Funções locais ....................................

-- Função que constrói o path absoluto de um arquivo ou diretório.
-- No caso de link simbólico retornamos apenas o nome do arquivo 
-- que representa o link.
-- 
-- @param basePath path inicial.
-- @param fileName nome do arquivo ou diretório.
-- @return path absoluto.
local function buildAbsolutePath(basePath, fileName)
  if string.match(fileName, " %->") then
    fileName = string.match(fileName, "(.-) %->")
  end

  if fileName == '' then
     return basePath
  end

  if #basePath == 0 or basePath == fileName then
     return fileName
  end

  if string.match(basePath, ext.fileSeparator, #basePath) then
     return basePath .. fileName
  end

  return basePath .. ext.fileSeparator .. fileName
end

-- Função que dado uma tabela que contém as linhas de uma saída de 'ls -ago'
-- descobre o índice que os paths começam. Caso não seja possível descobrir
-- o início de um path, lançamos um erro.
-- 
-- @param tabela em forma de array onde cada elemento é uma linha da saída de 'ls -ago'.
-- @return índice de onde começa o path.
function findInitPath(lines)
  if #lines == 0 then
     return nil, "Path não existe ou não temos permissão para acessá-lo."
  end

  local initPath

  if #lines == 1 then
     local line = lines[1]
     local i, e = string.find(line, ' %->')
     if e then
        line = string.sub(line, 1, e - 3)
     end
     i, e = string.find(line, ".* ")
     initPath = e + 1
  else
     for _, line in ipairs(lines) do
        local begin = string.find(line, "%.")
        if begin and begin > 12 then
          if not initPath then
             initPath = begin
          else
             if begin then
                initPath = math.min(initPath, begin)
             end
          end
        end
     end
  end

  if not initPath then
     return nil, "Não foi possível identificar o início dos paths."
  end
  return initPath
end

--..........................................................................

function SGA_DAEMON:isAlive()
  return true
end

--..........................................................................

function SGA_DAEMON:kill(status)
  verbose.kill("Recebida uma requisição de término de SGA!!!")
  self:exit(status)
end

--..........................................................................

function SGA_DAEMON:exit(status)
  verbose.kill("Encerrando...")

  if CSFS.launch_daemon == YES and CSFS.process ~= nil then
    CSFS.process:kill()
  end
  
  if SGAD_CONF.net_benchmark == YES and IPERF.process ~= nil then
    IPERF.process:kill()
  end

  os.exit(status)
end

--..........................................................................

function SGA_DAEMON:getNodeNames()
  local tab = {}
  local nodes = SGAD_CONF.nodes

  for i, node in ipairs(nodes) do
    table.insert( tab, node.name )
  end

  return tab
end

--..........................................................................

function SGA_DAEMON:getPaths(basePath)
	local basePathInfo = SGA_DAEMON:getPath(basePath)
	if basePathInfo.isSymbolicLink and basePathInfo.isDir then
		if not string.match(basePath, ext.fileSeparator, #basePath) then
			basePath = basePath .. ext.fileSeparator
		end
	end
	local lines, err = ext.ls(basePath, "-ago")
	if err then
		verbose.error(string.format("ERRO: %s", err))
		return {}
	end
	
	local initPath, err = findInitPath(lines)
	if err then
	 	verbose.error(string.format("Erro: %s (path: %s)", err, basePath))
	        return {}
	end

	local result = {}
	for _, line in ipairs(lines) do
		local fileName = string.sub(line, initPath)
		if fileName ~= '' and 
                   fileName ~= '.'  and fileName ~= './' and 
                   fileName ~= '..' and fileName ~= '../' then
			local absolutePath = buildAbsolutePath(basePath, fileName)
			local pathInfo = SGA_DAEMON:getPath(absolutePath)
			result[#result + 1] = pathInfo
		end
	end
	return result
end

--.........................................................................

function SGA_DAEMON:getPath(path)
        local noInfo = {}
        noInfo.isDir = false
	noInfo.isSymbolicLink = false
	noInfo.linkPath = ''
	noInfo.readable = false
	noInfo.writable = false
	noInfo.executable = false
	noInfo.sizeKB = -1
	noInfo.path = path
	noInfo.exists = false

        local attrs = ext.getAttributes(path)
	local exists = attrs.d or attrs.f

	if not exists then
		verbose.error(string.format("ERRO: Path não existe: %s", path))
		return noInfo 
	end

	local sizeKB = ext.size(path)/1024 --converte bytes em KB

	local pathInfo = {}
	pathInfo.isDir = attrs.d
	pathInfo.isSymbolicLink = attrs.h
	pathInfo.readable = attrs.r
	pathInfo.writable = attrs.w
	pathInfo.executable = attrs.x
	pathInfo.sizeKB = sizeKB
	pathInfo.path = path
	pathInfo.exists = true
	pathInfo.linkPath = ''

	return pathInfo
end

--..........................................................................

function SGA_DAEMON:meetsRequirement( requirement )
  if not SGAD_CONF.requirements then
     return nil
  end

  return SGAD_CONF.requirements[ requirement ] 
end

--..........................................................................

function SGA_DAEMON:execNetBench()
  if CSFS.launch_daemon == NO then
    return SGA_DAEMON.NetCapacity.ALL -- grande capacidade de transferência 
  else  
    if SGAD_CONF.net_benchmark == YES then
      return SGA_DAEMON.NetCapacity.CALC -- deve calcular a capacidade de transferência
    else
      return SGA_DAEMON.NetCapacity.NO -- pequena capacidade de transferência
    end
  end
end

--..........................................................................

function SGA_DAEMON:isCluster()
  return #self:getNodeNames() > 1
end

-- .......................................................................

-- A biblioteca do sga retorna a carga da CPU no ultimo minuto.
-- Cabe a este objeto calcular a carga nos últimos 5 e 15 minutos...
SGA_DAEMON.loadAvg = {}

function SGA_DAEMON.new_loadAvg ()
   obj = {
        update = 0,
        load5 = {},
        load15 = {},
        loadAvg1min = 0,
        loadAvg5min = 0,
        loadAvg15min = 0,
        acc1 = function (self, load1, maxold)
           local NOW, err = SGAD_CONF.serverManager.now()
           if err then
              verbose.error(string.format("ERRO: %s", err))
              NOW = SGA_DAEMON.INF
           end
           if maxold and (NOW - self.update) < maxold then
              return
           end
           self.update = NOW
           local load5 = self:acc("load5", load1, 5 * 60)
           local load15= self:acc("load15",load1, 15 * 60)
           self.loadAvg1min = load1
           self.loadAvg5min = load5
           self.loadAvg15min = load15
        end,
        acc = function (self, loadtb, currload, expire)
           local total = 0
           local nelem = 0
           local load = 0
           self[loadtb][self.update] = currload
           for tstamp, load in pairs(self[loadtb]) do
              if (self.update - tstamp) > expire then
                 self[loadtb][tstamp] = nil
              else
                 total = total + load
                 nelem = nelem + 1
              end
           end
           if nelem > 0 then
              load = total / nelem
           end
           return load
        end,
        getLoad = function (self)
           return {
              loadAvg1min = self.loadAvg1min,
              loadAvg5min = self.loadAvg5min,
              loadAvg15min = self.loadAvg15min,
           }
        end,
   }
   return obj
end

-- .......................................................................

function SGA_DAEMON:readMachineStateInformation(freqTb)
  local update = function (name, last, maxold)
     local server = SGAD_CONF.serverManager
     local memory, err = server.getmemory(name, maxold)
     local mfree_ram_perc = last.memory_ram_free_perc or self.ERROR_CODE 
     local mfree_swap_perc = last.memory_swap_free_perc or self.ERROR_CODE
     local njobs = last.number_of_jobs or self.ERROR_CODE

     local anyError
     if err then
       verbose.error(string.format("ERRO (%s): %s",name, err))
       anyError = true
     else
       local memoryload, err = server.getmemoryload(name, maxold)
       if err then
           verbose.error(string.format("ERRO (%s): %s",name, err))
           anyError = true
       else
         mfree_ram_perc = 0
         mfree_swap_perc = 0
         if (memory.ram > 0) then
           mfree_ram_perc = ( 1 - memoryload.ram / memory.ram) * 100
         end
         if (memory.swap > 0) then
           mfree_swap_perc = ( 1 - memoryload.swap / memory.swap) * 100
         end
       end
     end

     local cpu_load, err = server.getcpuload(name, maxold)
     if err then
        verbose.error(string.format("ERRO (%s): %s",name, err))
        anyError = true
        cpu_load = self.ERROR_CODE
        if last.load_avg_perc then
           cpu_load = last.load_avg_perc.loadAvg1min or self.ERROR_CODE
        end
     end

     if not SGA_DAEMON.loadAvg[name] then
       SGA_DAEMON.loadAvg[name] = SGA_DAEMON:new_loadAvg()
     end

     local avg
     if not err then
         SGA_DAEMON.loadAvg[name]:acc1(cpu_load, maxold)
         avg = SGA_DAEMON.loadAvg[name]:getLoad()
      else
         avg = SGA_DAEMON.loadAvg[name]:getLoad()
         avg.loadAvg1min = self.ERROR_CODE
     end


     njobs, err = server.getnumberofjobs(name, maxold)

     if err then
       verbose.error(string.format("ERRO (%s): %s", name, err))
       anyError = true
       njobs = self.ERROR_CODE
     end

     verbose.top(
       string.format("EXECUTANDO COLETA (ram:%d/swap:%d)(load:%d/%d/%d).",
         mfree_ram_perc, mfree_swap_perc,
         avg.loadAvg1min, avg.loadAvg5min, avg.loadAvg15min))

     return { name = name,
              number_of_jobs = njobs,
              memory_ram_free_perc = mfree_ram_perc,
              memory_swap_free_perc = mfree_swap_perc,
              load_avg_perc = avg,
              capacities =  {}
     }, anyError
  end

  local nodes = self:getNodesStaticInfo()
  local maxold = freqTb.freq 

  if not self.currentNode then
     self.currentNode = 1
     self.dynamic_data = {}
     self.disregard_times = {}
  end

  local err
  local nname = nodes[self.currentNode].name
  local currdata = self.dynamic_data[self.currentNode]
  local dtimes = self.disregard_times[self.currentNode] or
                 SGAD_CONF.disregard_times or 0

  if currdata and dtimes > 0 then
     self.dynamic_data[self.currentNode], err = update(nname, currdata, maxold)
  else
     self.dynamic_data[self.currentNode], err = update(nname, {}, maxold)
  end
  if err then
    if dtimes == 0 then
      self.disregard_times[self.currentNode] = 0
    else
      self.disregard_times[self.currentNode] = dtimes - 1
    end
  else
    self.disregard_times[self.currentNode] = nil
  end

  if #nodes > 1 then
     if self.currentNode == #nodes then
        freqTb.wakeme = freqTb.wakemeNext
     else
        if self.currentNode == 1 then
           freqTb.wakemeNext = freqTb.wakeme
        end

        local NOW, err = SGAD_CONF.serverManager.now()
        if err then
          NOW = SGA_DAEMON.INF 
        end
        freqTb.wakeme = NOW
     end
     self.currentNode = (self.currentNode % #nodes) + 1
  end
  if self.currentNode == 1 then
     -- Verifica se há falha no acesso ao disco.
     local hasDiskAccess = self:checkDiskAccess()
    
     -- Obtém a infomação dos jobs em execução no SGA
     local jobsInfo = SGAD_CONF.serverManager.getjobsinfo() or ""

     -- Envia os dados atualizados para o servidor
     local ssi = SGA_DAEMON.ssi
     local myname = self:getName()
     local dynamicInfo = {
       hasDiskAccess = hasDiskAccess,
       jobsInfo = jobsInfo,
       nodesInfo = self.dynamic_data,
     }
  
     -- A versão atual do luaorb não está tratando as exceções de
     -- comunicação com o servidor. Utilizar pcall para evitar
     -- a parada do SGA.
     -- local wasUpdated = self.ssi:updateSGAInfo(self.servant, myname,
     -- self.dynamic_data)
     local suc, wasUpdated = scheduler.pcall(self.ssi.updateSGAInfo, self.ssi,
       self.servant, myname, dynamicInfo)
     if not suc or not wasUpdated then
       verbose.error("Falha no envio de dados do SGA.")
       return nil
     end
  end
end

--..........................................................................

function SGA_DAEMON:isEnabled()
  return self.is_enabled;
end

--..........................................................................

function SGA_DAEMON:executeCommand( comm_string, comm_id, host, path,
  output_path, sandbox_paths) 
  if host == "" then host = nil end
  local comm = Command{ 
     command = comm_string, 
     id = comm_id,
     path = path,
     output_path = output_path,
     host = host,
	 sandbox_paths = sandbox_paths,
  }
  if comm then COMMAND:addCommandToList(comm) end
  return comm
end

--..........................................................................

function SGA_DAEMON:getHostName()
  return SGAD_CONF.name or self:getNodeNames()[1]
end
--..........................................................................

function SGA_DAEMON:getCSFSAddress()
  local address = {}
  address.host = (CSFS.properties and CSFS.properties.HOST) or self.NO_VALUE
  address.port = (CSFS.properties and tonumber(CSFS.properties.PORT)) or self.ERROR_CODE
  return address
end
--..........................................................................

function SGA_DAEMON:getCSFSRootDir()
  return (not CSFS.use_local_root_directories and CSFS.properties and CSFS.properties.CANONICAL_ROOT_DIR) or self.NO_VALUE
end
