/**
 * Módulo SGA Unix.
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <signal.h>
#include <fcntl.h>
#include <time.h>
#include <assert.h>

#include "sgalib.h"
#include "sgautils.h"

/* Declaração das funções. */
static int _create_process(lua_State* L);
static int _kill_process(lua_State* L);

/* =================================================== */
/* Funções auxiliares                                  */
/* =================================================== */

/**
 * Coleta o signal dos filhos.
 *
 * @param signo Sinal enviado
 *
 */
void sig_chld(int signo) // ZOMBIE
{
  int stat;
  waitpid ( -1, &stat, WNOHANG );
}

/**
 * Prepara uma vetor de strings vindas de uma tabela Lua.
 *
 * @param  L Ambiente Lua.
 * @param table Índice na pilha de Lua que contém as strings.
 * @param field Nome do campo que contém uma tabela de strings.
 *
 * @return Um vetor de strings.
 *
 */
static char**
_fill_env(lua_State *L, int table, const char* field)
{
  lua_pushstring(L, field);
  lua_gettable(L, table);

  if (lua_istable(L, -1)) {
    int n = luaL_getn(L, -1);  // n = size of "env" table
    char** env = (char**) malloc((n+1)*sizeof(char*));
    if (env == NULL) {
      fprintf(stderr,"Memory Overflow (malloc).");
      exit(-1);
    }

    int i;
    for(i = 1; i <= n ; i++) {
      lua_pushnumber(L, (double)i);
      lua_gettable(L, -2);
      const char* str = lua_tostring(L, -1);
      env[i-1] = strdup(str);
      if (env[i-1] == NULL) {
        fprintf(stderr,"Memory Overflow (strdup).");
        exit(-1);
      }
      lua_pop(L, 1);
    }
    env[n] = NULL;

    return env;
  }

  return NULL;
}

/**
 * Função auxiliar para recuperar uma string de Lua.
 *
 * @param L Ambiente Lua.
 * @param table Índice da tabela que contém a string.
 * @param field Nome do campo a ser recuperado.
 *
 * @return A string referente ao campo ou NULL caso o campo não seja string
 *
 */
static const char*
_get_string_field(lua_State* L, int table, const char* field)
{
  lua_pushstring(L, field);
  lua_gettable(L,table);

  if(lua_isstring(L, -1))
    return lua_tostring(L, -1);

  return NULL;
}


/**
 * Reconfigura todos os sinais para o comportamento padrão.
 *
 */
static void
_reset_signals()
{
  struct sigaction sa;

  sa.sa_handler = SIG_DFL;
  sigemptyset(&sa.sa_mask);
  sa.sa_flags = 0;

  for (int sig = 1; sig <= 64; sig++)
    sigaction(sig, &sa, (struct sigaction *)0);
}

/**
 * Fecha todos os descritores de arquivos, exceto stdin, stdout e stderr,
 * os quais são reposicionados para 0, 1 e 2, respectivamente.
 *
 * @param fd_in stdin
 * @param fd_out stdout
 * @param fd_err stderr
 *
 */
static void
_adjust_fds(int fd_in, int fd_out, int fd_err)
{
  // Fecha todos os descritores exceto stdin, stdout e stderr
  int maxFds = sysconf(_SC_OPEN_MAX);
  if(maxFds <= 0) {
    fprintf(stderr,"Child process: sysconf() failed.");
    exit(-1);
  }
  for(int i = 0 ; i < maxFds ; i++)
    if (i != fd_in && i != fd_out && i != fd_err)
      ::close(i);

  int fd = dup2(fd_in, 0);  //stdin
  assert(fd == 0);
  fd = dup2(fd_out, 1);     //stdout
  assert(fd == 1);
  fd = dup2(fd_err, 2);     //stderr
  assert(fd == 2);

  if (fd_in > 2)
    close(fd_in);
  if (fd_out > 2)
    close(fd_out);
  if (fd_err > 2)
    close(fd_err);
}

/**
 * Recupera o descritor de arquivo da tabela Lua.
 * Caso o campo aponte para uma string, este representa um arquivo
 * que deve ser aberto e seu descritor retornado.
 *
 * @param L Ambiente Lua.
 * @param table Tabela que contém o descritor.
 * @param field Campo da tabela que aponta para o descritor.
 * @param flags Flags utilizados na abertura do arquivo.
 * @param mode Modo de abertura do arquivo.
 *
 * @return Descritor para um arquivo.
 */
static int
_get_file_descriptor(lua_State* L, int table, const char* field, int flags,
  mode_t mode = 0)
{
  int fd = -1;
  lua_pushstring(L, field);
  lua_gettable(L, table);

  if(lua_isuserdata(L, -1)) {
    FILE** stream = (FILE**)lua_touserdata(L,-1);
    fd = fileno(*stream);
  }
  else if(lua_isnumber(L,-1)) {
    fd = (int) lua_tonumber(L, -1);
  }
  else if(lua_isstring(L,-1)) {
    const char* file_name = lua_tostring(L, -1);
    fd = open(file_name, flags, mode);
  }

  // Utiliza o descritor 'null' caso não nenhum outro esteja disponível
  if(fd <= 0)
    fd = open("/dev/null", O_RDWR);

  return fd;
}

/**
 * Retorna o PID atual do processo.
 *
 * @return PID atual do processo.
 *
 */
static int
_getpid(void)
{
  return (int)getpid();
}

/* =================================================== */
/* Funções exportadas para o ambiente de execução Lua. */
/* =================================================== */

/**
 * Dispara um novo processo. Se for informado ela configura as
 * variáveis de ambiente e os parâmetros de entrada.
 *
 * @param L Ambiente Lua.
 *
 */
static void
_new_process(lua_State* L)
{
  const char* commandline;
  const char* dir = NULL;
  char** argv = NULL;
  char** env = NULL;
  int fd_in = -1, fd_out = -1, fd_err = -1;

  umask(022);

  if(lua_isstring(L, 1))
    commandline = lua_tostring(L, 1);
  else {
    commandline = _get_string_field(L, 1, "cmd");
    if (commandline == NULL) {
      lua_pushstring(L, "The cmd field must be provided");
      lua_error(L);
    }

    // Diretório de execução
    dir = _get_string_field(L, 1, "dir");

    // Recupera os argumentos e as variáveis de ambiente.
    // Não é necessário liberar a memória alocada -- execv() ou exit(), abaixo
    argv = _fill_env(L,1,"argv");
    env = _fill_env(L,1,"env");

    // Recupera os descritores para stdin, stdout e stderr
    fd_in = _get_file_descriptor(L, 1, "input", O_RDONLY);
    fd_out = _get_file_descriptor(L, 1, "output",  O_RDWR | O_CREAT, 0664);
    fd_err = _get_file_descriptor(L, 1, "err_output",  O_RDWR | O_CREAT, 0664);
  }

  // Cria uma nova sessão para o processo filho
  setsid();

  // Ajusta os descritores stdin, stdout e stderr (fechando o restante)
  _adjust_fds(fd_in, fd_out, fd_err);

  // Reinicia o comportamento dos sinais
  _reset_signals();

  if(dir)
    chdir(dir);

  if(env) {
    execve(commandline, argv, env);
    fprintf(stderr,"Child process: execve() failed.");
  }
  else {
    execvp(commandline, argv);
    fprintf(stderr,"Child process: execv() failed.");
  }

  exit(-1);
}

/**
 * Coloca o processo para dormir por um determinado número de segundos.
 *
 */
static int
_sleep(lua_State* L)
{
  // Tempo do sleep
  long dtime = (long) luaL_checknumber(L,1);

  sleep(dtime);

  return 0;
}

/**
 * Finaliza a execução de um processo através do PID.
 *
 */
static int
_kill_process(lua_State* L)
{
  // Recupera o PID do processo
  pid_t pid = (pid_t) luaL_checknumber(L,1);

  kill(pid,SIGKILL);

  return 0;
}

/**
 * Realiza o fork do processo pai e dispara um novo processo.
 * O pai recebe o PID do processo filho.
 *
 */
static int
_create_process(lua_State* L)
{
  luaL_argcheck(L, lua_istable(L,1) || lua_isstring(L,1),
    1, "o argumento deve ser uma tabela ou uma string");

  pid_t pid = fork();
  if (pid == 0)
    _new_process(L);
  else
    lua_pushnumber(L,pid);

  return 1;
}

/**
 * Fornece a hora em segundos desde Epoch (1 de janeiro de 1970).
 *
 */
static int
_now(lua_State* L) {
   time_t tm = time(NULL);

  if (tm != ((time_t)-1) )
     lua_pushnumber(L, tm);
  else
     lua_pushnil(L);

  return 1;
}

/**
 * Registra funções auxiliares no ambiente Lua.
 *
 */
static void
init_proc_utils(lua_State *L)
{
  // @TODO Cadastrar em uma tabela separada
  lua_register(L, "unix_create_process", _create_process);
  lua_register(L, "kill_process", _kill_process);
  lua_register(L, "now", _now);
  lua_register(L, "sleep", _sleep);
}

/**
 * Função principal invocada para configurar o ambiente de execução
 * para o SGA Unix.
 *
 */
int luaopen_sga(lua_State *L) {
  // Ajuste de uma variável global para ajustar o pid do próprio sga-daemon
  int sga_pid = _getpid();

  // Inicialização das funcionalidades de kernel C relativo ao tratamento
  // de processos (questões de portabilidade são abstraídas aqui).
  init_proc_utils(L);

  // Captura o signal enviado pela finalização de um processo filho
  signal(SIGCHLD, sig_chld); // ZOMBIE

  lua_pushnumber(L, sga_pid);
  lua_setglobal(L, "SGA_PID");

  sga_require(L, "lib.Unix");

  return 0;
}
