#include "ConnectionConfig.h"
#include "util.h"
#include <QCoreApplication>
#include <QProcessEnvironment>
#include <QStringBuilder>
#include <QUrl>

namespace {

	struct {
		SslMode mode;
		const char* string;
	} SslModeStringTable[] = {
		{ SslMode::disable, "disable" },
		{ SslMode::allow, "allow" },
		{ SslMode::prefer, "prefer" },
		{ SslMode::require, "require" },
		{ SslMode::verify_ca, "verify-ca" },
		{ SslMode::verify_full, "verify-full" }
	};

    // inline const char *valuePtr(const std::string &v)
    // {
    // 	return v.empty() ? nullptr : v.c_str();
    // }

    struct {
        const char * host = "host";
        const char * hostaddr = "hostaddr";
        const char * port = "port";
        const char * dbname = "dbname";
        const char * user = "user";
        const char * password = "password";
        const char * sslmode = "sslmode";
        const char * sslcert = "sslcert";
        const char * sslkey = "sslkey";
        const char * sslrootcert = "sslrootcert";
        const char * sslcrl = "sslcrl";
    } keywords;

} // end unnamed namespace

QString SslModeToString(SslMode sm)
{	
    for (auto e : SslModeStringTable)
        if (e.mode == sm)
			return QString::fromUtf8(e.string);

    return {};
}

SslMode StringToSslMode(QString s)
{
    for (auto e : SslModeStringTable)
        if (e.string == s)
            return e.mode;

    return SslMode::allow;
}

ConnectionConfig::ConnectionConfig()
    : m_parameters{{"application_name", QCoreApplication::applicationName()}}
{}

const ConnectionGroup *ConnectionConfig::parent() const
{
	return m_group;
}

void ConnectionConfig::setParent(ConnectionGroup *grp)
{
	m_group = grp;
}


void ConnectionConfig::setUuid(const QUuid &uuid)
{
    if (uuid != m_uuid)
    {
		m_dirty = true;
		m_uuid = uuid;
	}
}


const QUuid &ConnectionConfig::uuid() const
{
	return m_uuid;
}


void ConnectionConfig::setName(const QString& desc)
{
    if (m_name != desc)
    {
		m_dirty = true;
		m_name = std::move(desc);
	}
}

const QString& ConnectionConfig::name() const
{
	return m_name;
}

void ConnectionConfig::setHost(const QString& host)
{
    setParameter(keywords.host, host);
}

QString ConnectionConfig::host() const
{
    return getParameter(keywords.host);
}

void ConnectionConfig::setHostAddr(const QString &v)
{
    setParameter(keywords.hostaddr, v);
}

QString ConnectionConfig::hostAddr() const
{
    return getParameter(keywords.hostaddr);
}

void ConnectionConfig::setPort(unsigned short port)
{
    setParameter(keywords.port, QString::number(port));
}

unsigned short ConnectionConfig::port() const
{
    QString s = getParameter(keywords.port);
    if (s.isEmpty())
        return 5432;

    unsigned short port = static_cast<unsigned short>(s.toInt());
    return port;
}

void ConnectionConfig::setUser(const QString& v)
{
    setParameter(keywords.user, v);
}

QString ConnectionConfig::user() const
{
    return getParameter(keywords.user);
}

void ConnectionConfig::setPassword(const QString& v)
{
    setParameter(keywords.password, v);
}

QString ConnectionConfig::password() const
{
    return getParameter(keywords.password);
}

void ConnectionConfig::setDbname(const QString& v)
{
    setParameter(keywords.dbname, v);
}

QString ConnectionConfig::dbname() const
{
    return getParameter(keywords.dbname);
}

void ConnectionConfig::setSslMode(SslMode m)
{
    setParameter(keywords.sslmode, SslModeToString(m));
}

SslMode ConnectionConfig::sslMode() const
{
    QString s = getParameter(keywords.sslmode);
    return StringToSslMode(s);
}

void ConnectionConfig::setSslCert(const QString& v)
{
    setParameter(keywords.sslcert, v);
}

QString ConnectionConfig::sslCert() const
{
    return getParameter(keywords.sslcert);
}

void ConnectionConfig::setSslKey(const QString& v)
{
    setParameter(keywords.sslkey, v);
}

QString ConnectionConfig::sslKey() const
{
    return getParameter(keywords.sslkey);
}

void ConnectionConfig::setSslRootCert(const QString& v)
{
    setParameter(keywords.sslrootcert, v);
}

QString ConnectionConfig::sslRootCert() const
{
    return getParameter(keywords.sslrootcert);
}

void ConnectionConfig::setSslCrl(const QString& v)
{
    setParameter(keywords.sslcrl, v);
}

QString ConnectionConfig::sslCrl() const
{
    return getParameter(keywords.sslcrl);
}

// bool ConnectionConfig::isSameDatabase(const ConnectionConfig &rhs) const
// {
//     return host() == rhs.host()
//         // && m_hostaddr == rhs.m_hostaddr
//         // && m_port     == rhs.m_port
//         // && m_user     == rhs.m_user
//         // && m_password == rhs.m_password
//         // && m_dbname   == rhs.m_dbname
//         ;

// }

bool ConnectionConfig::dirty() const
{
	return m_dirty;
}

void ConnectionConfig::clean()
{
	m_dirty = false;
}

QString ConnectionConfig::makeLongDescription() const
{
	QString result;
	result = name() % " (" % user() % "@" % host() % ":" % QString::number(port()) % "/" % dbname() % ")";
	return result;
}

const QByteArray& ConnectionConfig::encodedPassword() const
{
    return m_encodedPassword;
}

void ConnectionConfig::setEncodedPassword(const QByteArray &encodedPassword)
{
	m_dirty = true;
	m_encodedPassword = encodedPassword;
}

QString ConnectionConfig::escapeConnectionStringValue(const QString &value)
{
	bool contains_spaces = false;
	int escapes = 0;
	for (auto&& c : value)
        if (c == ' ')
            contains_spaces = true;
        else if (c == '\'' || c == '\\')
            ++escapes;

    if (contains_spaces || escapes > 0 || value.length() == 0)
    {
		QString result;
		result.reserve(2 + value.length() + escapes);
		result += '\'';
        for (auto&& c : value)
        {
            if (c == '\'' || c == '\\')
				result += '\\';
			result += c;
		}
		result += '\'';
		return result;
	}
    else
		return value;
}

QString ConnectionConfig::connectionString() const
{
	QString s;

    for (auto && param : m_parameters)
    {
        // maybe we should prevent empty parameters from staying in the map?
        if (!param.second.isEmpty())
        {
            if (!s.isEmpty())
                s += " ";
            s += param.first % "=" % escapeConnectionStringValue(param.second);
        }
    }
    return s;
}

void ConnectionConfig::setParameter(const QString &name, const QString &value)
{
    if (value.isEmpty())
    {
        if (m_parameters.erase(name) > 0)
        {
            m_dirty = true;
        }
    }
    else
    {
        auto findResult = m_parameters.find(name);
        if (findResult == m_parameters.end())
        {
            m_parameters.insert({name, value});
            m_dirty = true;
        }
        else if (findResult->second != value)
        {
            findResult->second = value;
            m_dirty = true;
        }
    }

    //if (name == "sslMode")
    //    m_sslMode = StringToSslMode(value);

    // Would it better to store everything in the map or keep the specific
    // fields for common keywords?
    // Map over fields
    // + can use foreach
    // - not strongly typed, but we should be carefull not to restrict ourselves
    //   the specific fields are more something that helps in the UI
}

QString ConnectionConfig::getParameter(const QString &name) const
{
    auto findResult = m_parameters.find(name);
    if (findResult == m_parameters.end())
        return {};
    return findResult->second;
}

void ConnectionConfig::writeToEnvironment(QProcessEnvironment &env) const
{
    strToEnv(env, "PGHOST", getParameter(keywords.host));
    strToEnv(env, "PGHOSTADDR", getParameter(keywords.hostaddr));
    strToEnv(env, "PGPORT", getParameter(keywords.port));
    strToEnv(env, "PGDATABASE", getParameter(keywords.dbname));
    strToEnv(env, "PGUSER", getParameter(keywords.user));
    strToEnv(env, "PGPASSWORD", getParameter(keywords.password));
    strToEnv(env, "PGSSLMODE", getParameter(keywords.sslmode));
    // strToEnv(env, "PGSSLCERT", m_sslCert);
    // strToEnv(env, "PGSSLKEY", m_sslKey);
    // strToEnv(env, "PGSSLROOTCERT", m_sslRootCert);
    // strToEnv(env, "PGSSLCRL", m_sslCrl);
	strToEnv(env, "PGSSLCOMPRESSION", "0");
	strToEnv(env, "PGCONNECT_TIMEOUT", "10");
	env.insert("PGCLIENTENCODING", "utf8");

	env.remove("PGREQUIRESSL");
}

void ConnectionConfig::strToEnv(QProcessEnvironment &env, const QString &var, const QString &val)
{
	if (val.isEmpty())
		env.remove(var);
	else
		env.insert(var, val);
}

void ConnectionGroup::erase(int idx, int count)
{
	m_connections.remove(idx, count);
}

int ConnectionGroup::add(std::shared_ptr<ConnectionConfig> cc)
{
	cc->setParent(this);
	m_connections.push_back(cc);
	return m_connections.size() - 1;
}

void ConnectionGroup::update(int idx, const ConnectionConfig &cc)
{
	auto node = m_connections[idx];
	*node = cc;
	node->setParent(this);
}
