/**
 * @file Props.cc
 *
 * Implements property file handling
 *
 * Author: Peter Helfer
 * Date: 2012-02-09
 */

#include <stdio.h>
#include <string.h>
#include <limits.h>
#include <libgen.h>
#include <string>
#include <fmt/format.h>
#include "Trace.hh"
#include "Util.hh"
#include "Props.hh"

/**
 * Remove comment, if present, from a line of text. A comment consists
 * of an unquoted # character and all that follows it.
 * @param line The line of text.
 * @param fname File name, used for error messaging only.
 * @param lineNum Line number, used for error messaging only.
 */
static void stripComment(char *line, const char  *fname, uint lineNum)
{
    char quoteChar = 0;
    for (char *p = line; *p != 0; p++) {
        if (quoteChar == '\\') {
            quoteChar = 0;
        } else if (*p == quoteChar) {
            quoteChar = 0;
        } else if (*p == '\\' || *p == '"' || *p == '\'') {
            quoteChar = *p;
        } else if (quoteChar == 0 && *p == '#') {
            *p = 0;
            return;
        }
    }
    if (quoteChar != 0) {
        fmt::print(stderr, "{}: line {} - Warning: unclosed quote: {}\n",
                fname, lineNum, line);
    }
}
        
// Replace occurrences of prop names with their values in s
//
string Props::substProps(string s)
{
    for (auto &prop : props) {
        for (size_t pos = s.find(prop.name);
             pos < string::npos;
             pos = s.find(prop.name, pos + 1))
        {
            s.replace(pos, prop.name.length(), prop.value);
            prop.used = true;
        }
    }
    return s;
}

void Props::fatal(const string &file, uint lineNum, string errMsg, string exhibit)
{
    TRACE_FATAL("{} line {} - {}: '{}'", file, lineNum, errMsg, exhibit);
}

void Props::fatal(Props::Prop *p, string errMsg)
{
    fatal(p->propsFile, p->lineNum, errMsg, p->value);
}

/**
 * Constructor
 */
Props::Props(const char *fname)
{
    readProps(fname);
}

void Props::readProps(const char *fname)
{
    FILE *fp = fopen(fname, "r");
    if (fp == NULL) {
        perror(fname);
        TRACE_FATAL("Failed to open {}\n", fname);
    }

    this->fname = fname;
    if (topLevelFname.empty()) {
        topLevelFname = fname;
    }

    char line[1000];

    for (uint lineNum = 1; fgets(line, sizeof(line), fp) != NULL; lineNum++) {
        stripComment(line, fname, lineNum);
        if (Util::isBlank(line)) continue;

        char *colonPos = strchr(line, ':');
        if (colonPos == NULL) {
            TRACE_FATAL("{}: line {} - Bad property (no colon): {}\n",
                        fname, lineNum, line);
        }

        // A double colon suppresses the "Duplicate property" warning
        // when overwriting aproperty's value
        //
        bool doubleColon = (*(colonPos + 1) == ':');
        
        string name = Util::wstrip(string(line).substr(0, colonPos - line));
        string value = Util::wstrip(doubleColon ? colonPos + 2 : colonPos + 1);

        if (Util::strCiEq(name, "include")) {
            // Include directive
            //
            string path = value;
            if (path[0] != '/') {
                // Relative path. Prepend directory path of current file.
                //
                char fnameCopy[strlen(fname)+1];
                strcpy(fnameCopy, fname);
                path = string(dirname(fnameCopy)) + '/' + path;
            }
            readProps(path.c_str());
        } else {
            // Ordinary prop
            //
            if (value.size() == 0) {
                TRACE_FATAL("{}: line {} - Bad property (no value): {}\n",
                            fname, lineNum, line);
            }

            value = substProps(value);

            Prop *p = findProp(name, false);
            if (p == NULL) {
                uint n = props.size();
                props.resize(n + 1);
                p = &props[n];
                p->name = name;
            } else if (!p->immutable && !doubleColon) {
                TRACE_WARN("{}: line {} - Duplicate property: {}\n",
                            fname, lineNum, name);
            }
            
            if (!p->immutable) {
                p->value = value;
                p->propsFile = fname;
                p->lineNum = lineNum;
            }
        }
    }
}

Props::Prop *Props::findProp(const string &name, bool required)
{
    for (uint i = 0; i < props.size(); i++) {
        if (Util::strCiEq(props[i].name, name)) {
            props[i].used = true;
            return &props[i];
        }
    }
    if (required) {
        TRACE_FATAL("Property '{}' not found in {}\n", name, topLevelFname);
    } else {
        return NULL;
    }
}

string Props::getString(const string &name, const string &defaultVal)
{
    Prop *p = findProp(name, false);

    if (p != NULL) {
        return (p->value);
    } else {
        return defaultVal;
    }
}

string  Props::getString(const string &name)
{
    return findProp(name, true)->value;
}

int Props::propToInt(Props::Prop *p)
{
    string errMsg;
    int val = Util::strToInt(p->value, errMsg);
    if (!errMsg.empty()) {
        fatal(p, errMsg);
    }
    return val;
}

int Props::getInt(const string &name, int defaultVal)
{
    Prop *p = findProp(name, false);
    if (p != NULL) {
        return propToInt(p);
    } else {
        return defaultVal;
    }
}

int Props::getInt(const string &name)
{
    return propToInt(findProp(name, true));
}

uint Props::propToUint(Props::Prop *p)
{
    string errMsg;
    uint val = Util::strToUint(p->value, errMsg);
    if (!errMsg.empty()) {
        fatal(p, errMsg);
    }
    return val;
}

uint Props::getUint(const string &name, uint defaultVal)
{
    Prop *p = findProp(name, false);
    if (p != NULL) {
        return propToUint(p);
    } else {
        return defaultVal;
    }
}

uint Props::getUint(const string &name)
{
    return propToUint(findProp(name, true));
}

double Props::propToDouble(Props::Prop *p)
{
    string errMsg;
    double val = Util::strToDouble(p->value, errMsg);
    if (!errMsg.empty()) {
        fatal(p, errMsg);
    }
    return val;
}

double Props::getDouble(const string &name, double defaultVal)
{
    Prop *p = findProp(name, false);
    if (p != NULL) {
        return propToDouble(p);
    } else {
        return defaultVal;
    }
}

double Props::getDouble(const string &name)
{
    return propToDouble(findProp(name, true));
}

bool Props::propToBool(Props::Prop *p)
{
    string errMsg;
    bool val = Util::strToBool(p->value, errMsg);
    if (!errMsg.empty()) {
        fatal(p, errMsg);
    }
    return val;
}

bool Props::getBool(const string &name, bool defaultVal)
{
    Prop *p = findProp(name, false);
    if (p != NULL) {
        return propToBool(p);
    } else {
        return defaultVal;
    }
}

bool Props::getBool(const string &name)
{
    return propToBool(findProp(name, true));
}

/**
 * Remove initial "{" and final "}" from a vector of tokens (destructive)
 * @param tokens
 */
void Props::removeBraces(std::vector<string> &tokens, Props::Prop *p)
{
    if (tokens[0] == "{" && tokens[tokens.size() - 1] == "}") {
        tokens.pop_back();
        tokens.erase(tokens.begin());
    }
    for (uint i = 0; i < tokens.size(); i++) {
        if(Util::strEq(tokens[i], "{") || Util::strEq(tokens[i], "}")) {
            fatal(p, "Unbalanced or misplaced brace");
        }
    }
}

std::vector<string> Props::propToStringVector(Props::Prop *p)
{
    char copy[p->value.size() + 1];
    strcpy(copy, p->value.c_str());

    std::vector<string> vals;

    // List of values may be enclosed in { }.
    // This makes it possible to specify an empty list.

    const char *errMsg = NULL;
    std::vector<string> tokens = 
        Util::tokenize(copy, " \t", errMsg, "", "{}");
    if (errMsg != NULL) {
        fatal(p, errMsg);
    }

    removeBraces(tokens, p);
    for (uint i = 0; i < tokens.size(); i++) {
        vals.push_back(tokens[i]);
    }
    return vals;
}

std::vector<string> Props::getStringVector(const string &name, const std::vector<string> defaultVal)
{
    Prop *p = findProp(name, false);
    if (p != NULL) {
        return propToStringVector(p);
    } else {
        return defaultVal;
    }
}

std::vector<string> Props::getStringVector(const string &name)
{
    return propToStringVector(findProp(name, true));
}

std::vector<int> Props::propToIntVector(Props::Prop *p)
{
    char copy[p->value.size() + 1];
    strcpy(copy, p->value.c_str());
    Prop dummyProp = *p;

    std::vector<int> vals;

    // List of values may be enclosed in { }.
    // This makes it possible to specify an empty list.

    const char *errMsg = NULL;
    std::vector<string> tokens = 
        Util::tokenize(copy, " \t", errMsg, "", "{}");
    if (errMsg != NULL) {
        fatal(p, errMsg);
    }

    removeBraces(tokens, p);
    for (uint i = 0; i < tokens.size(); i++) {
        dummyProp.value = tokens[i];
        vals.push_back(propToInt(&dummyProp));
    }
    return vals;
}

std::vector<int> Props::getIntVector(const string &name, const std::vector<int> defaultVal)
{
    Prop *p = findProp(name, false);
    if (p != NULL) {
        return propToIntVector(p);
    } else {
        return defaultVal;
    }
}

std::vector<int> Props::getIntVector(const string &name)
{
    return propToIntVector(findProp(name, true));
}

std::vector<uint> Props::propToUintVector(Props::Prop *p)
{
    char copy[p->value.size() + 1];
    strcpy(copy, p->value.c_str());
    Prop dummyProp = *p;

    std::vector<uint> vals;

    // List of values may be enclosed in { } as separate
    // tokens. This makes it possible to specify an empty list.

    const char *errMsg = NULL;
    std::vector<string> tokens = Util::tokenize(copy, " \t", errMsg);
    if (errMsg != NULL) {
        fatal(p, errMsg);
    }

    removeBraces(tokens, p);
    for (uint i = 0; i < tokens.size(); i++) {
        dummyProp.value = tokens[i];
        vals.push_back(propToUint(&dummyProp));
    }
    return vals;
}

std::vector<uint> Props::getUintVector(const string &name, const std::vector<uint> defaultVal)
{
    Prop *p = findProp(name, false);
    if (p != NULL) {
        return propToUintVector(p);
    } else {
        return defaultVal;
    }
}

std::vector<uint> Props::getUintVector(const string &name)
{
    return propToUintVector(findProp(name, true));
}

std::vector<double> Props::propToDoubleVector(Props::Prop *p)
{
    char copy[p->value.size() + 1];
    strcpy(copy, p->value.c_str());
    Prop dummyProp = *p;

    std::vector<double> vals;

    // List of values may be enclosed in { } as separate
    // tokens. This makes it possible to specify an empty list.

    const char *errMsg = NULL;
    std::vector<string> tokens = Util::tokenize(copy, " \t", errMsg);
    if (errMsg != NULL) {
        fatal(p, errMsg);
    }

    removeBraces(tokens, p);
    for (uint i = 0; i < tokens.size(); i++) {
        dummyProp.value = tokens[i];
        vals.push_back(propToDouble(&dummyProp));
    }
    return vals;
}

std::vector<double> Props::getDoubleVector(const string &name, const std::vector<double> defaultVal)
{
    Prop *p = findProp(name, false);
    if (p != NULL) {
        return propToDoubleVector(p);
    } else {
        return defaultVal;
    }
}

std::vector<double> Props::getDoubleVector(const string &name)
{
    return propToDoubleVector(findProp(name, true));
}

std::vector<std::vector<double> > Props::propToDoubleMatrix(Props::Prop *p)
{
    char copy[p->value.size() + 1];
    strcpy(copy, p->value.c_str());
    Prop dummyProp = *p;

    // The whole matrix and the individual row vectors must be
    // delimited by braces, like so:
    // { { 1.0 2.0 3.0 } { 4.0 5.0 6.0 } }
    // Furthermore, the row vectors must be of equal size.

    const char *errMsg = NULL;
    std::vector<string> tokens = 
        Util::tokenize(copy, " \t", errMsg, "", "{}");
    if(errMsg != NULL) {
        fatal(p, errMsg);
    }

    // strip the initial and final braces
    //
    if(tokens[0] != "{" || tokens[tokens.size() - 1] != "}") {
        fatal(p, "Matrix not brace-enclosed.");
    }
    tokens.pop_back();
    tokens.erase(tokens.begin());

    // Parse the row vectors
    //
    std::vector<std::vector<double> > vals;
    std::vector<double> row;
    enum TokenType { LBRACE, ELEM };
    TokenType expect = LBRACE;

    for (uint i = 0; i < tokens.size(); i++) {
        switch (expect) {
            case LBRACE:
                if(tokens[i] != "{") {
                    fatal(p->propsFile, p->lineNum, 
                          "Expected '{', got", tokens[i]);
                }
                expect = ELEM;
                break;
            case ELEM:
                if (tokens[i] == "{") {
                    fatal(p, "Expected element, got '{'");
                } else if (tokens[i] == "}") {
                    vals.push_back(row);
                    row.clear();
                    expect = LBRACE;
                } else {
                    dummyProp.value = tokens[i];
                    row.push_back(propToDouble(&dummyProp));
                }
                break;
            default:
                TRACE_FATAL("logic error");
        }
    }
    if(expect != LBRACE) {
        fatal(p, "unbalanced braces");
    }

    for (uint i = 1; i < vals.size(); i++) {
        if (vals[i].size() != vals[0].size()) {
            fatal(p, "rows don't have equal size.");
        }
    }
    return vals;
}

std::vector<std::vector<double> > Props::getDoubleMatrix(const string &name, const std::vector<std::vector<double> > defaultVal)
{
    Prop *p = findProp(name, false);
    if (p != NULL) {
        return propToDoubleMatrix(p);
    } else {
        return defaultVal;
    }
}

std::vector<std::vector<double> > Props::getDoubleMatrix(const string &name)
{
    return propToDoubleMatrix(findProp(name, true));
}

/**
 * Associate a name with a string value.
 * @param name The name
 * @param value The value
 * @param immutable If true, then subsequent attempts to change the value
 *        will have no effect. This is useful for setting properties by a
 *        command line option and not have them be overwritten by
 *        subsequent property file reading.
 */
void Props::setString(const string &name, string value, bool immutable)
{
    Prop *p = findProp(name, false);
    if (p == NULL) {
        uint n = props.size();
        props.resize(n + 1);
        p = &props[n];
        p->name = name;
    }
    if (!p->immutable) {
        p->value = value;
        p->lineNum = -1;
        p->immutable = immutable;
    }
}

// TODO: change all the buf and sprintf stuff to fmt::format

void Props::setInt(const string &name, int value)
{
    char buf[64];
    sprintf(buf, "%d", value);
    setString(name, buf);
}

void Props::setUint(const string &name, uint value)
{
    char buf[64];
    sprintf(buf, "%d", value);
    setString(name, buf);
}

void Props::setDouble(const string &name, double value)
{
    char buf[64];
    sprintf(buf, "%f", value);
    setString(name, buf);
}

void Props::setBool(const string &name, bool value)
{
    char buf[64];
    sprintf(buf, "%s", value ? "true" : "false");
    setString(name, buf);
}

void Props::setStringVector(const string &name, std::vector<string> v)
{
    char buf[1024];
    char buf1[32];
    sprintf(buf, "{ ");
    for (uint i = 0; i < v.size(); i++) {
        sprintf(buf1, "%s ", v[i].c_str());
        strcat(buf, buf1);
    }
    strcat(buf, " }");
    setString(name, buf);
}
    
void Props::setIntVector(const string &name, std::vector<int> v)
{
    char buf[1024];
    char buf1[32];
    sprintf(buf, "{ ");
    for (uint i = 0; i < v.size(); i++) {
        sprintf(buf1, "%d ", v[i]);
        strcat(buf, buf1);
    }
    strcat(buf, " }");
    setString(name, buf);
}
    
void Props::setUintVector(const string &name, std::vector<uint> v)
{
    char buf[1024];
    char buf1[32];
    sprintf(buf, "{ ");
    for (uint i = 0; i < v.size(); i++) {
        sprintf(buf1, "%u ", v[i]);
        strcat(buf, buf1);
    }
    strcat(buf, " }");
    setString(name, buf);
}
    
void Props::setDoubleVector(const string &name, std::vector<double> v)
{
    char buf[1024];
    char buf1[32];
    sprintf(buf, "{ ");
    for (uint i = 0; i < v.size(); i++) {
        sprintf(buf1, "%f ", v[i]);
        strcat(buf, buf1);
    }
    strcat(buf, " }");
    setString(name, buf);
}
    
void Props::setDoubleMatrix(const string &name, std::vector<std::vector<double> > m)
{
    char buf[1024];
    char buf1[32];
    sprintf(buf, "{ ");
    for (uint i = 0; i < m.size(); i++) {
        strcat(buf, "{ ");
        for (uint j = 0; j < m[i].size(); j++) {
            sprintf(buf1, "%f ", m[i][j]);
            strcat(buf, buf1);
        }
        strcat(buf, "} ");
    }
    strcat(buf, "}");
    setString(name, buf);
}

void Props::reportUnused(bool isFatal)
{
    bool someUnused = false;
    for (uint i = 0; i < props.size(); i++) {
        if (!props[i].used) {
            TRACE_WARN("{}: line {} - Unused property: {}",
                       props[i].propsFile, props[i].lineNum, props[i].name);
            someUnused = true;
        }
    }
    ABORT_IF(someUnused && isFatal, "Unused properties", 0);
}

string Props::toString()
{
    string s;
    
    for (uint i = 0; i < props.size(); i++) {
        s.append(props[i].name);
        s.append(": ");
        s.append(props[i].value);
        s.append("\n");
    }
    return s;
}