/*
 * This file belongs to the Galois project, a C++ library for exploiting parallelism.
 * The code is being released under the terms of the 3-Clause BSD License (a
 * copy is located in LICENSE.txt at the top-level directory).
 *
 * Copyright (C) 2018, The University of Texas at Austin. All rights reserved.
 * UNIVERSITY EXPRESSLY DISCLAIMS ANY AND ALL WARRANTIES CONCERNING THIS
 * SOFTWARE AND DOCUMENTATION, INCLUDING ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR ANY PARTICULAR PURPOSE, NON-INFRINGEMENT AND WARRANTIES OF
 * PERFORMANCE, AND ANY WARRANTY THAT MIGHT OTHERWISE ARISE FROM COURSE OF
 * DEALING OR USAGE OF TRADE.  NO WARRANTY IS EITHER EXPRESS OR IMPLIED WITH
 * RESPECT TO THE USE OF THE SOFTWARE OR DOCUMENTATION. Under no circumstances
 * shall University be liable for incidental, special, indirect, direct or
 * consequential damages or loss of profits, interruption of business, or
 * related expenses which may arise from use of Software or Documentation,
 * including but not limited to those resulting from defects in Software and/or
 * Documentation, or loss or inaccuracy of data of any kind.
 */

/*  This file is part of libDAI - http://www.libdai.org/
 *
 *  Copyright (c) 2006-2011, The libDAI authors. All rights reserved.
 *
 *  Use of this source code is governed by a BSD-style license that can be found
 * in the LICENSE file.
 */

/// \file
/// \brief Defines the Property and PropertySet classes, which are mainly used
/// for managing parameters of inference algorithms

#ifndef __defined_libdai_properties_h
#define __defined_libdai_properties_h

#include <iostream>
#include <sstream>
#include <boost/any.hpp>
#include <map>
#include <vector>
#include <typeinfo>
#include <dai/exceptions.h>
#include <dai/util.h>
#include <boost/lexical_cast.hpp>

namespace dai {

/// Type of the key of a Property
typedef std::string PropertyKey;

/// Type of the value of a Property
typedef boost::any PropertyValue;

/// A Property is a pair of a key and a corresponding value
typedef std::pair<PropertyKey, PropertyValue> Property;

/// Writes a Property object (key-value pair) to an output stream
/** \note Not all value types are automatically supported; if a type is unknown,
 * an UNKNOWN_PROPERTY_TYPE exception is thrown. Adding support for a new type
 * can be done by changing this function body.
 */
std::ostream& operator<<(std::ostream& os, const Property& p);

/// Represents a set of properties, mapping keys (of type PropertyKey) to values
/// (of type PropertyValue)
/** Properties are used for specifying parameters of algorithms in a convenient
 * way, where the values of the parameters can be of different types (e.g.,
 * strings, doubles, integers, enums). A PropertySet is an attempt to mimic the
 * functionality of a Python dictionary object in C++, using the boost::any
 * class.
 *
 *  A PropertySet can be converted to and from a string, using the following
 * format:
 *
 *  <tt>[key1=val1,key2=val2,...,keyn=valn]</tt>
 *
 *  That is,
 *  - the whole PropertySet is wrapped in square brackets ("[", "]")
 *  - all properties in the PropertySet are seperated by a comma (",")
 *  - each Property consists of:
 *    - the name of the key
 *    - an equality sign ("=")
 *    - its value (represented as a string)
 *
 *  Also, a PropertySet provides functionality for converting the representation
 * of individual values from some arbitrary type to and from std::string.
 *
 *  \note Not all types are automatically supported; if a type is unknown, an
 * UNKNOWN_PROPERTY_TYPE exception is thrown. Adding support for a new type can
 * be done in the body of the operator<<(std::ostream &, const Property &).
 */
class PropertySet : private std::map<PropertyKey, PropertyValue> {
public:
  /// \name Constructors and destructors
  //@{
  /// Default constructor
  PropertySet() {}

  /// Construct from a string
  /** \param s string in the format
   * <tt>"[key1=val1,key2=val2,...,keyn=valn]"</tt>
   */
  PropertySet(const std::string& s) {
    std::stringstream ss;
    ss << s;
    ss >> *this;
  }
  //@}

  /// \name Setting property keys/values
  //@{
  /// Sets a property (a key \a key with a corresponding value \a val)
  PropertySet& set(const PropertyKey& key, const PropertyValue& val) {
    this->operator[](key) = val;
    return *this;
  }

  /// Set properties according to \a newProps, overriding properties that
  /// already exist with new values
  PropertySet& set(const PropertySet& newProps) {
    const std::map<PropertyKey, PropertyValue>* m = &newProps;
    diaforeach(value_type i, *m) set(i.first, i.second);
    return *this;
  }

  /// Shorthand for (temporarily) adding properties
  /** \par Example:
      \code
PropertySet p()("method","BP")("verbose",1)("tol",1e-9)
      \endcode
   */
  PropertySet operator()(const PropertyKey& key,
                         const PropertyValue& val) const {
    PropertySet copy = *this;
    return copy.set(key, val);
  }

  /// Sets a property (a key \a key with a corresponding value \a val, which is
  /// first converted from \a ValueType to string)
  /** The implementation makes use of boost::lexical_cast.
   *  \tparam ValueType Type from which the value should be cast
   *  \throw IMPOSSIBLE_TYPECAST if the type cast cannot be done
   */
  template <typename ValueType>
  PropertySet& setAsString(const PropertyKey& key, const ValueType& val) {
    try {
      return set(key, boost::lexical_cast<std::string>(val));
    } catch (boost::bad_lexical_cast&) {
      DAI_THROWE(IMPOSSIBLE_TYPECAST,
                 "Cannot cast value of property '" + key + "' to string.");
    }
  }

  /// Converts the type of the property value corresponding with \a key from
  /// string to \a ValueType (if necessary)
  /** The implementation makes use of boost::lexical_cast
   *  \tparam ValueType Type to which the value should be cast
   *  \throw IMPOSSIBLE_TYPECAST if the type cast cannot be done
   */
  template <typename ValueType>
  void convertTo(const PropertyKey& key) {
    PropertyValue val = get(key);
    if (val.type() != typeid(ValueType)) {
      DAI_ASSERT(val.type() == typeid(std::string));
      try {
        set(key, boost::lexical_cast<ValueType>(getAs<std::string>(key)));
      } catch (boost::bad_lexical_cast&) {
        DAI_THROWE(IMPOSSIBLE_TYPECAST, "Cannot cast value of property '" +
                                            key +
                                            "' from string to desired type.");
      }
    }
  }
  //@}

  //@}

  /// \name Queries
  //@{
  /// Return number of key-value pairs
  size_t size() const { return std::map<PropertyKey, PropertyValue>::size(); }

  /// Removes all key-value pairs
  void clear() { std::map<PropertyKey, PropertyValue>::clear(); }

  /// Removes key-value pair with given \a key
  size_t erase(const PropertyKey& key) {
    return std::map<PropertyKey, PropertyValue>::erase(key);
  }

  /// Check if a property with the given \a key is defined
  bool hasKey(const PropertyKey& key) const {
    PropertySet::const_iterator x = find(key);
    return (x != this->end());
  }

  /// Returns a set containing all keys
  std::set<PropertyKey> keys() const {
    std::set<PropertyKey> res;
    const_iterator i;
    for (i = begin(); i != end(); i++)
      res.insert(i->first);
    return res;
  }

  /// Gets the value corresponding to \a key
  /** \throw OBJECT_NOT_FOUND if the key cannot be found in \c *this
   */
  const PropertyValue& get(const PropertyKey& key) const {
    PropertySet::const_iterator x = find(key);
    if (x == this->end())
      DAI_THROWE(OBJECT_NOT_FOUND,
                 "PropertySet::get cannot find property '" + key + "'");
    return x->second;
  }

  /// Gets the value corresponding to \a key, cast to \a ValueType
  /** \tparam ValueType Type to which the value should be cast
   *  \throw OBJECT_NOT_FOUND if the key cannot be found in \c *this
   *  \throw IMPOSSIBLE_TYPECAST if the type cast cannot be done
   */
  template <typename ValueType>
  ValueType getAs(const PropertyKey& key) const {
    try {
      return boost::any_cast<ValueType>(get(key));
    } catch (const boost::bad_any_cast&) {
      DAI_THROWE(IMPOSSIBLE_TYPECAST, "Cannot cast value of property '" + key +
                                          "' to desired type.");
      return ValueType();
    }
  }

  /// Gets the value corresponding to \a key, cast to \a ValueType, converting
  /// from a string if necessary
  /** If the type of the value is already equal to \a ValueType, no conversion
   * is done. Otherwise, the type of the value should be a std::string, in which
   * case boost::lexical_cast is used to convert this to \a ValueType. \tparam
   * ValueType Type to which the value should be cast/converted \throw
   * OBJECT_NOT_FOUND if the key cannot be found in \c *this \throw
   * IMPOSSIBLE_TYPECAST if the type cast cannot be done
   */
  template <typename ValueType>
  ValueType getStringAs(const PropertyKey& key) const {
    PropertyValue val = get(key);
    if (val.type() == typeid(ValueType)) {
      return boost::any_cast<ValueType>(val);
    } else if (val.type() == typeid(std::string)) {
      try {
        return boost::lexical_cast<ValueType>(getAs<std::string>(key));
      } catch (boost::bad_lexical_cast&) {
        DAI_THROWE(IMPOSSIBLE_TYPECAST, "Cannot cast value of property '" +
                                            key +
                                            "' from string to desired type.");
      }
    } else
      DAI_THROWE(IMPOSSIBLE_TYPECAST, "Cannot cast value of property '" + key +
                                          "' from string to desired type.");
    return ValueType();
  }
  //@}

  /// \name Input/output
  //@{
  /// Writes a PropertySet object to an output stream.
  /** It uses the format <tt>"[key1=val1,key2=val2,...,keyn=valn]"</tt>.
   *  \note Only a subset of all possible types is supported (see the
   * implementation of this function). Adding support for more types has to be
   * done by hand. \throw UNKNOWN_PROPERTY_TYPE if the type of a property value
   * is not supported.
   */
  friend std::ostream& operator<<(std::ostream& os, const PropertySet& ps);

  /// Reads a PropertySet object from an input stream.
  /** It expects a string in the format
   * <tt>"[key1=val1,key2=val2,...,keyn=valn]"</tt>. Values are stored as
   * strings. \throw MALFORMED_PROPERTY if the string is not in the expected
   * format
   */
  friend std::istream& operator>>(std::istream& is, PropertySet& ps);
  //@}
};

} // end of namespace dai

#endif
