#if defined(__FreeBSD__)
#define _WITH_GETLINE
#endif

#include "php_snuffleupagus.h"


static zend_result sp_process_config_root(sp_parsed_keyword *parsed_rule) {
  sp_config_keyword sp_func[] = {
    {parse_unserialize,         SP_TOKEN_UNSERIALIZE_HMAC, &(SPCFG(unserialize))},
    {parse_unserialize_noclass, SP_TOKEN_UNSERIALIZE_NOCLASS, &(SPCFG(unserialize_noclass))},
    {parse_enable,              SP_TOKEN_HARDEN_RANDOM, &(SPCFG(random).enable)},
    {parse_log_media,           SP_TOKEN_LOG_MEDIA, &(SPCFG(log_media))},
    {parse_uint,               SP_TOKEN_LOG_MAX_LEN, &(SPCFG(log_max_len))},
    {parse_disabled_functions,  SP_TOKEN_DISABLE_FUNC, NULL},
    {parse_readonly_exec,       SP_TOKEN_READONLY_EXEC, &(SPCFG(readonly_exec))},
    {parse_enable,              SP_TOKEN_GLOBAL_STRICT, &(SPCFG(global_strict).enable)},
    {parse_upload_validation,   SP_TOKEN_UPLOAD_VALIDATION, &(SPCFG(upload_validation))},
    {parse_cookie,              SP_TOKEN_COOKIE_ENCRYPTION, NULL},
    {parse_global,              SP_TOKEN_GLOBAL, NULL},
    {parse_enable,              SP_TOKEN_AUTO_COOKIE_SECURE, &(SPCFG(auto_cookie_secure).enable)},
    {parse_enable,              SP_TOKEN_XXE_PROTECTION, &(SPCFG(xxe_protection).enable)},
    {parse_eval_filter_conf,    SP_TOKEN_EVAL_BLACKLIST, &(SPCFG(eval).blacklist)},
    {parse_eval_filter_conf,    SP_TOKEN_EVAL_WHITELIST, &(SPCFG(eval).whitelist)},
    {parse_session,             SP_TOKEN_SESSION_ENCRYPTION, &(SPCFG(session))},
    {parse_enable,              SP_TOKEN_SLOPPY_COMPARISON, &(SPCFG(sloppy).enable)},
    {parse_wrapper_whitelist,   SP_TOKEN_ALLOW_WRAPPERS, &(SPCFG(wrapper))},
    {parse_ini_protection,      SP_TOKEN_INI_PROTECTION, &(SPCFG(ini))},
    {parse_ini_entry,           SP_TOKEN_INI, NULL},
    {NULL, NULL, NULL}};
  return sp_process_rule(parsed_rule, sp_func);
}

zend_result sp_parse_config(const char *const filename) {
  FILE *fd = fopen(filename, "rb");
  if (fd == NULL) {
    sp_log_err("config", "Could not open configuration file %s : %s", filename, strerror(errno));
    return FAILURE;
  }

  size_t step = 8192;
  size_t max_len = step, len = 0;
  zend_string *data = zend_string_alloc(max_len, 0);
  char *ptr = ZSTR_VAL(data);

  size_t bytes;
  while ((bytes = fread(ptr, 1, max_len - len, fd))) {
    len += bytes;
    if (max_len - len <= 0) {
      max_len += step;
      data = zend_string_extend(data, max_len, 0);
      ptr = ZSTR_VAL(data) + len;
    } else {
      ptr += bytes;
    }
  }
  fclose(fd);

  data = zend_string_truncate(data, len, 0);
  ZSTR_VAL(data)[len] = 0;

  int ret = sp_config_scan(ZSTR_VAL(data), sp_process_config_root);

  zend_string_release_ex(data, 0);

  return ret;
}


zend_result sp_process_rule(sp_parsed_keyword *parsed_rule, const sp_config_keyword *const config_keywords) {
  for (sp_parsed_keyword *kw = parsed_rule; kw->kw; kw++) {
    bool found_kw = false;
    for (const sp_config_keyword *ckw = config_keywords; ckw->func; ckw++) {
      if (kw->kwlen == strlen(ckw->token) && !strncmp(kw->kw, ckw->token, kw->kwlen)) {
        if (ckw->func) {
          int ret = ckw->func(ckw->token, kw, ckw->retval);
          switch (ret) {
            case SP_PARSER_SUCCESS:
              break;
            case SP_PARSER_ERROR:
              return FAILURE;
            case SP_PARSER_STOP:
              return SUCCESS;
          }
        }
        found_kw = true;
        break;
      }
    }

    if (!found_kw) {
      zend_string *kwname = zend_string_init(kw->kw, kw->kwlen, 0);
      sp_log_err("config", "Unexpected keyword '%s' on line %zu", ZSTR_VAL(kwname), kw->lineno);
      zend_string_release_ex(kwname, 0);
      return FAILURE;
    }
  }
  return SUCCESS;
}

#define CHECK_DUPLICATE_KEYWORD(retval) \
  if (*(void**)(retval)) { \
    sp_log_err("config", "duplicate keyword '%s' on line %zu", token, kw->lineno); \
    return SP_PARSER_ERROR; }


SP_PARSEKW_FN(parse_empty) {
  if (kw->arglen) {
    sp_log_err("config", "Unexpected argument for keyword '%s' - it should be '%s()' on line %zu", token, token, kw->lineno);
    return SP_PARSER_ERROR;
  }
  if (kw->argtype != SP_ARGTYPE_EMPTY) {
    sp_log_err("config", "Missing parenthesis for keyword '%s' - it should be '%s()' on line %zu", token, token, kw->lineno);
    return SP_PARSER_ERROR;
  }
  *(bool *)retval = true;
  return SP_PARSER_SUCCESS;
}

SP_PARSEKW_FN(parse_list) {
  CHECK_DUPLICATE_KEYWORD(retval);

  sp_list_node **list = retval;

  SP_PARSE_ARG(value);

  char* tmp = ZSTR_VAL(value);
  while (1) {
    const char* const tok = strsep(&tmp, ",");
    if (tok == NULL) {
      break;
    }
    *list = sp_list_insert(*list, zend_string_init(tok, strlen(tok), 1));
  }
  zend_string_release(value);

  return SP_PARSER_SUCCESS;
}

SP_PARSEKW_FN(parse_php_type) {
  SP_PARSE_ARG(value);

  if (zend_string_equals_literal_ci(value, "undef")) {
    *(sp_php_type *)retval = SP_PHP_TYPE_UNDEF;
  } else if (zend_string_equals_literal_ci(value, "null")) {
    *(sp_php_type *)retval = SP_PHP_TYPE_NULL;
  } else if (zend_string_equals_literal_ci(value, "true")) {
    *(sp_php_type *)retval = SP_PHP_TYPE_TRUE;
  } else if (zend_string_equals_literal_ci(value, "false")) {
    *(sp_php_type *)retval = SP_PHP_TYPE_FALSE;
  } else if (zend_string_equals_literal_ci(value, "long")) {
    *(sp_php_type *)retval = SP_PHP_TYPE_LONG;
  } else if (zend_string_equals_literal_ci(value, "double")) {
    *(sp_php_type *)retval = SP_PHP_TYPE_DOUBLE;
  } else if (zend_string_equals_literal_ci(value, "string")) {
    *(sp_php_type *)retval = SP_PHP_TYPE_STRING;
  } else if (zend_string_equals_literal_ci(value, "array")) {
    *(sp_php_type *)retval = SP_PHP_TYPE_ARRAY;
  } else if (zend_string_equals_literal_ci(value, "object")) {
    *(sp_php_type *)retval = SP_PHP_TYPE_OBJECT;
  } else if (zend_string_equals_literal_ci(value, "resource")) {
    *(sp_php_type *)retval = SP_PHP_TYPE_RESOURCE;
  } else if (zend_string_equals_literal_ci(value, "reference")) {
    *(sp_php_type *)retval = SP_PHP_TYPE_REFERENCE;
  } else {
    zend_string_release(value);
    sp_log_err("error", ".%s() is expecting a valid php type ('false', 'true',"
                " 'array'. 'object', 'long', 'double', 'null', 'resource', "
                "'reference', 'undef') on line %zu", token, kw->lineno);
    return SP_PARSER_ERROR;
  }
  zend_string_release(value);
  return SP_PARSER_SUCCESS;
}


SP_PARSEKW_FN(parse_str) {
  CHECK_DUPLICATE_KEYWORD(retval);
  SP_PARSE_ARG(value);

  *(zend_string **)retval = value;

  return SP_PARSER_SUCCESS;
}

SP_PARSEKW_FN(parse_int) {
  int ret = SP_PARSER_SUCCESS;
  SP_PARSE_ARG(value);

  char *endptr;
  errno = 0;
  *(int*)retval = (int)strtoimax(ZSTR_VAL(value), &endptr, 10);
  if (errno != 0 || !endptr || endptr == ZSTR_VAL(value)) {
    sp_log_err("config", "Failed to parse arg '%s' of `%s` on line %zu", ZSTR_VAL(value), token, kw->lineno);
    ret = SP_PARSER_ERROR;
  }
  zend_string_release(value);
  return ret;
}

SP_PARSEKW_FN(parse_uint) {
  int ret = SP_PARSER_SUCCESS;
  SP_PARSE_ARG(value);

  char *endptr;
  errno = 0;
  *(u_int*)retval = (u_int)strtoul(ZSTR_VAL(value), &endptr, 10);
  if (errno != 0 || !endptr || endptr == ZSTR_VAL(value)) {
    sp_log_err("config", "Failed to parse arg '%s' of `%s` on line %zu", ZSTR_VAL(value), token, kw->lineno);
    ret = SP_PARSER_ERROR;
  }
  zend_string_release(value);
  return ret;
}

SP_PARSEKW_FN(parse_cidr) {
  CHECK_DUPLICATE_KEYWORD(retval);
  SP_PARSE_ARG(value);

  sp_cidr *cidr = pecalloc(1, sizeof(sp_cidr), 1);

  if (0 != get_ip_and_cidr(ZSTR_VAL(value), cidr)) {
    pefree(cidr, 1);
    cidr = NULL;
  }

  *(sp_cidr **)retval = cidr;
  return cidr ? SP_PARSER_SUCCESS : SP_PARSER_ERROR;
}

SP_PARSEKW_FN(parse_regexp) {
  CHECK_DUPLICATE_KEYWORD(retval);
  SP_PARSE_ARG(value);

  sp_regexp *compiled_re = sp_regexp_compile(value);
  if (!compiled_re) {
    sp_log_err("config", "Invalid regexp '%s' for '.%s()' on line %zu", ZSTR_VAL(value), token, kw->lineno);
    zend_string_release_ex(value, 1);
    return SP_PARSER_ERROR;
  }

  *(sp_regexp **)retval = compiled_re;

  return SP_PARSER_SUCCESS;
}

void sp_free_disabled_function(void *data) {
  sp_disabled_function *df = data;

  sp_free_zstr(df->textual_representation);

  sp_free_zstr(df->filename);
  sp_regexp_free(df->r_filename);

  sp_free_zstr(df->function);
  sp_regexp_free(df->r_function);
  sp_list_free(df->functions_list, free);

  sp_free_zstr(df->hash);

  sp_tree_free(df->param);
  sp_regexp_free(df->r_param);

  sp_regexp_free(df->r_ret);
  sp_free_zstr(df->ret);

  sp_regexp_free(df->r_value);
  sp_free_zstr(df->value);

  sp_regexp_free(df->r_key);
  sp_free_zstr(df->key);

  sp_free_zstr(df->dump);
  sp_free_zstr(df->alias);

  // sp_list_free(df->param_array_keys);
  // sp_list_free(df->var_array_keys);

  sp_tree_free(df->var);

  pefree(df->cidr, 1);
}

void sp_free_cookie(void *data) {
  sp_cookie *c = data;
  if (c->name)
    zend_string_release_ex(c->name, 1);
  sp_regexp_free(c->name_r);
}

void sp_free_zstr(void *data) {
  if (data) {
    zend_string_release_ex((zend_string*)data, 1);
  }
}

void sp_free_ini_entry(void *data) {
  sp_ini_entry *entry = data;

  sp_free_zstr(entry->key);
  sp_free_zstr(entry->min);
  sp_free_zstr(entry->max);
  sp_regexp_free(entry->regexp);
  sp_free_zstr(entry->msg);
  sp_free_zstr(entry->set);
}
