/*
**  GXSNMP -- An SNMP management application.
**
**  This program is free software; you can redistribute it and/or modify
**  it under the terms of the GNU General Public License as published by
**  the Free Software Foundation; either version 2 of the License, or
**  (at your option) any later version.
**
**  This program is distributed in the hope that it will be useful,
**  but WITHOUT ANY WARRANTY; without even the implied warranty of
**  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
**  GNU General Public License for more details.
**
**  You should have received a copy of the GNU General Public License
**  along with this program; if not, write to the Free Software
**  Foundation, Inc.,  59 Temple Place - Suite 330, Cambridge, MA 02139, USA.
**
**  config.c -- Configuration file handling for John's collector.
**  Heavily stolen from Jochen's configuration file parser.
*/

#include <config.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <glib.h>

#include "config_file.h"

/*
**  The configuration is stored in a cascaded hash table. Each element
**  of the table can be either a string, numeric or again a hash table.
**  This is a private, opaque data structure.
*/

enum {
  CONFIG_NODE_NOTYPE,
  CONFIG_NODE_NUMBER,
  CONFIG_NODE_STRING,
  CONFIG_NODE_TABLE
};

struct config_node {
  gchar       * key;		/* Pointer to key for this node */
  gint          type;		/* Type of this node */
  union value {
    gint         number;	/* Numerical value of node */
    gchar      * string;	/* Pointer to string value of node */
    GHashTable * table;		/* Pointer to hash table of more nodes */
  } value;
};

/******************************************************************************
**
**  API subroutine config_node_is_number()
**
**  Returns TRUE if the node contains a number, FALSE otherwise.
**
******************************************************************************/

gboolean
config_node_is_number (CONFIG_NODE * node)
{
  if (!node) return FALSE;
  return (node->type == CONFIG_NODE_NUMBER);
}

/******************************************************************************
**
**  API subroutine config_node_is_string()
**
**  Returns TRUE if the node contains a string, FALSE otherwise.
**
******************************************************************************/

gboolean
config_node_is_string (CONFIG_NODE * node)
{
  if (!node) return FALSE;
  return (node->type == CONFIG_NODE_STRING);
}

/******************************************************************************
**
**  API subroutine config_node_is_list()
**
**  Returns TRUE if the node contains a list of nodes, FALSE otherwise.
**
******************************************************************************/

gboolean
config_node_is_list (CONFIG_NODE * node)
{
  if (!node) return FALSE;
  return (node->type == CONFIG_NODE_TABLE);
}

/******************************************************************************
**
**  API subroutine config_node_new()
**
**  Creates a new, untyped node.
**
**  Before inserting the node in the configuration tree, the user should
**  call either config_node_set_number, config_node_set_string, or 
**  config_node_set_list to establish the type of the node and initialize
**  it with a value.
**
**  It is the responsibility of the caller to use config_node_insert to
**  insert the node into the configuration tree.
**
**  Returns a pointer to the new node if successful, NULL otherwise.
**
******************************************************************************/

CONFIG_NODE *
config_node_new (gchar       * key)
{
  CONFIG_NODE * node;

  if (!key) return NULL;

  node               = (CONFIG_NODE *) g_malloc (sizeof (CONFIG_NODE));
  node->key          = g_strdup (key);
  node->type         = CONFIG_NODE_NOTYPE;

  return node;
}


/******************************************************************************
**
**  API subroutine config_node_new_number()
**
**  Creates a new number node.
**
**  It is the responsibility of the caller to use config_node_insert to
**  insert the node into the configuration tree.
**
**  Returns a pointer to the new node if successful, NULL otherwise.
**
******************************************************************************/

CONFIG_NODE *
config_node_new_number (gchar       * key,
			gint          number)
{
  CONFIG_NODE * node;

  if (!key) return NULL;

  node               = (CONFIG_NODE *) g_malloc (sizeof (CONFIG_NODE));
  node->type 	     = CONFIG_NODE_NUMBER;
  node->value.number = number;

  return node;
}

/******************************************************************************
**
**  API subroutine config_node_new_string()
**
**  Creates a new string node.
**
**  It is the responsibility of the caller to use config_node_insert to
**  insert the node into the configuration tree.
**
**  Returns a pointer to the new node if successful, NULL otherwise.
**
******************************************************************************/

CONFIG_NODE *
config_node_new_string (gchar       * key,
			gchar       * string)
{
  CONFIG_NODE * node;

  node               = (CONFIG_NODE *) g_malloc (sizeof (CONFIG_NODE));
  node->key          = g_strdup (key); 
  node->type         = CONFIG_NODE_STRING;
  node->value.string = g_strdup (string);

  return node;
}

/******************************************************************************
**
**  API subroutine config_node_new_list()
**
**  Adds a new node, containing an empty list, to an existing list node.
**  Returns a pointer to the new node if successful, NULL otherwise.
**
******************************************************************************/

CONFIG_NODE *
config_node_new_list (gchar       * key)
{
  CONFIG_NODE * node;

  node              = (CONFIG_NODE *) g_malloc (sizeof (CONFIG_NODE));
  node->key         = g_strdup (key);
  node->type        = CONFIG_NODE_TABLE;
  node->value.table = g_hash_table_new(g_str_hash, g_str_equal);

  return node;
}

/******************************************************************************
**
**  API subroutine config_node_insert()
**
**  Adds an existing node to an existing list node.
**  Returns TRUE if successful, FALSE otherwise.
**
******************************************************************************/

gboolean
config_node_insert (CONFIG_NODE * node,
                    CONFIG_NODE * parent)
{

  if (!node)                                        return FALSE;
  if (!parent)                                      return FALSE;
  if (parent->type != CONFIG_NODE_TABLE)            return FALSE;
  if (g_hash_table_lookup (parent->value.table,
                           node->key))              return FALSE;

  g_hash_table_insert(parent->value.table, node->key, node);

  return TRUE;
}

/******************************************************************************
**
**  API subroutine config_node_remove()
**
**  Removes a child node from a list node.
**  Returns TRUE if successful, FALSE otherwise.
**
******************************************************************************/

gboolean
config_node_remove (CONFIG_NODE * node,
                    CONFIG_NODE * parent)
{

  if (!node)                                        return FALSE;
  if (!parent)                                      return FALSE;
  if (parent->type != CONFIG_NODE_TABLE)            return FALSE;
  if (g_hash_table_lookup (parent->value.table,
                           node->key) != node)      return FALSE;

  g_hash_table_remove(parent->value.table, node->key);

  return TRUE;
}

/******************************************************************************
**
**  API subroutine config_node_destroy()
**
**  Destroys a node, freeing all memory allocated to that node.
**  If the target node is a list node, all nodes in that list are
**  recursively destroyed also.
**
**  Returns the number of nodes destroyed.
**
******************************************************************************/

static gboolean
do_destroy (gpointer key, gpointer value, gpointer user_data)
{
  CONFIG_NODE * node  = value;
  gint        * count = user_data;

  *count = *count + 1;
  if (node->type == CONFIG_NODE_STRING)
    g_free (node->value.string);

  if (node->type == CONFIG_NODE_TABLE)
    {
      g_hash_table_foreach_remove (node->value.table, do_destroy, count);
      g_hash_table_destroy (node->value.table);
    }
  g_free (node->key);
  return TRUE;
}

gint    
config_node_destroy (CONFIG_NODE * node)
{
  gint count = 0;

  if (!node) return 0;
  do_destroy (node->key, node, &count);
  return count;
}

/******************************************************************************
**
**  API subroutine config_node_get_number()
**
**  Returns the contents of a number node, or zero on error.
**
******************************************************************************/

gint
config_node_get_number (CONFIG_NODE * node)
{
  if (!node)                            return 0;
  if (node->type != CONFIG_NODE_NUMBER) return 0;

  return node->value.number;
}

/******************************************************************************
**
**  API subroutine config_node_get_string()
**
**  Returns the contents of a string node, or NULL on error.
**
******************************************************************************/

gchar *
config_node_get_string (CONFIG_NODE * node)
{
  if (!node)                            return NULL;
  if (node->type != CONFIG_NODE_STRING) return NULL;

  return node->value.string;
}

/******************************************************************************
**
**  API subroutine config_node_set_number()
**
**  Changes the contents of a number node, or changes an untyped node
**  into a number node and assigns a value.
**
**  Returns TRUE if successful, FALSE otherwise.
**
******************************************************************************/

gboolean
config_node_set_number (CONFIG_NODE * node,
			gint          number)
{
  if (!node)                            return FALSE;

  if (node->type == CONFIG_NODE_NOTYPE)
    node->type = CONFIG_NODE_NUMBER;

  if (node->type != CONFIG_NODE_NUMBER) return FALSE;

  node->value.number = number;

  return TRUE;
}

/******************************************************************************
**
**  API subroutine config_node_set_string()
**
**  Changes the contents of a string node, or changes an untyped node
**  into a string node and assigns a value.
**
**  Returns TRUE if successful, FALSE otherwise.
**
******************************************************************************/

gboolean
config_node_set_string (CONFIG_NODE * node,
			gchar       * string)
{
  if (!node) return FALSE;
  if (node->type == CONFIG_NODE_NOTYPE)
    {
      node->type         = CONFIG_NODE_STRING;
      node->value.string = NULL;
    }
  if (node->type != CONFIG_NODE_STRING) return FALSE;
  if (node->value.string) 
    g_free (node->value.string);
  node->value.string = g_strdup (string);
  return TRUE;
}

/******************************************************************************
**
**  API subroutine config_node_set_list()
**
**  Changes an untyped node into an empty list node.
**
**  Returns TRUE if successful, FALSE otherwise.
**
******************************************************************************/

gboolean
config_node_set_list (CONFIG_NODE * node)
{
  if (!node) return FALSE;
  if (node->type != CONFIG_NODE_NOTYPE)
    return FALSE;
  node->type        = CONFIG_NODE_TABLE;
  node->value.table = g_hash_table_new(g_str_hash, g_str_equal);
  return TRUE;
}

/******************************************************************************
**
**  API subroutine config_node_lookup()
**
**  Looks up a child node by name in an existing list node.
**  Returns a pointer to the child node if successful, NULL otherwise.
**
******************************************************************************/

CONFIG_NODE *
config_node_lookup (CONFIG_NODE * node, gchar * key)
{
  if (!node)                           return NULL;
  if (node->type != CONFIG_NODE_TABLE) return NULL;

  return g_hash_table_lookup (node->value.table, key);
}

/******************************************************************************
**
**  API subroutine config_node_foreach()
**
**  Calls a user-defined function once for each node in a list.
**  
******************************************************************************/

void
config_node_foreach (CONFIG_NODE * node, GHFunc function, gpointer user_data)
{
  if (!node)                           return;
  if (node->type != CONFIG_NODE_TABLE) return;

  g_hash_table_foreach (node->value.table, function, user_data);
}

/******************************************************************************
**
**  API subroutine config_load()
**
**  Builds an in-memory node structure from the contents of a disk file.
**  Returns a pointer to the root of the node structure if successful,
**  NULL otherwise.
**
******************************************************************************/

static CONFIG_NODE *
find_or_add_child_table_node (CONFIG_NODE * parent, GScanner * scanner)
{
  CONFIG_NODE * node;

  node = config_node_lookup (parent, scanner->value.v_string);
  if (node) 
    return config_node_is_list (node) ? node : NULL;
  node = config_node_new_list (scanner->value.v_string);
  return config_node_insert (node, parent) ? node : NULL;
}

/*
**  Internal subroutine used to read in the configuration file.
**  On entry, an identifier has just been parsed and is sitting
**  in the scanner structure.  do_load_insert completes the 
**  processing of the current statement, then returns.
*/

static gboolean
do_load_insert (CONFIG_NODE * parent, GScanner * scanner)
{
  GTokenType     token;
  CONFIG_NODE  * node;

  while (1) 
    {
      token = g_scanner_peek_next_token (scanner);
      switch (token)
	{
          case G_TOKEN_EOF:
            return FALSE;

          case ';':
            node = find_or_add_child_table_node (parent, scanner);
	    g_scanner_get_next_token (scanner);
	    return (node != NULL);

          case G_TOKEN_IDENTIFIER:
            node = find_or_add_child_table_node (parent, scanner);
            if (!node)
              return FALSE;
            g_scanner_get_next_token (scanner);
            return do_load_insert (node, scanner);

          case '{':
            node = find_or_add_child_table_node(parent, scanner);
	    if (!node) return FALSE;
            g_scanner_get_next_token (scanner);
            while (1)
              {
                token = g_scanner_get_next_token (scanner);
	        switch (token)
	          {
                    case G_TOKEN_IDENTIFIER:
                      if (!do_load_insert (node, scanner))
                        return FALSE;
                      break;

                    case '}':
		      token = g_scanner_get_next_token (scanner);
		      return (token == ';');

                    default:
                      return FALSE;
                  }
              }

          case '=':
            if (config_node_lookup(parent, scanner->value.v_string))
              return FALSE;
            node = config_node_new (scanner->value.v_string);
            g_scanner_get_next_token (scanner);
            token = g_scanner_get_next_token (scanner);
            switch (token)
              {
                case G_TOKEN_STRING:
		  config_node_set_string (node, scanner->value.v_string);
                  break;

                case G_TOKEN_INT:
		  config_node_set_number (node, scanner->value.v_int);
                  break;

                default:
                  return FALSE;
              }
            config_node_insert (node, parent);
            token = g_scanner_get_next_token (scanner);
            return (token == ';');

          default:
            return FALSE;
	}
    }
}

CONFIG_NODE *
config_load (gchar * filename)
{
  gint          fd;
  GScanner    * scanner;
  CONFIG_NODE * root_node;
  GTokenType    token;

  if (!filename) return NULL;

  fd = open (filename, O_RDONLY);
  if (fd < 0)
    {
      g_warning ("Unable to open config file %s: %s\n", filename, 
		 g_strerror (errno));
      return NULL;
    }

  root_node = config_node_new_list ("root");

  scanner = g_scanner_new (NULL);
  g_scanner_input_file (scanner, fd);
  scanner->input_name = filename;
  while (1) 
    {
      token = g_scanner_get_next_token (scanner);
      switch (token)
	{
	  case G_TOKEN_EOF:
	    g_scanner_destroy (scanner);
	    close (fd);
	    return root_node;
	  
          case G_TOKEN_IDENTIFIER:
            if (do_load_insert(root_node, scanner))
	      break;

          default:
	    g_scanner_error (scanner, "Error parsing config file\n");
	    g_scanner_destroy (scanner);
	    config_node_destroy (root_node);
	    return NULL;
	} 
    }
}

/******************************************************************************
**
**  API subroutine config_save()
**
**  This subroutine writes the configuration file out to a file.
**  Set the filename field to NULL to write to stdout.
**
******************************************************************************/

struct dump_work {
  FILE * file;
  gint   indent;
};

static void
do_dump(gpointer keyname, gpointer value, gpointer user_data)
{
  gchar            * key  = (gchar *)keyname;
  CONFIG_NODE      * node = (CONFIG_NODE *) value;
  struct dump_work * work = user_data;
  int                i;

  for (i=0; i < work->indent; i++)
    fprintf (work->file, "  ");
  fprintf(work->file, "%s", key);

  switch (node->type)
    {
      case CONFIG_NODE_NUMBER:
        fprintf (work->file, " = %d;\n", node->value.number);
        break;
      case CONFIG_NODE_STRING:
        fprintf(work->file, " = \"%s\";\n", node->value.string);
        break;
      case CONFIG_NODE_TABLE:
        i = g_hash_table_size (node->value.table);
        if (i > 0)
	  {
            fprintf(work->file, " {\n");
            work->indent++;
            g_hash_table_foreach (node->value.table, do_dump, user_data);
            work->indent--;
            for (i=0; i < work->indent; i++)
              fprintf(work->file, "  ");
	    fprintf (work->file, "}");
          }
        fprintf (work->file, ";\n");
        break;
      default:
        printf("This should never happen\n");
    }
}

gboolean
config_save (CONFIG_NODE * node, gchar * filename)
{
  struct dump_work work;

  work.indent = 0;

  if (!config_node_is_list (node))
    return FALSE;

  if (filename)
    {
      work.file = fopen (filename, "w");
      if (!work.file)
        {
          g_warning ("Unable to open output config file: %s\n",
                     g_strerror (errno));
          return FALSE;
        }
    }
  else
    work.file = stdout;

  g_hash_table_foreach (node->value.table, do_dump, &work);

  return TRUE;
};

/* EOF */
