/*
 *
 *  Copyright (C) 1997-2021, OFFIS e.V.
 *  All rights reserved.  See COPYRIGHT file for details.
 *
 *  This software and supporting documentation were developed by
 *
 *    OFFIS e.V.
 *    R&D Division Health
 *    Escherweg 2
 *    D-26121 Oldenburg, Germany
 *
 *
 *  Module:  ofstd
 *
 *  Author: Marco Eichelberg
 *
 *  Purpose:
 *    classes: OFConfigFile
 *
 */

#ifndef OFCONFIG_H
#define OFCONFIG_H

#include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */
#include "dcmtk/ofstd/ofstring.h"
#include "dcmtk/ofstd/ofstack.h"
#include "dcmtk/ofstd/ofstream.h"

/*
 *  Short description of configuration file structure:
 *    - The data in a configuration file have a tree structure.
 *      The tree has a depth defined at instantiation time (by default, OFConfigFile_MaxLevel), 
 *      not including the (imaginary) root node.
 *    - A level 0 entry (a leaf) has the form: KEYWORD = VALUE,
 *      where the keyword starts on row one of a line.
 *    - A level 1 entry has the form [KEYWORD]
 *    - A level 2 entry has the form [[KEYWORD]] (and so on).
 *    - Keywords may consist of:
 *        A..Z, a..z (which are converted to uppercase),
 *        0..9,
 *        '-'
 *    - Values can be any kind of ASCII text. Values may span multiple lines.
 *      To continue a value in the next line, the next line MUST start with
 *      (any amount of) whitespace, which is discarded when reading the value.
 *      linefeeds (converted to ASCII 10 if necessary) are kept in the
 *      value string. Empty lines are discarded (and also their linefeed).
 *    - The data must have a "clean" tree structure. This means that there
 *      MUST be a level 2 keyword before any level 1 keyword etc.
 *    - lines starting with the comment char (default  is "#") are interpreted 
 *      as comment lines.
 *
 */

#define OFConfigFile_MaxLevel 2
#define OFConfigFile_CommentChar '#'

class OFConfigFile;
class OFConfigFileNode;

typedef OFConfigFileNode *OFPConfigFileNode;


/** this class maintains one configuration file entry (key-value pair)
 *  and the links that comprise the configuration file tree structure.
 *  Internal use only.
 */
class DCMTK_OFSTD_EXPORT OFConfigFileNode
{
public:
  /** constructor.
   *  @param keyword configuration keyword, copied into this object
   */
  OFConfigFileNode(const char *keyword);

  /// destructor, recursively deletes whole tree
  ~OFConfigFileNode();

  /** return keyword as C string
   */
  const char *getKeyword() const
  {
    return keyword_.c_str();
  }

  /** return current value as C string
   */
  const char *getValue() const
  {
    return value_.c_str();
  }

  /** set value from C string
   *  @param c C string, must not be NULL
   */
  void setValue(const char *c)
  {
    value_ = c;
  }

  /** check if keyword matches given string
   *  @param c C string, must not be NULL
   *  @return true if equal, false otherwise
   */
  OFBool match(const char *c) const
  {
    return (keyword_ == c);
  }

  /** check if keyword compares "<" to given string
   *  @param c C string, must not be NULL
   *  @return true if equal, false otherwise
   */
  OFBool less(const char *c) const
  {
    return (keyword_ < c);
  }

  /** return pointer to next object in tree on same level
   *  @return pointer to next object, NULL if empty
   */
  OFConfigFileNode *getBrother() const
  {
    return brother_;
  }

  /** return pointer to next object in tree on lower level
   *  @return pointer to lower object, NULL if empty
   */
  OFConfigFileNode *getSon() const
  {
    return son_;
  }

  /** set pointer to next object in tree on same level
   *  @param brother pointer to next object in tree
   */
  void setBrother(OFConfigFileNode *brother)
  {
    brother_ = brother;
  }

  /** set pointer to next object in tree on lower level
   *  @param son pointer to next object in tree
   */
  void setSon(OFConfigFileNode *son)
  {
    son_ = son;
  }

  /** print the content of this node to an output stream
   *  @param out output stream to print to
   *  @param level tree depth (level)
   */
  void print(STD_NAMESPACE ostream& out, unsigned int level);

private:
  /// private undefined copy constructor
  OFConfigFileNode(const OFConfigFileNode& arg);

  /// private undefined copy assignment operator
  OFConfigFileNode &operator=(const OFConfigFileNode& arg);

  /// pointer to next object in tree on same level
  OFConfigFileNode *brother_;

  /// pointer to next object in tree on lower level
  OFConfigFileNode *son_;

  /// configuration keyword
  OFString keyword_;

  /// configuration value
  OFString value_;
};

/// pointer to a OFConfigFileNode object
typedef OFConfigFileNode *OFConfigFileNodePtr;

/** structure used by class OFConfigFile to store a cursor pointing
 *  to the currently selected section and entry in the config data
 */
class DCMTK_OFSTD_EXPORT OFConfigFileCursor
{
public:
  /** default constructor
   */
  OFConfigFileCursor(unsigned int maxLevel) 
  : array_(NULL)
  , maxLevel_(maxLevel)
  { 
  	clear(); 
  }

  /** copy constructor
   */
  OFConfigFileCursor(const OFConfigFileCursor& source);

  /** destructor
   */
  ~OFConfigFileCursor() 
  {
    delete[] array_;
  }

  /** assignment operator
   */
  OFConfigFileCursor& operator=(const OFConfigFileCursor& source);

  /// return object to default constructed state (invalid cursor)
  void clear();

  /** return keyword as C string.
   *  Precondition is that section_valid(level) return true.
   *  @param level cursor level
   */
  const char *getKeyword(unsigned int level) const
  {
    if ((level <= maxLevel_) && array_ && array_[level]) return array_[level]->getKeyword(); else return NULL;
  }

  /** return value as C string
   *  Precondition is that section_valid(level) return true.
   *  @param level cursor level
   */
  const char *getValue(unsigned int level) const
  {
    if ((level <= maxLevel_) && array_ && array_[level]) return array_[level]->getValue(); else return NULL;
  }

  /** checks if the cursor points to a valid location up to
   *  the given level
   *  @param level cursor level
   *  @return true if valid, false otherwise
   */
  OFBool section_valid(unsigned int level) const;

  /** sets cursor to the entry with keyword "key" at the given level.
   *  Cursor must be valid at the level higher than the one specified.
   *  If the keyword exists more than once, the first occurance is found.
   *  If the keyword does not exist, the cursor becomes invalid at the
   *  specified level. The cursor always becomes invalid at all levels
   *  lower than specified.
   *  @param level cursor level
   *  @param key key value
   *  @param anchor pointer to anchor node in config tree
   */
  void set_section(
    unsigned int level, 
    const char *key,
    OFConfigFileNode *anchor);
   
  /** sets cursor to the first entry at the given level (without
   *  changing the cursor position at higher levels).
   *  Cursor must be valid at the level higher than the one specified.
   *  If no section is found the cursor becomes invalid at the
   *  specified level. The cursor always becomes invalid at all levels
   *  lower than specified.
   *  @param level cursor level
   *  @param anchor pointer to anchor node in config tree
   */
  void first_section(
    unsigned int level,
    OFConfigFileNode *anchor);

  /** sets cursor to the next entry at the given level (without
   *  changing the cursor position at higher levels).
   *  Cursor must be valid at the level specified.
   *  If no section is found the cursor becomes invalid at the
   *  specified level. The cursor always becomes invalid at all levels
   *  lower than specified.
   *  @param level cursor level
   */
  void next_section(unsigned int level);

  /** inserts a new node into the tree at the current cursor position
   *  @param level level of insertion
   *  @param newnode node to be inserted, must not be NULL
   *  @param anchor pointer to anchor node of tree, may be modified
   *  @param orderedMode mode flag for processing configuration files.
   *    See documentation of OFConfigFile::orderedMode_
   */
  void insert(
    unsigned int level,
    OFConfigFileNode *& newnode,
    OFConfigFileNode *& anchor,
    OFBool orderedMode);

  /** dummy comparison operator, needed by MSVC5 with STL.
   *  @return always false
   */
  OFBool operator<(const OFConfigFileCursor& /* arg */) const
  {
    return OFFalse;
  }

  /** dummy comparison operator, needed by MSVC5 with STL.
   *  @return always true
   */
  OFBool operator==(const OFConfigFileCursor& /* arg */) const
  {
    return OFTrue;
  }

private:

 /** helper method that is called by insert() in ordered mode
  *  to insert a new node at a given tree level (i.e. within a list
  *  of nodes that share the same parent node) in alphabetical order,
  *  replacing any older node with identical keyword.
  *  @param parent  parent node of new new to be inserted
  *  @param newnode pointer to new node to be inserted
  */
  void orderedInsert(
    OFConfigFileNode *parent,
    OFConfigFileNode *&newnode);

  /// the cursor is an array of pointers to OFConfigFileNode objects
  OFConfigFileNodePtr *array_;

  /// depth of tree, i.e. number of entries in array_
  unsigned int maxLevel_;  
};


/** class for reading and evaluating configuration files.
 *  An object of this class reads upon construction a text
 *  config file and offers methods allowing to browse the
 *  configuration data
 */
class DCMTK_OFSTD_EXPORT OFConfigFile
{
public:

  /** constructor.
   *  @param infile file from which the configuration data is to be read.
   *  @param maxLevel depth of the tree maintained in this config file, default 2
   *  @param commentChar character to start comment lines, default '#'
   *  @param orderedMode mode flag for processing configuration files.
   *    See documentation of member variable orderedMode_
   */
  OFConfigFile(
    FILE *infile, 
    unsigned int maxLevel = OFConfigFile_MaxLevel,
    char commentChar = OFConfigFile_CommentChar,
    OFBool orderedMode = OFFalse);

  /** destructor
   */
  virtual ~OFConfigFile();

  /** load configuration file
   *  @param infile configuration file, must already be open for reading.
   */
  void loadFile(FILE *infile);
  
  /** gets the name of the keyword at the specified level
   *  in the cursor path. Cursor must be valid at the specified level.
   *  @param level cursor level
   *  @return keyword if valid, NULL otherwise.
   */
  const char *get_keyword(unsigned int level);
  
  /** gets the value for the current entry (level 0 keyword).
   *  Cursor must point to a valid entry (i.e. be valid at level 0)
   *  @return value if valid, NULL otherwise.
   */
  const char *get_value();
  
  /** gets the value for the current entry and interprets it as
   *  a boolean value. The keywords "yes", "no", "on", "off", "1", "0", 
   *  "true" and false" are recognised in upper, lower and mixed case.
   *  If the value is different from any recognised keyword, the specified
   *  default is returned
   *  Cursor must point to a valid entry (i.e. be valid at level 0)
   *  @param defaultvalue default to be returned if no keyword is recognised
   *    of if the cursor is invalid.
   *  @return boolean value.
   */
  OFBool get_bool_value(OFBool defaultvalue);
  
  /** checks if the cursor points to a valid entry in the
   *  config data tree up to the the specified level.
   *  @param level tree level (0 for leaf)
   *  @return OFTrue if valid at the specified level.
   */
  OFBool section_valid(unsigned int level) const
  {
    return cursor_.section_valid(level);
  }
  
  /** sets cursor to the entry with keyword "key" at the given level.
   *  Cursor must be valid at the level higher than the one specified.
   *  If the keyword exists more than once, the first occurance is found.
   *  If the keyword does not exist, the cursor becomes invalid at the
   *  specified level. The cursor always becomes invalid at all levels
   *  lower than specified.
   *  @param level cursor level
   *  @param key key value
   */
  void set_section(unsigned int level, const char *key)
  {
    cursor_.set_section(level, key, anchor_);
  }
   
  /** sets cursor to the first entry at the given level (without
   *  changing the cursor position at higher levels).
   *  Cursor must be valid at the level higher than the one specified.
   *  If no section is found the cursor becomes invalid at the
   *  specified level. The cursor always becomes invalid at all levels
   *  lower than specified.
   *  @param level cursor level
   */
  void first_section(unsigned int level)
  {
    cursor_.first_section(level, anchor_);
  }

  /** sets cursor to the next entry at the given level (without
   *  changing the cursor position at higher levels).
   *  Cursor must be valid at the level specified.
   *  If no section is found the cursor becomes invalid at the
   *  specified level. The cursor always becomes invalid at all levels
   *  lower than specified.
   *  @param level cursor level
   */
  void next_section(unsigned int level)
  {
    cursor_.next_section(level);
  }
  
  /** puts the current cursor position on a cursor stack.
   */
  void save_cursor();
  
  /** restores a previously stored cursor position from the cursor stack.
   */
  void restore_cursor();
  
  /** sets the cursor to a different section. This "shortcut" method allows
   *  to specify multiple section levels at the same time.
   *  The cursor becomes invalid when the section is not found or if parameters
   *  for more level then present in the tree are specified.
   *  @param key1 level 1 section key, i.e. [KEY]
   *  @param key2 level 2 section key, i.e. [[KEY]]. If omitted, section 2 remains unchanged.
   *  @param key2 level 3 section key, i.e. [[[KEY]]]. If omitted, section 3 remains unchanged.
   */
  void select_section(
    const char *key1,
    const char *key2=NULL,
    const char *key3=NULL);
  
  /** sets the cursor to the given level 0 keyword and returns
   *  the string value assigned to this keyword.
   *  @param key0 level 0 keyword
   *  @return value string if found, NULL otherwise
   */
  const char *get_entry(const char *key0);

  /** print the content of the configuration to an output stream
   *  @param out output stream to print to
   */
  void print(STD_NAMESPACE ostream& out);

private:  

  /** reads the next character from the input file,
   *  maintains the current line number and filters out
   *  comments.
   *  @param infile the file to be read
   *  @return next character
   */
  char read_char(FILE *infile);

  /** reads the next non-whitespace character from
   *  the input file and returns as uppercase character.
   *  @param infile the file to be read
   *  @return next non-whitespace character
   */
  char read_keywordchar(FILE *infile);
  
  /** reads a complete entry from the config file.
   *  An entry can either be a section heading or
   *  an entry of the form key=value. The entry is inserted
   *  into the config data tree managed by this object.
   *  @param infile the file to be read
   */
  void read_entry(FILE *infile);
  
  /** writes a character to the string buffer maintained in
   *  "buffer". Automatically resizes buffer if necessary.
   *  @param c character to be stored
   */
  void store_char(char c);

  /** private undefined copy constructor
   */
  OFConfigFile(const OFConfigFile&);
  
  /** private undefined assignment operator
   */
  OFConfigFile& operator=(const OFConfigFile&);


  /// stack of cursor positions that can be saved and restored
  OFStack<OFConfigFileCursor> stack_;

  /// current cursor position
  OFConfigFileCursor cursor_;

  /// anchor to data tree
  OFConfigFileNode *anchor_;

  /// flag indicating whether newline during file read
  int isnewline_;

  /// flag indicating whether CR was read during file read
  int crfound_;

  /// buffer during file read
  char *buffer_;

  /// index into buffer during file read
  size_t bufptr_;

  /// buffer size during file read
  long bufsize_;

  /// depth of tree, i.e. number of entries in array_
  unsigned int maxLevel_;  

  /// character starting comment lines  
  char commentChar_;
  
  /** mode flag for reading configuration file. If false, new entries to the
   *  tree containing the configuration file are just appended and not sorted.
   *  This allows multiple entries with the same keyword to be present, only the
   *  first of which will be found by search-by-keyword methods such as set_section,
   *  select_section or get_entry. In ordered mode, the tree is maintained in sorted
   *  order and a new entry will replace an older entry with identical keyword.
   *  This permits multiple configuration files to "overlay" each other but is
   *  somewhat slower when reading the configuration file.
   */
  OFBool orderedMode_;
};

#endif
