/* $Id$ 
 * 
 * Licensed to the Apache Software Foundation (ASF) under one or more 
 * contributor license agreements. See the NOTICE file distributed with  
 * this work for additional information regarding copyright ownership. 
 * The ASF licenses this file to you under the Apache License, Version  
 * 2.0 (the "License"); you may not use this file except in compliance  
 * with the License. You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 

/*
 * etch_config.c
 * config items and config file parse.
 * config file is formatted as a java-style properties file expected as ansi or utf-8.
 */
#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h>
#include <ctype.h>

#pragma warning(disable:4996) /* "unsafe" function warning */
#include "etch_config.h"  
#include "etchlog.h"

char* ETCHCFIG = "CFIG";

#define ETCHMAXCONFIGLINESIZE 240
#define ETCHCONFIG_TOKEN_FOUND 0
#define ETCHCONFIG_LINE_IS_BLANK_OR_COMMENT 1

#define ETCHCONFIG_DATATYPE_INT   1
#define ETCHCONFIG_DATATYPE_FIXED 2
#define ETCHCONFIG_DATATYPE_CHAR  3

#define NOTWHITESPACE(p) (*p != ' '  && *p != '\t')
#define ISWHITESPACE(p)  (*p == ' '  || *p == '\t')
#define NOTENDOFLINE(p)  (*p != '\0' && *p != '\n' && *p != '\r')
#define ISENDOFLINE(p)   (*p == '\0' || *p == '\n' || *p == '\r')

#define ETCHSERVERDEFAULTCONFIGFILEPATH  "../etch-c-server.properties"
#define ETCHCLIENTDEFAULTCONFIGFILEPATH  "../etch-c-client.properties"

typedef struct ndx {int id; char* key;} ndx; 

typedef struct etchconfig_work
{ int  keycount, item, datatype, length, value, is_bool;
  char *beg, *end, *endobj, *begkey, *equal, *begval, *endval;
  double fixedval;
} etchconfig_work;

int etchconfig_identify_value (etchconfig_work*);
int etchconfig_assign_value   (const etchconfig_work*); 
int etchconfig_identify_item  (etchconfig_work*, char* line, ndx* keys);  


/**
 * get_etch_server_configfile_path()
 * return the path to the server config file. 
 */
char* get_etch_server_configfile_path()
{
    static char* path = ETCHSERVERDEFAULTCONFIGFILEPATH;
    return path;
}


/**
 * get_etch_client_configfile_path()
 * return the path to the client config file. 
 */
char* get_etch_client_configfile_path()
{
    static char* path = ETCHCLIENTDEFAULTCONFIGFILEPATH;
    return path;
}


/**
 * open_etch_configfile() 
 */
int open_etch_configfile (const int is_client, FILE** f)
{
    int result = 0;   
    char* path = is_client? get_etch_client_configfile_path(): get_etch_server_configfile_path();

    if (NULL == (*f = fopen(path, "r")))
    {   path = is_client?  ETCHCLIENTDEFAULTCONFIGFILEPATH: ETCHSERVERDEFAULTCONFIGFILEPATH;

        if (NULL == (*f = fopen(path, "r")))   
        {  etchlog(ETCHCFIG, ETCHLOG_WARNING, "could not open config %s - using defaults\n", path);
           result = -1;
        }
    }

    return result;
}


/**
 * etch_reset_config()
 * initialize global config items with etch defaults.  
 * @remarks whenever a new config item is added, a setter for it must be coded 
 * herein in order to ensure that a default value exists for the item, and thus
 * it will have an appropriate value if either no config file entry is coded for
 * it, or if a config file can't be located and read.
 */
int etch_reset_config (const int is_client)
{
    memset(&config, 0, sizeof(struct etch_config));
    config.memory_watch_id = 0;
    config.log_level = ETCHCONFIGDEF_LOG_LEVEL;
    config.loglevel  = get_etch_loglevel(ETCHCONFIGDEF_LOG_LEVEL);
    config.max_log_files = ETCHCONFIGDEF_MAX_LOGFILES;
    config.max_log_lines = ETCHCONFIGDEF_MAX_LOGLINES;
    config.is_validate_on_read  = ETCHCONFIGDEF_IS_VALIDATE_ON_READ;
    config.is_validate_on_write = ETCHCONFIGDEF_IS_VALIDATE_ON_WRITE;
    config.is_log_to_file = ETCHCONFIGDEF_IS_LOG_TO_FILE;
    config.is_log_to_console = ETCHCONFIGDEF_IS_LOG_TO_CONSOLE;
    config.is_log_memory_leak_detail = ETCHCONFIGDEF_IS_LOG_MEMORY_LEAK_DETAIL;
    config.is_destroy_messages_with_mailbox = ETCHCONFIGDEF_IS_DESTROY_MESSAGES_WITH_MAILBOX;
    config.default_mailbox_read_waitms = ETCHCONFIGDEF_DEFAULT_MAILBOX_READ_WAITMS;
    config.sleepSecondsPriorClientExit = ETCHCONFIGDEF_SLEEP_SECONDS_PRIOR_CLIENT_EXIT;
    strncpy(config.example_string, ETCHCONFIGDEF_EXAMPLE_STRING_VALUE, ETCHCONFIGDEF_EXAMPLE_STRING_MAXLEN);

    return 0;
}


/**
 * etch_read_configfile()
 * read and validate config file, populate global config items. 
 * @param is_client boolean indicating if reading config on client side.
 * @param is_reset boolean indicating if global config items should be reset
 * to default values prior to reading the config file.
 * @return 0 success, -1 failure.
 * @remarks whenever a new config item is added, a mapping from that item's 
 * key to its external config file name must be coded herein.
 */
int etch_read_configfile (const int is_client, const int is_reset)
{
    int  result = 0, linenum = 0, itemcount = 0;
    char line [ETCHMAXCONFIGLINESIZE+1];  
    etchconfig_work work; 
    FILE* f = 0;

    ndx config_keys[] =                     
    { {ETCHCONFIGKEY_LOG_LEVEL,  "logLevel"},
      {ETCHCONFIGKEY_MAX_LOGFILES, "maxLogFiles"},
      {ETCHCONFIGKEY_MAX_LOGLINES, "maxLogLines"},
      {ETCHCONFIGKEY_ETCH_WATCH_ID, "memoryWatchID"},
      {ETCHCONFIGKEY_IS_LOG_TO_FILE,  "isLogToFile"},
      {ETCHCONFIGKEY_IS_LOG_TO_CONSOLE,  "isLogToConsole"},
      {ETCHCONFIGKEY_IS_VALIDATE_ON_READ,   "isValidateOnRead"},
      {ETCHCONFIGKEY_IS_VALIDATE_ON_WRITE,  "isValidateOnWrite"},
      {ETCHCONFIGKEY_IS_DESTROY_MESSAGES_WITH_MAILBOX, "isDestroyMessagesWithMailbox"},
      {ETCHCONFIGKEY_IS_DISPLAY_MEMORY_LEAK_DETAIL,    "isDisplayMemoryLeakDetail"},
      {ETCHCONFIGKEY_DEFAULT_MAILBOX_READ_WAITMS  ,    "defaultMailboxReadWaitms"},
      {ETCHCONFIGKEY_SLEEP_SECONDS_PRIOR_CLIENT_EXIT,  "sleepSecondsPriorClientExit"},
      {ETCHCONFIGKEY_TEST_STRING, "testString"},  /* for testing only */
      {ETCHCONFIGKEY_TEST_INT,    "testInt"},     /* for testing only */
      {ETCHCONFIGKEY_TEST_BOOL,   "testBool"},    /* for testing only */
      {ETCHCONFIGKEY_TEST_FIXED,  "testFixed"},   /* for testing only */
    };

    memset(&work, 0, sizeof(etchconfig_work));
    work.keycount = sizeof(config_keys) / sizeof(ndx);

    if (is_reset)  /* reset config entries to etch defaults */
        etch_reset_config (is_client);
  
    if (-1 == open_etch_configfile (is_client, &f))  /* open config file */
        return -1;

    while(1) /* while not eof on config file ... */
    {
        if (NULL == fgets (line, ETCHMAXCONFIGLINESIZE, f)) break;
        work.item = 0;
        linenum++;

        result = etchconfig_identify_item (&work, line, config_keys);
        if (ETCHCONFIG_LINE_IS_BLANK_OR_COMMENT == result) continue; 

        if (-1 == result) 
        {   etchlog(ETCHCFIG, ETCHLOG_ERROR, "at line %d: name not recognized\n", linenum);
            continue;
        } 

        if((-1 == etchconfig_identify_value(&work)) || (-1 == etchconfig_assign_value(&work)))
        {
            etchlog(ETCHCFIG, ETCHLOG_ERROR, "at line %d: invalid item value\n", linenum);
            continue;
        }

        if (*work.endval == ';') /* warning that a semicolon may have been typed in error */
            etchlog(ETCHCFIG, ETCHLOG_WARNING, "at line %d: value ends with semicolon\n", linenum);

        itemcount++;
    }
       
    fclose(f);
    etchlog(ETCHCFIG, ETCHLOG_DEBUG, "%d config entries read\n", itemcount);
    return itemcount;
}


/**
 * etchconfig_identify_item()
 * parse out and identify object token   
 */
int etchconfig_identify_item (etchconfig_work* work, char* line, ndx* keys)  
{
    int  i;
    ndx  *keyi = keys;
    char csave, *q, *p = line;

    while(ISWHITESPACE(p)) p++;  /* find first noblank character */
    if (ISENDOFLINE(p)) return ETCHCONFIG_LINE_IS_BLANK_OR_COMMENT;
    work->beg = p;               /* mark initial character */ 

    while(*p && *p != '#' && *p != '=') p++;  /* find equal sign */              
    if (*p == '#')      return ETCHCONFIG_LINE_IS_BLANK_OR_COMMENT;
    if (ISENDOFLINE(p)) return -1;

    work->equal = p;             /* mark equal sign */

    q = work->equal; q--;
    while(ISWHITESPACE(q))  q--; /* strip trailing whitespace */

    work->end = q;               /* mark final character */
    q++;                         /* point after item name */
    csave = *q;                  /* save character at that position */
    *q = '\0';                   /* and terminate item name string */
    
    for(i = 0, work->item = 0; i < (const int) work->keycount; i++, keyi++)
    {   /* compare found item name with expected item names */
        if (stricmp(keyi->key, work->beg) == 0)
        {   work->item = keyi->id;
            break;
        }
    }
 
    *q = csave;  /* restore character replaced above */
    return work->item? ETCHCONFIG_TOKEN_FOUND: -1;
}


/**
 * etchconfig_identify_item()
 * parse out item value and determine its data type.   
 */                                            
int etchconfig_identify_value (etchconfig_work* work)             
{
    char *p = NULL, *q = NULL;
    int  i = 0, digits = 0, expecteddigits = 0, dots = 0, isnegative = 0; 
    work->length = work->datatype = work->is_bool = 0;
    work->begval = work->endval = NULL; 

    p = work->equal; p++;                     /* start after equal sign */
    while(*p && ISWHITESPACE(p)) p++;         /* find first noblank character */
    if (ISENDOFLINE(p) || *p == '#') return -1;

    q = p;
    while(NOTENDOFLINE(q) && *q != '#') q++;  /* find end of line or comment */
    q--;
    while(ISWHITESPACE(q)) q--;               /* strip trailing whitespace */
              
    /* calculate length of value */                              
    work->length = (int) ((size_t) q - (size_t) p + 1);    
    if (work->length < 1) return -1; 

    if (*p == '\"')                 /* if quoted string ... */
    {   if (*q != '\"') return -1;  /* test for close quote */
        p++; q--;                   /* remove quotes */
        work->datatype = ETCHCONFIG_DATATYPE_CHAR;
    }

    work->begval = p;   /* set value boundaries */
    work->endval = q;
    *++q = '\0';        /* add terminator for logging */

    if (work->datatype) /* data type now known? */
        return ETCHCONFIG_TOKEN_FOUND;
                     
    /* determine data type of value */   
    expecteddigits = work->length; 
        
    for(; i < (const int) work->length; i++, p++)
    {
      if  (isdigit(*p))
           digits++;
      else
      if  (*p == '-' && i == 0)  /* negative sign? */
           isnegative = TRUE;
      else                       /* decimal point? */
      if  (*p == '.')
           dots++;      
    }

    if (digits > 0)
    {   if (isnegative) expecteddigits--;
        if (dots == 1)  expecteddigits--;
    }

    if (digits == expecteddigits && dots == 0)
    {    /* integral value e.g. 314159 */
         work->datatype = ETCHCONFIG_DATATYPE_INT;
         work->value = atoi(work->begval);
         if (work->value == 0 ||work->value == 1)
             work->is_bool = TRUE;  /* integer is boolean-valued */
    }
    else
    if  (digits == expecteddigits && dots == 1)
    {    /* fixed decimal value e.g. 3.14159 */
         work->datatype = ETCHCONFIG_DATATYPE_FIXED;
         work->fixedval = strtod(work->begval, &q);
    }    /* unquoted string */
    else work->datatype = ETCHCONFIG_DATATYPE_CHAR;

    return ETCHCONFIG_TOKEN_FOUND;
}


/**
 * get_etch_loglevel()
 * convert log level from character supplied in config to ordinal used by logger. 
 */  
int get_etch_loglevel (const unsigned char loglvl)
{
    int thislevel = -1;
    switch(toupper(loglvl))
    { case 'D': thislevel = ETCHLOG_DEBUG;   break;   
      case 'I': thislevel = ETCHLOG_INFO;    break;  
      case 'X': thislevel = ETCHLOG_XDEBUG;  break;   
      case 'E': thislevel = ETCHLOG_ERROR;   break;   
      case 'W': thislevel = ETCHLOG_WARNING; break;   
    }
    return thislevel;
}


/**
 * etchconfig_assign_value()
 * edit value from config file and assign to internal config item. 
 * @remarks whenever a new config item is added, its key, plus code 
 * to validate the value, must be added to the switch herein.
 */  
int etchconfig_assign_value (const etchconfig_work* work)                   
{
    const int datatype = work->datatype; 
    const int ivalue   = work->value, is_bool_value = work->is_bool;
    const char* cvalue = work->begval;
    int result = 0, n = 0;

    switch(work->item)
    { 
      case ETCHCONFIGKEY_LOG_LEVEL:
           if (datatype != ETCHCONFIG_DATATYPE_CHAR) 
               result = -1; 
           else  /* translate character level to ordinal */
           if (-1 == (n = get_etch_loglevel (*cvalue)))
               result = -1;
           else 
           {  config.loglevel = n;   /* ordinal level */
              config.log_level = *cvalue;  /* character level */
           }
           break;

      case ETCHCONFIGKEY_MAX_LOGFILES:
           if (datatype != ETCHCONFIG_DATATYPE_INT) result = -1;
           else config.max_log_files = ivalue;
           break;

      case ETCHCONFIGKEY_MAX_LOGLINES:
           if (datatype != ETCHCONFIG_DATATYPE_INT) result = -1;
           else config.max_log_lines = ivalue;
           break;

      case ETCHCONFIGKEY_ETCH_WATCH_ID:
           if (datatype != ETCHCONFIG_DATATYPE_INT) result = -1;
           else config.memory_watch_id = ivalue;
           break;

      case ETCHCONFIGKEY_DEFAULT_MAILBOX_READ_WAITMS:
           if (datatype != ETCHCONFIG_DATATYPE_INT || ivalue < (-1)) 
               result = -1;
           else config.default_mailbox_read_waitms = ivalue;
           break;

      case ETCHCONFIGKEY_IS_LOG_TO_FILE:
           if (!is_bool_value) result = -1; 
           else config.is_log_to_file = ivalue;
           break;

      case ETCHCONFIGKEY_IS_LOG_TO_CONSOLE:
           if (!is_bool_value) result = -1; 
           else config.is_log_to_console = ivalue;
           break;

      case ETCHCONFIGKEY_IS_VALIDATE_ON_READ:
           if (!is_bool_value) result = -1; 
           else config.is_validate_on_read = ivalue;
           break;

      case ETCHCONFIGKEY_IS_VALIDATE_ON_WRITE:
           if (!is_bool_value) result = -1; 
           else config.is_validate_on_write = ivalue;
           break;

      case ETCHCONFIGKEY_IS_DESTROY_MESSAGES_WITH_MAILBOX:
           if (!is_bool_value) result = -1; 
           else config.is_destroy_messages_with_mailbox = ivalue;
           break;

      case ETCHCONFIGKEY_IS_DISPLAY_MEMORY_LEAK_DETAIL:
           if (!is_bool_value) result = -1; 
           else config.is_log_memory_leak_detail = ivalue;
           break;

      case ETCHCONFIGKEY_SLEEP_SECONDS_PRIOR_CLIENT_EXIT:
           if (datatype != ETCHCONFIG_DATATYPE_INT || ivalue < 0) 
               result = -1;
           else config.sleepSecondsPriorClientExit = ivalue;
           break;

      /* test items for testing config file parsing */
      case ETCHCONFIGKEY_TEST_STRING: /* example for a string config entry */
           if (datatype != ETCHCONFIG_DATATYPE_CHAR) result = -1;
           else if (strlen(cvalue) > ETCHCONFIGDEF_EXAMPLE_STRING_MAXLEN) result = -1; 
           else strcpy(config.example_string, cvalue);
           break;

      case ETCHCONFIGKEY_TEST_INT:
           if (datatype != ETCHCONFIG_DATATYPE_INT) result = -1;
           break;

      case ETCHCONFIGKEY_TEST_BOOL:
           if (!is_bool_value) result = -1; 
           break;

      case ETCHCONFIGKEY_TEST_FIXED:
           if (datatype != ETCHCONFIG_DATATYPE_FIXED) result = -1;
           break;

      default: result = -1; 
    }

    return result;
}
