/****************************************************************************
 * apps/fsutils/inifile/inifile.c
 *
 *   Copyright (C) 2014 Gregory Nutt. All rights reserved.
 *   Author: Gregory Nutt <gnutt@nuttx.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name NuttX nor the names of its contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <debug.h>

#include "fsutils/inifile.h"

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

/* The maximum size of a line in the INI file */

#ifndef CONFIG_FSUTILS_INIFILE_MAXLINE
#  define CONFIG_FSUTILS_INIFILE_MAXLINE 256
#endif

#ifndef CONFIG_FSUTILS_INIFILE_DEBUGLEVEL
#  define CONFIG_FSUTILS_INIFILE_DEBUGLEVEL 0
#endif

#ifdef CONFIG_CPP_HAVE_VARARGS
#  if CONFIG_FSUTILS_INIFILE_DEBUGLEVEL > 0
#    define inidbg(format, ...) \
       printf(EXTRA_FMT format EXTRA_ARG, ##__VA_ARGS__)
#  else
#    define inidbg(x...)
#  endif

#  if CONFIG_FSUTILS_INIFILE_DEBUGLEVEL > 1
#    define iniinfo(format, ...) \
       printf(EXTRA_FMT format EXTRA_ARG, ##__VA_ARGS__)
#  else
#    define iniinfo(x...)
#  endif
#else
#  if CONFIG_FSUTILS_INIFILE_DEBUGLEVEL > 0
#    define inidbg printf
#  else
#    define inidbg (void)
#  endif

#  if CONFIG_FSUTILS_INIFILE_DEBUGLEVEL > 1
#    define iniinfo printf
#  else
#    define iniinfo (void)
#  endif
#endif

/****************************************************************************
 * Private Types
 ****************************************************************************/

/* A structure that describes one entry from the INI file */

struct inifile_var_s
{
  FAR char *variable;
  FAR char *value;
};

/* This structure describes the state of one instance of the INI file parser */

struct inifile_state_s
{
  FILE *instream;
  int   nextch;
  char  line[CONFIG_FSUTILS_INIFILE_MAXLINE+1];
};

/****************************************************************************
 * Private Data
 ****************************************************************************/

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

static bool inifile_next_line(FAR struct inifile_state_s *priv);
static int  inifile_read_line(FAR struct inifile_state_s *priv);
static int  inifile_read_noncomment_line(FAR struct inifile_state_s *priv);
static bool inifile_seek_to_section(FAR struct inifile_state_s *priv,
              FAR const char *section);
static bool inifile_read_variable(FAR struct inifile_state_s *priv,
              FAR struct inifile_var_s *varinfo);
static FAR char *
            inifile_find_section_variable(FAR struct inifile_state_s *priv,
              FAR const char *variable);
static FAR char *
            inifile_find_variable(FAR struct inifile_state_s *priv,
              FAR const char *section, FAR const char *variable);

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name:  inifile_next_line
 *
 * Description:
 *   Skip to the first character of the next line.  Returns true if the end
 *   of file was not encountered.
 *
 ****************************************************************************/

static bool inifile_next_line(FAR struct inifile_state_s *priv)
{
  /* Search ahead for the end of line mark (or possibly the end of file mark) */

  while ((priv->nextch != '\n') && (priv->nextch != EOF))
    {
      priv->nextch = getc(priv->instream);
    }

  /* Re-prime the pump with the first character from the next line.  NOTE:
   * this logic depends on the fact that getc() will return EOF repeatedly.
   */

  priv->nextch = getc(priv->instream);
  return (priv->nextch != EOF);
}

/****************************************************************************
 * Name:  inifile_read_line
 *
 * Description:
 *   Read the next line from the INI file into the line buffer and return
 *   the number of characters read into the buffer. If we hit the end of a
 *   section (or the end of a file), this function will return a count of
 *   zero.
 *
 ****************************************************************************/

static int inifile_read_line(FAR struct inifile_state_s *priv)
{
  int nbytes;

  /* Assuming that the file pointer is correctly positioned at the beginning
   * of the next line, read until the end of line indication is found (or
   * until the line buffer is full).  This is basically fgets().
   */

  nbytes = 0;
  while ((nbytes < CONFIG_FSUTILS_INIFILE_MAXLINE) &&
         (priv->nextch != EOF) &&
         (priv->nextch != '\n'))
   {
      /* Always ignore carriage returns */

      if (priv->nextch != '\r')
        {
          /* Ignore any leading whitespace on the line */

          if (nbytes || (priv->nextch != ' ' && priv->nextch != '\t'))
            {
              /* Add the new character to the line buffer */

              priv->line[nbytes] = priv->nextch;
              nbytes++;
            }
        }

      /* Get the next character from the INI file */

      priv->nextch = getc(priv->instream);
    }

  /* NUL terminate the string */

  priv->line[nbytes] = '\0';

  /* Skip to the first character of the next line.  This should normally
   * just amount to skipping over the newline, but could be more involved
   * if we had to truncate the line to fit into the line buffer.
   */

  if (priv->nextch != EOF)
    {
      (void)inifile_next_line(priv);
    }

  /* And return the number of bytes read (excluding the NUL terminator and
   * leading whitespace).
   */

  return nbytes;
}

/****************************************************************************
 * Name:  inifile_read_noncomment_line
 *
 * Description:
 *   Read until either a (1) no further lines are found in the file, or (2)
 *   a line that does not begin with a semi-colon is found
 *
 ****************************************************************************/

static int inifile_read_noncomment_line(FAR struct inifile_state_s *priv)
{
  int nbytes;

  /* Read until either a (1) no further lines are found in
   * the file, or (2) a line that does not begin with a semi-colon
   * is found */

  do nbytes = inifile_read_line(priv);
  while (nbytes > 0 && priv->line[0] == ';');

  return nbytes;
}

/****************************************************************************
 * Name:  inifile_seek_to_section
 *
 * Description:
 *   Positions the file pointer to the line containing the first variable
 *   description within the INI file. Returns 1 if the section was found.
 *   In this case, the file pointer will be positioned at the beginning of
 *   the first variable line.
 *
 ****************************************************************************/

static bool inifile_seek_to_section(FAR struct inifile_state_s *priv,
                                    FAR const char *section)
{
  int nbytes;

  /* Rewind to the beginning of the INI file and re-prime the pump with the
   * first character from the INI file.
   */

  rewind(priv->instream);
  priv->nextch = getc(priv->instream);

  /* Loop until either the section is found, or until we hit the end of the
   * INI file.
   */

  do
    {
      /* Read the next line into the input buffer.  A returned value of zero
       * bytes means nothing special here -- could be EOF or a blank line.
       */

      nbytes = inifile_read_noncomment_line(priv);

      /* It takes at least three bytes of data to be a candidate for a
       * section header.
       */

      if (nbytes >= 3)
        {
          /* A section header must begin with a left bracket */

          if (priv->line[0] == '[')
            {
              /* The section name should start with the first character
               * after the left bracket.
               */

              FAR char *sectend;

              /* The section name should extend to the right bracket. */

              sectend = strchr(&priv->line[1], ']');

              /* Replace the right bracket (if found) with a NULL
               * terminator.
               */

              if (sectend)
                {
                  *sectend = '\0';
                }

              /* Then compare the section name to the one we are looking for */

              if (strcasecmp(&priv->line[1], section) == 0)
                {
                  /* The section names match!  Return success */

                  return true;
                }
            }
        }
    }
  while (priv->nextch != EOF);

  /* If we got here, we search the whole INI file without finding
   * the requested section
   */

  inidbg("ERROR: Section \"%s\" not found\n", section);
  return false;
}

/****************************************************************************
 * Name:  inifile_read_variable
 *
 * Description:
 *   Obtain variable info from the next line in the section.  This assumes
 *   that the file pointer is pointing to the beginning of the next line.
 *   If there is no further data in the section, false is returned.
 *
 ****************************************************************************/

static bool inifile_read_variable(FAR struct inifile_state_s *priv,
                                  FAR struct inifile_var_s *varinfo)
{
  FAR char *ptr;

  /* Read until either (1) the end of file is found, (2) the end of
   * the section is found, or (3) a valid variable assignment is found.
   */

  for (;;)
    {
      /* Read the next line in the buffer */

      int nbytes = inifile_read_noncomment_line(priv);

      /* Make sure that the line is non-NULL and that this is not the
       * beginning of a new section
       */

      if (!nbytes || priv->line[0] == '[')
       {
         return false;
       }

      /* Search for the '=' delimiter.  NOTE  the line is guaranteed to
       * be NULL terminated by inifile_read_noncomment_line().
       */

      ptr = strchr(&priv->line[1], '=');

      /* If the delimiter was found, return success */

      if (ptr)
        {
          /* Put NUL termination between the variable name and the
           * variable value (replacing the equal sign).
           */

          *ptr = '\0';

          /* Set up the return structure.  NOTE:  value may point at
           * a NULL string
           */

          varinfo->variable = (char*)priv->line;
          varinfo->value    = (ptr + 1);
          return true;
        }
    }
}

/****************************************************************************
 * Name:  inifile_find_section_variable
 *
 * Description:
 *   Find the value string associated with the variable name.  This function
 *   will return NULL on failure to find the variable.  It will return a
 *   pointer to an empty string is the variable is found, but not assigned a
 *   value.
 *
 ****************************************************************************/

static FAR char *
  inifile_find_section_variable(FAR struct inifile_state_s *priv,
    FAR const char *variable)
{
  /* Loop until either (1) we hit the end of file, (2) we hit the end
   * of the section, or (3) we find the variable that we are looking
   * for/
   */

  iniinfo("variable=\"%s\"\n", variable);

  for (;;)
    {
      /* Get the next variable from this section. */

      struct inifile_var_s varinfo;
      bool found = inifile_read_variable(priv, &varinfo);

      /* Is there anything left in the section? */

      if (!found)
        {
          iniinfo("Returning NULL\n");
          return NULL;
        }

      iniinfo("varinfo.variable=\"%s\"\n", varinfo.variable);

      /* Does the variable name match the one we are looking for? */

      if (strcasecmp(varinfo.variable, variable) == 0)
        {
          /* Yes... then we have it! */

          iniinfo("Returning \"%s\"\n", varinfo.value);
          return varinfo.value;
        }
    }
}

/****************************************************************************
 * Name:  inifile_find_variable
 *
 * Description:
 *   Obtains the specified string value for the specified variable name
 *   within the specified section of the INI file.
 *
 ****************************************************************************/

static FAR char *inifile_find_variable(FAR struct inifile_state_s *priv,
                                       FAR const char *section,
                                       FAR const char *variable)
{
  FAR char *ret = NULL;

  iniinfo("section=\"%s\" variable=\"%s\"\n", section, variable);

  /* Seek to the first variable in the specified section of the INI file */

  if (priv->instream && inifile_seek_to_section(priv, section))
    {
      /* If the seek was successful, then find the value string within
       * the section
       */

      FAR char *value = inifile_find_section_variable(priv, variable);
      iniinfo("variable_value=0x%p\n", value);

      if (value && *value)
        {
          iniinfo("variable_value=\"%s\"\n", value);
          ret = value;
        }
    }

  /* Return the string that we found. */

  iniinfo("Returning 0x%p\n", ret);
  return ret;
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name:  inifile_initialize
 *
 * Description:
 *   Initialize for access to the INI file 'inifile_name'
 *
 ****************************************************************************/

INIHANDLE inifile_initialize(FAR const char *inifile_name)
{
  /* Allocate an INI file parser state structure */

  FAR struct inifile_state_s *priv =
    (FAR struct inifile_state_s *)malloc(sizeof(struct inifile_state_s));

  if (!priv)
    {
      inidbg("ERROR: Failed to allocate state structure\n");
      return (INIHANDLE)NULL;
    }

  /* Open the specified INI file for reading */

  priv->instream = fopen(inifile_name, "r");

  /* Prime the pump */

  if (priv->instream)
    {
      priv->nextch = getc(priv->instream);
      return (INIHANDLE)priv;
    }
  else
    {
      inidbg("ERROR: Could not open \"%s\"\n", inifile_name);
      return (INIHANDLE)NULL;
    }
}

/****************************************************************************
 * Name:  inifile_uninitialize
 *
 * Description:
 *   Free resources commit to INI file parsing
 *
 ****************************************************************************/

void inifile_uninitialize(INIHANDLE handle)
{
  FAR struct inifile_state_s *priv = (FAR struct inifile_state_s *)handle;

  if (priv)
    {
      /* Close the INI file stream */

      if (priv->instream)
        {
          fclose(priv->instream);
        }

      /* Release the state structure */

      free(priv);
    }
}

/****************************************************************************
 * Name: inifile_read_string
 *
 * Description:
 *   Obtains the specified string value for the specified variable name
 *   within the specified section of the INI file.  The receiver of the
 *   value string should call inifile_free_string when it no longer needs
 *   the memory held by the value string.
 *
 ****************************************************************************/

FAR char *inifile_read_string(INIHANDLE handle,
                              FAR const char *section,
                              FAR const char *variable,
                              FAR const char *defvalue)
{
  FAR struct inifile_state_s *priv = (FAR struct inifile_state_s *)handle;
  FAR char *ret = NULL;
  FAR const char *value;

  /* Get a reference to the volatile version of the string */

  value = inifile_find_variable(priv, section, variable);

  /* If the variable was not found, then use the default value */

  if (!value)
    {
      /* Selecting the default string */

      value = defvalue;
    }

  /* If this was successful, create a non-volatile copy of the string
   * We do this even if the default value is used because the caller
   * will (eventually) deallocate it.
   */

  if (value)
    {
      ret = strdup(value);
    }

  /* Return the string that we found. */

  return ret;
}

/****************************************************************************
 * Name:  inifile_read_integer
 *
 * Description:
 *   Obtains the specified integer value for the specified variable name
 *   within the specified section of the INI file
 *
 ****************************************************************************/

long inifile_read_integer(INIHANDLE handle,
                          FAR const char *section,
                          FAR const char *variable,
                          FAR long defvalue)
{
  FAR struct inifile_state_s *priv = (FAR struct inifile_state_s *)handle;
  FAR char *value;
  long ret = defvalue;

  /* Assume failure to find the requested value */

  iniinfo("section=\"%s\" variable=\"%s\" defvalue=%d\n",
          section, variable, defvalue);

  /* Get the value as a string first */

  value = inifile_find_variable(priv, section, variable);

  /* If this was successful, then convert the string to an integer value. */

  if (value)
    {
      /* Then convert the string to an integer value (accept any base, and
       * ignore all conversion errors.
       */

      iniinfo("%s=\"%s\"\n", variable, value);
      ret = strtol(value, NULL, 0);
    }

  /* Return the value that we found. */

  iniinfo("Returning %d\n", ret);
  return ret;
}

/****************************************************************************
 * Name:  inifile_free_string
 *
 * Description:
 *   Release resources allocated for the value string previously obtained
 *   from inifile_read_string.  The purpose of this inline function is to
 *   hide the memory allocator used by this implementation.
 *
 ****************************************************************************/

void inifile_free_string(FAR char *value)
{
  if (value)
    {
      free(value);
    }
}
