/*
   Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License, version 2.0,
   as published by the Free Software Foundation.

   This program is also distributed with certain software (including
   but not limited to OpenSSL) that is licensed under separate terms,
   as designated in a particular file or component or in included license
   documentation.  The authors of MySQL hereby grant you an additional
   permission to link the program and your derivative works with the
   separately licensed software that they have included with MySQL.

   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, version 2.0, 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
*/

/*
 * Properties.hpp
 *
 */

#ifndef Properties_hpp
#define Properties_hpp

#include <map>
#include <string>
#include <ios>
#include <iostream>
#include <istream>
#include <ostream>
#include <streambuf>
#include <fstream>
#include <cassert>
#include <climits>

namespace utils {
    
using std::map;
using std::wstring;
using std::ios_base;
using std::istream;
//using std::wistream;
using std::ostream;
//using std::wostream;
using std::streambuf;
//using std::wstreambuf;

/**
 * The Properties class is a specialized map container that stores
 * elements composed of a key string and a corresponding value string.
 *
 * This class offers load/store functions to read properties from or
 * save them to a std::streambuf, stream, or file.  The format for reading
 * and writing properties is the same used by Java Properties:
 *   http://java.sun.com/javase/6/docs/api/java/util/Properties.html
 *   http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html
 *   http://java.sun.com/j2se/1.4.2/docs/api/java/util/Properties.html
 *
 * This implementation supports non-Ascii characters but assumes:
 * ... XXX ...
 *
 * This class derives publicly from std::map.  Generally, this would
 * be problematic, since std::map does not seem to provide a virtual
 * destructor (at least, GNU's Standard C++ Library v3 does not).
 * In other words, without a virtual destructor, std::map does not
 * support derived types whose instances can also pass as std::maps.
 *
 * Under this limitation, the best approach would be for Properties
 * to derive privately (or protectedly) from std::map only, so that
 * Properties instances are not exposed to being deleted through a
 * std::map base pointer.  As a consequence, this requires to
 * publicly redeclare most of the std::map members (e.g.
 * "using std::map<std::wstring,std::wstring>::operator[];").
 *
 * However, class Properties is lucky, though, in that it does not
 * declare any data members and destructor, for it conceptually
 * posesses no state apart from the inherited std::map for the key,
 * value mapping.  It is therefore unproblematic if a Properties
 * instance is deleted through its base pointer.
 *
 * Nevertheless, any classes publicly derived from Properties would
 * have to reconsider the lack of a virtual destuctor in std::map.
 */
class Properties : public map<wstring, wstring>
{
public:
    /**
     * Reads properties from the character file and adds them to this
     * property table.
     */
    void load(const char* filename)
        throw (ios_base::failure);

    /**
     * Reads properties from the character input stream and adds them
     * to this property table.
     */
    void load(istream& is)
        throw (ios_base::failure);

    /**
     * Reads properties from the character buffer and adds them
     * to this property table.
     *
     * The line-oriented format is the same as used by Java Properties.
     * The byte stream is read under the ISO 8859-1 character encoding,
     * so, all non-ISO 8859-1 characters of the key and value strings
     * need to be expressed as an escape sequence.
     */
    void load(streambuf& ib)
        throw (ios_base::failure);

    /**
     * Reads properties from the wide character input stream and adds them
     * to this property table.
     */
    // not implemented yet
    //void load(wistream& is)
    //    throw (ios_base::failure);

    /**
     * Reads properties from the wide character buffer and adds them
     * to this property table.
     *
     * The byte stream is read under the UTF-32/UCS-4 character encoding,
     * and characters of the key and value strings are not parsed as an
     * escape sequence.
     */
    // not implemented yet
    //void load(wstreambuf& ib)
    //    throw (ios_base::failure);

    /**
     * Writes this property table to the character file.
     */
    void store(const char* filename, const wstring* header = NULL) const
        throw (ios_base::failure);

    /**
     * Writes this property table to the character output stream.
     */
    void store(ostream& os, const wstring* header = NULL) const
        throw (ios_base::failure);

    /**
     * Writes this property table to the character buffer.
     *
     * The format is suitable for reading the properties using the load
     * function.  The stream is written using the ISO 8859-1 character
     * encoding and characters of the key and value strings are examined
     * to see whether they should be rendered as an escape sequence.
     *
     * If the header argument is not null, then an ASCII # character, the
     * header string, and a line separator are first written to the output
     * stream. Thus, the header can serve as an identifying comment.
     */
    void store(streambuf& ob, const wstring* header = NULL) const
        throw (ios_base::failure);

    /**
     * Writes this property table to the wide character output stream.
     */
    // not implemented yet
    //void store(wostream& os, const wstring* header = NULL) const
    //    throw (ios_base::failure);

    /**
     * Writes this property table to the wide character buffer.
     *
     * The format is suitable for reading the properties using the load
     * function.  The stream is written using the UTF-32/UCS-4 character
     * encoding, and characters of the key and value strings are not
     * rendered as an escape sequence.
     *
     * If the header argument is not null, then an ASCII # character, the
     * header string, and a line separator are first written to the output
     * stream. Thus, the header can serve as an identifying comment.
     */
    // not implemented yet
    //void store(wstreambuf& ob, const wstring* header = NULL) const
    //    throw (ios_base::failure);

protected:
    static bool isWS(int c);
    static bool isNL(int c);
    static bool isComment(int c);
    static bool isAssign(int c);
    static bool isKeyTerminator(int c);
    static bool isEsc(int c);
    static void skipWS(streambuf& ib);
    static void skipComment(streambuf& ib);
    static void readIgnored(streambuf& ib);
    static void readEsc(wstring& s, streambuf& ib);
    static void readKey(wstring& s, streambuf& ib);
    static void readValue(wstring& s, streambuf& ib);
    static bool isPrintableAscii(wchar_t c);
    static void writeAsciiEsc(streambuf& os, wchar_t c);
    static void writeUnicodeEsc(streambuf& os, wchar_t c);
    static void writeKey(streambuf& os, const wstring& s);
    static void writeValue(streambuf& os, const wstring& s);
    static void writeChar(streambuf& os, char c);
};

inline istream&
operator>>(istream& s, Properties& p)
{
    p.load(s);
    return s;
}

/*
// not implemented yet
inline wistream&
operator>>(wistream& s, Properties& p)
{
    p.load(s);
    return s;
}
*/

inline ostream&
operator<<(ostream& s, const Properties& p)
{
    p.store(s);
    return s;
}

/*
// not implemented yet
inline wostream&
operator<<(wostream& s, const Properties& p)
{
    p.store(s);
    return s;
}
*/


// ---------------------------------------------------------------------------
// Properties Implementation
// ---------------------------------------------------------------------------

using std::cout;
using std::wcout;
using std::endl;
using std::ifstream;
using std::ofstream;
using std::streambuf;
using std::stringbuf;

// ---------------------------------------------------------------------------

inline bool
Properties::isWS(int c)
{
    switch (c) {
    case 0x09: // '\t' HT
    case 0x0c: // '\f' FF
    case 0x20: // ' '  SPACE
        return true;
    }
    return false;
}

inline bool
Properties::isNL(int c)
{
    switch (c) {
    case 0x0a: // '\n' LF
    case 0x0d: // '\r' CR
        return true;
    }
    return false;
}

inline bool
Properties::isComment(int c)
{
    switch (c) {
    case 0x21: // '!'
    case 0x23: // '#'
        return true;
    }
    return false;
}

inline bool
Properties::isAssign(int c)
{
    switch (c) {
    case 0x3a: // ':'
    case 0x3d: // '='
        return true;
    }
    return false;
}

inline bool
Properties::isKeyTerminator(int c)
{
    return isWS(c) || isAssign(c) || isNL(c);
}

inline bool
Properties::isEsc(int c)
{
    switch (c) {
    case 0x5c: // '\\'
        return true;
    }
    return false;
}

inline void
Properties::skipWS(streambuf& ib)
{
    int c;
    while ((c = ib.snextc()) != EOF && isWS(c));
}

inline void
Properties::skipComment(streambuf& ib)
{
    int c;
    // comments cannot have escaped line terminators
    while ((c = ib.snextc()) != EOF && !isNL(c));
    ib.sbumpc();
}

inline void
Properties::readIgnored(streambuf& ib)
{
    int c;
    while ((c = ib.sgetc()) != EOF) {
        if (isWS(c)) {
            skipWS(ib);
            c = ib.sgetc();
        }
        if (isNL(c)) {
            ib.sbumpc();
            continue;
        }
        if (isComment(c)) {
            ib.sbumpc();
            skipComment(ib);
            continue;
        }
        return;
    }
}

inline void
Properties::readEsc(wstring& s, streambuf& ib)
{
    int c = ib.sgetc();
    switch (c) {
    case EOF:
        return;
    case 0x0a: // '\n' LF
    case 0x0d: // '\r' CR
        // escaped EOL (CR, LF, CRLF)
        if ((c = ib.snextc()) != 0x0a) // '\n' LF
            ib.sungetc();
        skipWS(ib);
        return;
    case 0x6e: // 'n'
        // LF ("newline") char escape
        c = 0x0a; // '\n'
        break;
    case 0x72: // 'r'
        // CR ("return") char escape
        c = 0x0d; // '\r'
        break;
    case 0x75: { // 'u'
        // unicode escape
        c = ib.sbumpc();
        // store input characters in case not an escape sequence
        wstring raw;
        raw += c; // silently drop backslash by general rule
        unsigned int d = 0;
        for (int i = 0; i < 4; i++) {
            d <<= 4;
            c = ib.sbumpc();
            raw += static_cast<wchar_t>(c); // exlicit cast preferred
            switch (c) {
            case 0x30: case 0x31: case 0x32: case 0x33: case 0x34:
            case 0x35: case 0x36: case 0x37: case 0x38: case 0x39:
                // '0'..'9'
                d += c - 0x30; // - '0'
                break;
            case 0x41: case 0x42: case 0x43: case 0x44: case 0x45: case 0x46:
                // 'A'..'F'
                d += 10 + c - 0x41; // - 'A'
                break;
            case 0x61: case 0x62: case 0x63: case 0x64: case 0x65: case 0x66:
                // 'a'..'f'
                d += 10 + c - 0x61; // - 'a'
                break;
            case EOF:
            default:
                // not a unicode escape sequence, write the raw char sequence
                s += static_cast<wchar_t>(c); // exlicit cast preferred
                return;
            }
        }
        s += static_cast<wchar_t>(d); // exlicit cast preferred
        return;
    }
    default:
        // unrecognized escape no error, silently drop preceding backslash
        break;
    }
    s += static_cast<wchar_t>(c); // exlicit cast preferred
    ib.sbumpc();
}

inline void
Properties::readKey(wstring& s, streambuf& ib)
{
    int c;
    while ((c = ib.sgetc()) != EOF) {
        if (isKeyTerminator(c)) {
            if (isNL(c)) {
                return;
            }
            if (isWS(c)) {
                skipWS(ib);
                c = ib.sgetc();
            }
            if (isAssign(c)) {
                skipWS(ib);
            }
            return;
        }

        ib.sbumpc();
        if (isEsc(c)) {
            readEsc(s, ib);
        } else {
            s += static_cast<wchar_t>(c); // exlicit cast preferred
        }
    }
}

inline void
Properties::readValue(wstring& s, streambuf& ib)
{
    int c;
    while ((c = ib.sgetc()) != EOF) {
        ib.sbumpc();
        if (isNL(c)) {
            return;
        }

        if (isEsc(c)) {
            readEsc(s, ib);
        } else {
            s += static_cast<wchar_t>(c); // exlicit cast preferred
        }
    }
}

inline bool
Properties::isPrintableAscii(wchar_t c)
{
    return (L'\x20' <= c && c <= L'\x7e');
}

inline void
Properties::writeChar(streambuf& os, char c)
{
    int n = os.sputc(c);
    if (n == EOF)
        throw ios_base::failure("Error writing to streambuf");
}

inline void
Properties::writeAsciiEsc(streambuf& os, wchar_t c)
{
    assert(L'\x20' <= c && c <= L'\x7e');
    char d;
    switch (c) {
    case L'\t':     // HT
        d = '\x74'; // 't'
        break;
    case L'\n':    // LF
        d = '\x6e'; // 'n'
        break;
    case L'\f':    // FF
        d = '\x66'; // 'f'
        break;
    case L'\r':    // CR
        d = '\x72'; // 'r'
        break;
    case L' ':     // SPACE
    case L'!':
    case L'#':
    case L':':
    case L'=':
    case L'\\':
        d = static_cast<char>(c); // exlicit cast preferred
        break;
    default:
        // write the raw character
        writeChar(os, static_cast<char>(c)); // explicit cast preferred
        return;
    }
    writeChar(os, '\x5c'); // '\\'
    writeChar(os, d);
}

inline void
Properties::writeUnicodeEsc(streambuf& os, wchar_t c)
{
    assert(c < L'\x20' || L'\x7e' < c);

    // subsequent code depends upon a UCS-2 (UTF-16) or UTF-32
    // encoding of wide characters
    const int w = sizeof(wchar_t);
    assert(w == 2 || w == 4);
    assert(CHAR_BIT == 8);

    // write unicode escape sequence as "\\unnnn" or "\Unnnnnnnn"
    writeChar(os, '\x5c'); // '\\'
    writeChar(os, w == 2 ? '\x75' : '\x55'); // 'u' : 'U'
    static const char ascii[] = { '\x30', '\x31', '\x32', '\x33',
                                  '\x34', '\x35', '\x36', '\x37',
                                  '\x38', '\x39', '\x41', '\x42',
                                  '\x43', '\x44', '\x45', '\x46' }; // '0'..'F'
    for (unsigned int i = w * 8 - 4; i >= 0; i -= 4) {
        writeChar(os, ascii[(c>>i) & 0xF]);
    }
}

inline void
Properties::writeKey(streambuf& os, const wstring& s)
{
    for (wstring::const_iterator i = s.begin(); i != s.end(); ++i) {
        const wchar_t c = *i;
        if (isPrintableAscii(c))
            writeAsciiEsc(os, c);
        else
            writeUnicodeEsc(os, c);
    }
}

inline void
Properties::writeValue(streambuf& os, const wstring& s)
{
    wstring::const_iterator i = s.begin();
    for (; i != s.end() && *i == L'\x20'; ++i) { // L' '  SPACE
        // write leading spaces escaped
        writeAsciiEsc(os, *i);
    }
    for (; i != s.end(); ++i) {
        const wchar_t c = *i;
        if (c == L'\x20') { // L' '  SPACE
            // write embedded or tailing spaces unescaped
            writeChar(os, '\x20'); // ' '  SPACE
        } else if (isPrintableAscii(c)) {
            writeAsciiEsc(os, c);
        } else {
            writeUnicodeEsc(os, c);
        }
    }
}

// ---------------------------------------------------------------------------

inline void
Properties::load(const char* filename)
    throw (ios_base::failure)
{
    assert(filename);
    ifstream ifs;
    ifs.exceptions(ifstream::failbit | ifstream::badbit);
    ifs.open(filename);
    assert(!ifs.bad()); // thrown ios_base::failure
    load(ifs);
    ifs.close();
}

inline void
Properties::load(istream& is)
    throw (ios_base::failure)
{
    istream::iostate exceptions = is.exceptions(); // backup
    is.exceptions(istream::failbit | istream::badbit);
    streambuf* ib = is.rdbuf();
    assert(ib != NULL); // thrown ios_base::failure
    load(*ib);
    is.exceptions(exceptions); // restore
}

inline void
Properties::load(streambuf& ib)
    throw (ios_base::failure)
{
    while (ib.sgetc() != EOF) {
        readIgnored(ib);
        if (ib.sgetc() == EOF)
            return;

        // parse property
        wstring k;
        readKey(k, ib);
        wstring v;
        readValue(v, ib);
        //wcout << "('" << k << "', '" << v << "')" << endl;
        (*this)[k] = v; // YYY
        //this->operator[](k) = v;
    }
}

inline void
Properties::store(const char* filename, const wstring* header) const
    throw (ios_base::failure)
{
    assert(filename);
    ofstream ofs;
    ofs.exceptions(ifstream::failbit | ifstream::badbit);
    ofs.open(filename);
    assert(!ofs.bad());
    store(ofs, header);
    ofs.close();
}

inline void
Properties::store(ostream& os, const wstring* header) const
    throw (ios_base::failure)
{
    ostream::iostate exceptions = os.exceptions(); // backup
    os.exceptions(istream::failbit | istream::badbit);
    streambuf* ob = os.rdbuf();
    assert(ob != NULL); // thrown ios_base::failure
    store(*ob, header);
    os.exceptions(exceptions); // restore
}

inline void
Properties::store(streambuf& os, const wstring* header) const
    throw (ios_base::failure)
{
    // subsequent code for writing the header, keys and values
    // depends upon UCS-2 (UTF-16) or UTF-32 character encoding
    const int w = sizeof(wchar_t);
    assert(w == 2 || w == 4);
    assert(CHAR_BIT == 8);
    assert(L'!' == '\x21');
    assert(L'A' == '\x41');
    assert(L'a' == '\x61');
    assert(L'~' == '\x7e');
    (void)w;
    
    if (header != NULL) {
        writeChar(os, '\x23'); // '#'
        writeKey(os, *header);
        writeChar(os, '\x0a'); // '\n'
    }

    for (const_iterator i = begin(); i != end(); ++i) {
        const wstring& key = i->first;
        const wstring& value = i->second;
        writeKey(os, key);
        writeChar(os, '\x3d'); // '='
        writeValue(os, value);
        writeChar(os, '\x0a'); // '\n'
    }
}

// ---------------------------------------------------------------------------

} // utils

#endif // Properties_hpp
