#include "database/schemamanager.h"

#include "util/db/fwdsqlquery.h"
#include "util/db/sqltransaction.h"
#include "util/xml.h"
#include "util/logger.h"
#include "util/assert.h"


const QString SchemaManager::SETTINGS_VERSION_STRING = "mixxx.schema.version";
const QString SchemaManager::SETTINGS_MINCOMPATIBLE_STRING = "mixxx.schema.min_compatible_version";

namespace {
    mixxx::Logger kLogger("SchemaManager");

    int readCurrentSchemaVersion(SettingsDAO& settings) {
        QString settingsValue = settings.getValue(SchemaManager::SETTINGS_VERSION_STRING);
        // May be a null string if the schema has not been created. We default the
        // startVersion to 0 so that we automatically try to upgrade to revision 1.
        if (settingsValue.isNull()) {
            return 0; // initial version
        } else {
            bool ok = false;
            int schemaVersion = settingsValue.toInt(&ok);
            VERIFY_OR_DEBUG_ASSERT(ok && (schemaVersion >= 0)) {
                kLogger.critical()
                        << "Invalid database schema version" << settingsValue;
            }
            return schemaVersion;
        }
    }
}

SchemaManager::SchemaManager(const QSqlDatabase& database)
    : m_database(database),
      m_settingsDao(database),
      m_currentVersion(readCurrentSchemaVersion(m_settingsDao)) {
}

bool SchemaManager::isBackwardsCompatibleWithVersion(int targetVersion) const {
    QString backwardsCompatibleVersion =
            m_settingsDao.getValue(SETTINGS_MINCOMPATIBLE_STRING);
    bool ok = false;
    int iBackwardsCompatibleVersion = backwardsCompatibleVersion.toInt(&ok);

    // If the current backwards compatible schema version is not stored in the
    // settings table, assume the current schema version is only backwards
    // compatible with itself.
    if (backwardsCompatibleVersion.isNull() || !ok) {
        // rryan 11/2010 We just added the backwards compatible flags, and some
        // people using the Mixxx trunk are already on schema version 7. This
        // special case is for them. Schema version 7 is backwards compatible
        // with schema version 3.
        if (m_currentVersion == 7) {
            iBackwardsCompatibleVersion = 3;
        } else {
            iBackwardsCompatibleVersion = m_currentVersion;
        }
    }

    // If the target version is greater than the minimum compatible version of
    // the current schema, then the current schema is backwards compatible with
    // targetVersion.
    return iBackwardsCompatibleVersion <= targetVersion;
}

SchemaManager::Result SchemaManager::upgradeToSchemaVersion(
        const QString& schemaFilename,
        int targetVersion) {
    VERIFY_OR_DEBUG_ASSERT(m_currentVersion >= 0) {
        return Result::UpgradeFailed;
    }

    if (m_currentVersion == targetVersion) {
        kLogger.info()
                << "Database schema is up-to-date"
                << "at version" << m_currentVersion;
        return Result::CurrentVersion;
    } else if (m_currentVersion < targetVersion) {
        kLogger.info()
                << "Upgrading database schema"
                << "from version" << m_currentVersion
                << "to version" << targetVersion;
    } else {
        if (isBackwardsCompatibleWithVersion(targetVersion)) {
            kLogger.info()
                    << "Current database schema is newer"
                    << "at version" << m_currentVersion
                    << "and backwards compatible"
                    << "with version" << targetVersion;
            return Result::NewerVersionBackwardsCompatible;
        } else {
            kLogger.warning()
                    << "Current database schema is newer"
                    << "at version" << m_currentVersion
                    << "and incompatible"
                    << "with version" << targetVersion;
            return Result::NewerVersionIncompatible;
        }
    }

    if (kLogger.debugEnabled()) {
        kLogger.debug()
                << "Loading database schema migrations from"
                << schemaFilename;
    }
    QDomElement schemaRoot = XmlParse::openXMLFile(schemaFilename, "schema");
    if (schemaRoot.isNull()) {
        kLogger.critical()
                << "Failed to load database schema migrations from"
                << schemaFilename;
        return Result::SchemaError;
    }

    QDomNodeList revisions = schemaRoot.childNodes();

    QMap<int, QDomElement> revisionMap;

    for (int i = 0; i < revisions.count(); i++) {
        QDomElement revision = revisions.at(i).toElement();
        QString version = revision.attribute("version");
        VERIFY_OR_DEBUG_ASSERT(!version.isNull()) {
            kLogger.critical()
                    << "Failed to parse database schema migrations from"
                    << schemaFilename;
            return Result::SchemaError;
        }
        int iVersion = version.toInt();
        revisionMap[iVersion] = revision;
    }

    // The checks above guarantee that currentVersion < targetVersion when we
    // get here.
    while (m_currentVersion < targetVersion) {
        int nextVersion = m_currentVersion + 1;

        // Now that we bake the schema.xml into the binary it is a programming
        // error if we include a schema.xml that does not have information on
        // how to get all the way to targetVersion.
        VERIFY_OR_DEBUG_ASSERT(revisionMap.contains(nextVersion)) {
            kLogger.critical()
                     << "Migration path for upgrading database schema"
                     << "from version" << m_currentVersion
                     << "to version" << nextVersion
                     << "is missing";
            return Result::SchemaError;
        }

        QDomElement revision = revisionMap[nextVersion];
        QDomElement eDescription = revision.firstChildElement("description");
        QDomElement eSql = revision.firstChildElement("sql");
        QString minCompatibleVersion = revision.attribute("min_compatible");

        // Default the min-compatible version to the current version string if
        // it's not in the schema.xml
        if (minCompatibleVersion.isNull()) {
            minCompatibleVersion = QString::number(nextVersion);
        }

        VERIFY_OR_DEBUG_ASSERT(!eSql.isNull()) {
            kLogger.critical()
                    << "Failed to parse database schema migrations from"
                    << schemaFilename;
            return Result::SchemaError;
        }

        QString description = eDescription.text();
        QString sql = eSql.text();

        kLogger.info()
                << "Upgrading to database schema to version"
                << nextVersion << ":"
                << description.trimmed();

        SqlTransaction transaction(m_database);

        // TODO(XXX) We can't have semicolons in schema.xml for anything other
        // than statement separators.
        QStringList sqlStatements = sql.split(";");

        QStringListIterator it(sqlStatements);

        bool result = true;
        while (result && it.hasNext()) {
            QString statement = it.next().trimmed();
            if (statement.isEmpty()) {
                // skip blank lines
                continue;
            }
            FwdSqlQuery query(m_database, statement);
            result = query.isPrepared() && query.execPrepared();
        }

        if (result) {
            m_settingsDao.setValue(SETTINGS_VERSION_STRING, nextVersion);
            m_settingsDao.setValue(SETTINGS_MINCOMPATIBLE_STRING, minCompatibleVersion);
            transaction.commit();
            m_currentVersion = nextVersion;
            kLogger.info()
                    << "Upgraded database schema"
                    << "to version" << m_currentVersion;
        } else {
            kLogger.critical()
                    << "Failed to upgrade database schema from version"
                    << m_currentVersion << "to version" << nextVersion;
            transaction.rollback();
            return Result::UpgradeFailed;
        }
    }
    return Result::UpgradeSucceeded;
}
