/* inifile.c:
 *
 * vim:smartindent ts=8:sts=2:sta:et:ai:shiftwidth=2
 ****************************************************************
 * Copyright (C) 2005 Canonical Limited
 *        Authors: Robert Collins <robert.collins@canonical.com>
 *
 * See the file "COPYING" for further information about
 * the copyright and warranty status of this work.
 */


#include "hackerlab/bugs/panic.h"
#include "hackerlab/char/str.h"
#include "hackerlab/mem/alloc-limits.h"
#include "hackerlab/rx-posix/regex.h"
#include "hackerlab/vu/safe.h"
#include "libfsutils/file-contents.h"
#include "libinifile/inifile.h"

static t_uchar const *inifile_section_string = "^[[:space:]]*\\[([[:alnum:][:space:])]*)\\]";
static t_uchar const *inifile_non_comment = "^[[:space:]]*(;|#).*";
static t_uchar const *key_pattern_string = "^[[:space:]]*([[:alnum:]_)]+)[[:space:]]*=([^#;]*)(;(.*))?$";

/**
 * \brief load an inifile from disk
 *
 * initialises the inifile with the content of the file from disk.
 * \param inifile the inifile to be loaded into
 * \param filename the file to be loaded
 * \return 0 on success
 */
int
inifile_load (inifile_t *inifile, t_uchar const *filename)
{
    /* FIXME: check for file existence 20050308 RBC*/
    t_uchar *content = file_contents (filename);
    inifile_process_text (inifile, content);
    lim_free (0, content);
    return 0;
}

/**
 * \brief save a inifile merge to disk
 * \param inifile the inifile to save
 * \param merge_index the merge to save
 * \param filename the file to save
 */
int
inifile_save_merge (inifile_t *inifile, int merge_index, t_uchar const *filename)
{
    /* FIXME use non 'safe' calls and possibly fail 
     * 2000308 RBC */
    t_uchar *content = inifile_get_text_merge (inifile, merge_index);
    file_set_contents (filename, content);
    lim_free (0, content);
    return 0;
}

/**
 * \brief initialise an inifile
 *
 * \param inifile the inifile to be loaded into
 * \return 0 on success
 */
void
inifile_init (inifile_t *inifile)
{
    inifile->content = NULL;
}

/**
 * \brief process the text in an inifile 
 *
 * \param inifile the inifile to apply results to
 * \param content the text to process.
 * \return 0 on success
 */
int
inifile_process_text (inifile_t *inifile, t_uchar const *content)
{
    rel_table temp;
    temp = rel_nl_split (content);
    rel_add_records (&inifile->content, rel_make_record ("[]", 0), 0);
    rel_append_x (&inifile->content, temp);
    rel_free_table (temp);
    return 0;
}

/**
 * \brief get the text representation of an inifile
 *
 * \param inifile the inifile to retrieve
 * \param merge_index get the nth inifile from a merged inifile.
 * \return a heap allocated string
 */
t_uchar *
inifile_get_text_merge (inifile_t *inifile, int merge_index)
{
    int line;
    t_uchar *result=NULL;
    int copying = 0;
    int current_merge = 0;
    rel_for_each(inifile->content, line)
      {
        if (inifile_line_type (inifile->content[line][0]) == INIFILE_SECTION && !str_cmp("[]", inifile->content[line][0]))
            copying = current_merge++ == merge_index;
        else if (copying)
            result = str_replace (result, str_alloc_cat_many (0, result, inifile->content[line][0], "\n", str_end));
      }
    return result;
}
/**
 * \brief get the text representation of an inifile
 *
 * \param inifile the inifile to retrieve
 * \return a heap allocated string
 */
t_uchar *
inifile_get_text (inifile_t *inifile)
{
    return inifile_get_text_merge (inifile, 0);
}

/**
 * \brief free the resources of inifile
 */
void
inifile_finalise (inifile_t *inifile)
{
    rel_free_table (inifile->content);
    inifile->content = NULL;
}

/**
 * \brief helper function for getting a match from a regex pattern
 *
 * \param pattern a cached regex pattern
 * \param pattern_string the regex string to compile if needed
 * \param input the string to match against
 * \param match the nth match to return
 */
static t_uchar *
inifile_regex_get_match (regex_t **pattern, t_uchar const *pattern_string, t_uchar const *input, unsigned int match)
{
  regmatch_t matches[match];
  int regexres;

  if (!*pattern)
    {
      int re_error;
      *pattern = lim_malloc (0, sizeof (**pattern));
      re_error = regcomp (*pattern, pattern_string, REG_EXTENDED);
      invariant (!re_error);
    }
  if ((regexres = regexec (*pattern, input, match, matches, 0)))
    {
      char buf[50];
      if (regexres == REG_NOMATCH)
          return NULL;
      regerror (regexres, *pattern, buf, 50);
      safe_printfmt (2, "failed during regex match for section name: %d %s\n", regexres, buf);
      return NULL;
    }
  return str_save_n (0, input + matches[match - 1].rm_so, matches[match - 1].rm_eo - matches[match - 1].rm_so);
}

/**
 * \brief get the section name from a line
 * \param line the line
 * \return t_char * allocated on the heap, or NULL if not a section line.
 */
t_uchar *
inifile_section_name (t_uchar const *line)
{
  static regex_t * pattern = NULL;
  return inifile_regex_get_match (&pattern, inifile_section_string, line, 2);
}

/* the standard key patten */
static regex_t * key_pattern = NULL;
/* group 2 is the keyname, */

/**
 * \brief get the key name from a line
 *
 * note that comment lines have a key of ""
 * \param line the line
 * \return t_char * allocated on the heap, or NULL if not a key line.
 */
t_uchar *
inifile_key_name (t_uchar const *line)
{
  static regex_t * comment = NULL;
  t_uchar *result;
  result = inifile_regex_get_match (&key_pattern, key_pattern_string, line, 2);
  if (!result)
    {
      result = inifile_regex_get_match (&comment, inifile_non_comment, line, 2);
      if (result)
          result = str_replace (result, str_save (0, ""));
    }
  return result;
}

/**
 * \brief get the value from a line
 *
 * note that comment lines have no value
 * \param line the line
 * \return t_char * allocated on the heap.
 */
t_uchar *
inifile_value (t_uchar const *line)
{
  static regex_t * comment = NULL;
  t_uchar *result;
  result = inifile_regex_get_match (&key_pattern, key_pattern_string, line, 3);
  if (!result)
    {
      result = inifile_regex_get_match (&comment, inifile_non_comment, line, 2);
      if (result)
          result = str_replace (result, str_save (0, ""));
    }
  return result;
}

/**
 * \brief get the comment from a line
 *
 * \param line the line
 * \return t_char * allocated on the heap.
 */
t_uchar *
inifile_comment (t_uchar const *line)
{
  static regex_t * comment = NULL;
  t_uchar *result;
  result = inifile_regex_get_match (&key_pattern, key_pattern_string, line, 5);
  if (!result)
    {
      result = inifile_regex_get_match (&comment, inifile_non_comment, line, 2);
      if (result)
          result = str_replace (result, str_save (0, line));
    }
  return result;
}

/**
 * \brief get the list of keys a particular section has
 *
 * keys called "" are comment lines
 * \param inifile the inifile to query
 * \param section the section to return. "" is the top of the file
 * \return a rel_table containing the sections' key list.
 */
rel_table
inifile_get_section (inifile_t *inifile, t_uchar const *section)
{
    rel_table result = NULL;
    int line;
    t_uchar * current_section = str_save (0, "");
    rel_for_each (inifile->content, line)
      {
        if (inifile_line_type (inifile->content[line][0]) == INIFILE_SECTION)
          {
            current_section = str_replace (current_section, inifile_section_name (inifile->content[line][0]));
          }
        else if (!str_casecmp(current_section, section))
          {
            int index = 0;
            t_uchar *temp_key=inifile_key_name (inifile->content[line][0]);
            if (!temp_key)
                continue;
            while (index < rel_n_records (result) && str_casecmp (temp_key, result[index][0]))
                ++index;
            if (index == rel_n_records (result))
                rel_add_records (&result, rel_make_record (temp_key, 0), 0);
            lim_free (0, temp_key);
          }
      }
    lim_free (0, current_section);
    return result;
}

/**
 * \brief get the list of values (and comments) for a keyname in a particular section
 *
 * to get just comments, use "" as the key
 * \param inifile the inifile to query
 * \param section the section to return. "" is the top of the file
 * \param key the key to query.
 * \return a rel_table containing the keys values in field 0 and comment in field 1.
 */
rel_table
inifile_get_key_values (inifile_t *inifile, t_uchar const *section, t_uchar const *key)
{
    rel_table result = NULL;
    int line;
    t_uchar * current_section = str_save (0, "");
    rel_for_each (inifile->content, line)
      {
        if (inifile_line_type (inifile->content[line][0]) == INIFILE_SECTION)
          {
            current_section = str_replace (current_section, inifile_section_name (inifile->content[line][0]));
          }
        else if (!str_casecmp(current_section, section))
          {
            t_uchar *temp_key=inifile_key_name (inifile->content[line][0]);
            t_uchar *temp_value;
            t_uchar *temp_comment;
            if (str_casecmp (temp_key, key))
              {
                lim_free (0, temp_key);
                continue;
              }
            temp_value = inifile_value (inifile->content[line][0]);
            temp_comment = inifile_comment (inifile->content[line][0]);
            rel_add_records (&result, rel_make_record (temp_value, temp_comment/*, lineno */, 0), 0);
            lim_free (0, temp_key);
            lim_free (0, temp_value);
            lim_free (0, temp_comment);
          }
      }
    lim_free (0, current_section);
    return result;
}

/** 
 * \brief identify the type of a line from an ini file
 * \param line a line to identify
 * \return inifile_line_type_t
 */
inifile_line_type_t
inifile_line_type (t_uchar const *line)
{
    int position;
    int looking_for = 0;
    t_uchar *temp;
    /* shortcuts by regex. stuff efficiency ini files are small */
    if ((temp = inifile_section_name (line)))
      {
        lim_free (0, temp);
        return INIFILE_SECTION;
      }
    /* 0 ->  ; # a-zA-Z 
     * 1 -> =
     */
    for (position = 0; position < str_length (line); ++position)
      {
        switch (looking_for)
          {
          case 0:
          if (line[position] == ';')
              return INIFILE_COMMENT;
          else if (line[position] == '#')
              return INIFILE_COMMENT;
          else if (('a' <= line[position] && line[position] <= 'z') || 
                   ('A' <= line[position] && line[position] <= 'Z'))
              looking_for = 1;
          break;
          case 1:
          if (line[position] == '=')
              return INIFILE_KEY;
          break;
          }
      }
    return INIFILE_COMMENT;
}

/**
 * \brief create a ini file line
 */
static rel_record
inifile_make_line (t_uchar const *key, t_uchar const * value, t_uchar const * comment)
{
    t_uchar * temp_line;
    rel_record result;
    if (!str_length (key))
        temp_line = str_save (0, comment);
    else if (str_length (comment))
        temp_line = str_alloc_cat_many (0, key, "=", value, ";", comment, str_end);
    else
        temp_line = str_alloc_cat_many (0, key, "=", value, str_end);
    result = rel_make_record (temp_line, 0);
    lim_free (0, temp_line);
    return result;
}

/**
 * \brief add a new key to an inifile.
 *
 * the key is added at the last line of the last instance of the section in belongs in.
 * \param inifile the file to update
 * \param section the section the key belongs in ("" for no section).
 * \param key the key name ("" for a literal comment)
 * \param value the value for the key
 * \param comment the comment to use
 * \return void
 */
void
inifile_add_key (inifile_t *inifile, t_uchar const *section, t_uchar const *key, t_uchar const * value, t_uchar const * comment)
{
    int candidate_insertion = 0;
    int line;
    t_uchar * current_section = str_save (0, "");
    rel_for_each (inifile->content, line)
      {
        if (inifile_line_type (inifile->content[line][0]) == INIFILE_SECTION)
          {
            if (!str_casecmp (current_section, section))
                candidate_insertion = line;
            current_section = str_replace (current_section, inifile_section_name (inifile->content[line][0]));
          }
      }
    if (!rel_n_records (inifile->content))
        rel_add_records (&inifile->content, rel_make_record ("[]", 0), 0);
    
    if (!str_casecmp (current_section, section))
        candidate_insertion = rel_n_records (inifile->content);
    if (str_length (section) && candidate_insertion == 0)
      {
        t_uchar * temp_section = str_alloc_cat_many (0, "[", section, "]", str_end);
        rel_insert_records (&inifile->content, rel_n_records (inifile->content), rel_make_record (temp_section, 0), 0);
        candidate_insertion = rel_n_records (inifile->content);
        lim_free (0, temp_section);
      }
    rel_insert_records (&inifile->content, candidate_insertion, inifile_make_line (key, value, comment), 0);
    lim_free (0, current_section);
}

/**
 * \brief update an existing key with new value and comment
 * \param inifile the inifile
 * \param the key we are changing
 * \param key the key to update
 * \param index the index (from-0) of the keys values to change
 * \param value the value to set
 * \param comment the comment to set
 */
void
inifile_update_key (inifile_t *inifile, t_uchar const *section, t_uchar const *key, int index, t_uchar const * value, t_uchar const * comment)
{
    int seen_keys = 0;
    int line;
    t_uchar * current_section = str_save (0, "");
    rel_for_each (inifile->content, line)
      {
        if (inifile_line_type (inifile->content[line][0]) == INIFILE_SECTION)
          {
            current_section = str_replace (current_section, inifile_section_name (inifile->content[line][0]));
          }
        else if (!str_casecmp(current_section, section))
          {
                t_uchar *temp_key=inifile_key_name (inifile->content[line][0]);
                if (str_casecmp (temp_key, key))
                  {
                    lim_free (0, temp_key);
                    continue;
                  }
                /* matching key */
                if (seen_keys++ == index)
                  {
                    /* assign it a value */
                    rel_replace_record (inifile->content, line,inifile_make_line (key, value, comment));
                  }
                lim_free (0, temp_key);
          }
      }
    lim_free (0, current_section);
}

/**
 * \brief remove a key from an inifile
 * \param inifile the inifile
 * \param section the section we are looking for the key in
 * \param key the key to remove
 * \param index the index (from-0) of the keys values to remove (-1 for all)
 */
void 
inifile_remove_key (inifile_t *inifile, t_uchar const *section, t_uchar const *key, int index)
{
    int seen_keys = 0;
    int line;
    int line_to_remove = -1;
    t_uchar * current_section = str_save (0, "");
    /* factoring this out needs a keys_iterator built */
    rel_for_each (inifile->content, line)
      {
        if (inifile_line_type (inifile->content[line][0]) == INIFILE_SECTION)
          {
            current_section = str_replace (current_section, inifile_section_name (inifile->content[line][0]));
          }
        else if (!str_casecmp(current_section, section))
          {
                t_uchar *temp_key=inifile_key_name (inifile->content[line][0]);
                if (str_casecmp (temp_key, key))
                  {
                    lim_free (0, temp_key);
                    continue;
                  }
                /* matching key */
                if (seen_keys++ == index)
                  {
                    line_to_remove = line;
                  }
                lim_free (0, temp_key);
          }
      }
    lim_free (0, current_section);
    if (line_to_remove > -1)
        rel_remove_records (&inifile->content, line_to_remove, line_to_remove);
}

/** 
 * \brief retrieve a single-valued key, with an optional default value
 */
t_uchar *
inifile_get_single_string (inifile_t *inifile, t_uchar const * section, t_uchar const * key, t_uchar const * default_value)
{
    rel_table values;
    t_uchar *result;
    values = inifile_get_key_values (inifile, section, key);
    if (!rel_n_records (values))
	result = str_save (0, default_value);
    else
	result = str_save (0, values[rel_n_records (values) - 1][0]);

    rel_free_table (values);
    return result;
}

/** 
 * \brief set a single-valued key, adding it if it doesn't exist
 */
void
inifile_set_single_string (inifile_t *inifile, t_uchar const * section, t_uchar const * key, t_uchar const * value, t_uchar const * comment)
{
    rel_table values;
    values = inifile_get_key_values (inifile, section, key);
    if (!rel_n_records (values))
        inifile_add_key (inifile, section, key, value, comment);
    else
        inifile_update_key (inifile, section, key, rel_n_records (values) - 1, value, comment);

    rel_free_table (values);
}


syntax highlighted by Code2HTML, v. 0.9.1