/* SPDX-License-Identifier: GPL-3.0-or-later
 * SPDX-FileCopyrightText: 2022-2024 Soren Stoutner <soren@stoutner.com>
 *
 * This file is part of Privacy Browser PC <https://www.stoutner.com/privacy-browser-pc/>.
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 *
 * 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 for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <https://www.gnu.org/licenses/>.
 */

// Application headers.
#include "DomainsDatabase.h"
#include "Settings.h"
#include "helpers/UserAgentHelper.h"

// Define the private static schema constants.
const int DomainsDatabase::SCHEMA_VERSION = 7;

// Define the public static constants.
const QString DomainsDatabase::CONNECTION_NAME = QLatin1String("domains_database");
const QString DomainsDatabase::CUSTOM_ZOOM_FACTOR = QLatin1String("custom_zoom_factor");
const QString DomainsDatabase::DOM_STORAGE = QLatin1String("dom_storage");
const QString DomainsDatabase::DOMAIN_NAME = QLatin1String("domain_name");
const QString DomainsDatabase::DOMAINS_TABLE = QLatin1String("domains");
const QString DomainsDatabase::EASYLIST = QLatin1String("easylist");
const QString DomainsDatabase::EASYPRIVACY = QLatin1String("easyprivacy");
const QString DomainsDatabase::FANBOYS_ANNOYANCE_LIST = QLatin1String("fanboys_annoyance_list");
const QString DomainsDatabase::ID = QLatin1String("_id");
const QString DomainsDatabase::JAVASCRIPT = QLatin1String("javascript");
const QString DomainsDatabase::LOCAL_STORAGE = QLatin1String("local_storage");
const QString DomainsDatabase::ULTRALIST = QLatin1String("ultralist");
const QString DomainsDatabase::ULTRAPRIVACY = QLatin1String("ultraprivacy");
const QString DomainsDatabase::USER_AGENT = QLatin1String("user_agent");
const QString DomainsDatabase::ZOOM_FACTOR = QLatin1String("zoom_factor");

// Construct the class.
DomainsDatabase::DomainsDatabase() {}

void DomainsDatabase::addDatabase()
{
    // Add the domain settings database.
    QSqlDatabase domainsDatabase = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), CONNECTION_NAME);

    // Set the database name.
    domainsDatabase.setDatabaseName(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QLatin1String("/domains.db"));

    // Open the database.
    if (domainsDatabase.open())  // Opening the database succeeded.
    {
        // Check to see if the domains table already exists.
        if (domainsDatabase.tables().contains(DOMAINS_TABLE))  // The domains table already exists.
        {
            // Create a schema version query.
            QSqlQuery schemaVersionQuery(domainsDatabase);

            // Query the database schema version.
            schemaVersionQuery.exec(QLatin1String("PRAGMA user_version"));

            // Move to the first record.
            schemaVersionQuery.first();

            // Get the current schema version.
            int currentSchemaVersion = schemaVersionQuery.value(0).toInt();

            // Check to see if the schema has been updated.
            if (currentSchemaVersion < SCHEMA_VERSION)
            {
                // Run the schema update code.
                switch (currentSchemaVersion)
                {
                    // Upgrade from schema version 0 to schema version 1.
                    case 0:
                    {
                        // Create an add JavaScript query.
                        QSqlQuery addJavaScriptQuery(domainsDatabase);

                        // Add the JavaScript column.
                        addJavaScriptQuery.exec(QLatin1String("ALTER TABLE ") + DOMAINS_TABLE + QLatin1String(" ADD COLUMN ") + JAVASCRIPT + QLatin1String(" INTEGER DEFAULT 0"));

                        // Fall through to the next case.
                        [[fallthrough]];
                    }

                    // Upgrade from schema version 1 to schema version 2.
                    case 1:
                    {
                        // Create an add user agent query.
                        QSqlQuery addUserAgentQuery(domainsDatabase);

                        // Add the User Agent column.
                        addUserAgentQuery.exec(QLatin1String("ALTER TABLE ") + DOMAINS_TABLE + QLatin1String(" ADD COLUMN ") + USER_AGENT + QLatin1String(" TEXT DEFAULT '") +
                                               UserAgentHelper::SYSTEM_DEFAULT_DATABASE + QLatin1String("'"));

                        // Fall through to the next case.
                        [[fallthrough]];
                    }

                    // Upgrade from schema version 2 to schema version 3.
                    case 2:
                    {
                        // Create an add zoom factor query.
                        QSqlQuery addZoomFactorQuery(domainsDatabase);

                        // Add the Zoom Factor columns.
                        addZoomFactorQuery.exec(QLatin1String("ALTER TABLE ") + DOMAINS_TABLE + QLatin1String(" ADD COLUMN ") + ZOOM_FACTOR + QLatin1String(" INTEGER DEFAULT 0"));
                        addZoomFactorQuery.exec(QLatin1String("ALTER TABLE ") + DOMAINS_TABLE + QLatin1String(" ADD COLUMN ") + CUSTOM_ZOOM_FACTOR + QLatin1String(" REAL DEFAULT 1.0"));

                        // Fall through to the next case.
                        [[fallthrough]];
                    }

                    // Upgrade from schema version 3 to schema version 4.
                    case 3:
                    {
                        // Create an add DOM storage query.
                        QSqlQuery addDomStorageQuery(domainsDatabase);

                        // Add the DOM Storage column.
                        addDomStorageQuery.exec(QLatin1String("ALTER TABLE ") + DOMAINS_TABLE + QLatin1String(" ADD COLUMN ") + DOM_STORAGE + QLatin1String(" INTEGER DEFAULT 0"));

                        // Fall through to the next case.
                        [[fallthrough]];
                    }

                    // Upgrade from schema version 4 to schema version 5, first used in Privacy Browser PC 0.1.
                    case 4:
                    {
                        // Create an add local storage query.
                        QSqlQuery addLocalStorageQuery(domainsDatabase);

                        // Add the Local Storage column.
                        addLocalStorageQuery.exec(QLatin1String("ALTER TABLE ") + DOMAINS_TABLE + QLatin1String(" ADD COLUMN ") + LOCAL_STORAGE + QLatin1String(" INTEGER DEFAULT 0"));

                        // Fall through to the next case.
                        [[fallthrough]];
                    }

                    // Upgrade from schema version 5 to schema version 6, first used in Privacy Browser PC 0.5.
                    case 5:
                    {
                        // Instantiate a spinner query.
                        QSqlQuery spinnerQuery(domainsDatabase);

                        // Set the query to be forward only (increases performance while iterating over the query).
                        spinnerQuery.setForwardOnly(true);

                        // Prepare the query.
                        spinnerQuery.prepare(QLatin1String("SELECT ") + ID + QLatin1String(",") + JAVASCRIPT + QLatin1String(",") + LOCAL_STORAGE + QLatin1String(",") + DOM_STORAGE +
                                             QLatin1String(" FROM ") + DOMAINS_TABLE);

                        // Execute the query.
                        spinnerQuery.exec();

                        // Update the spinner values so that enabled is 1 and disabled is 2.
                        while (spinnerQuery.next())
                        {
                            // Initialize the new spinner values.
                            int newJavaScriptValue = SYSTEM_DEFAULT;
                            int newLocalStorageValue = SYSTEM_DEFAULT;
                            int newDomStorageValue = SYSTEM_DEFAULT;

                            // Update the new JavaScript value if needed.
                            switch (spinnerQuery.value(JAVASCRIPT).toInt())
                            {
                                // Disabled used to be 1.
                                case 1:
                                {
                                    // Update the value to be 2.
                                    newJavaScriptValue = DISABLED;

                                    break;
                                }

                                // Enabled used to be 2.
                                case 2:
                                {
                                    // Update the new value to be 1.
                                    newJavaScriptValue = ENABLED;

                                    break;
                                }
                            }

                            // Update the new local storage value if needed.
                            switch (spinnerQuery.value(LOCAL_STORAGE).toInt())
                            {
                                // Disabled used to be 1.
                                case 1:
                                {
                                    // Update the new value to be 2.
                                    newLocalStorageValue = DISABLED;

                                    break;
                                }

                                // Enabled used to be 2.
                                case 2:
                                {
                                    // Update the new value to be 1.
                                    newLocalStorageValue = ENABLED;

                                    break;
                                }
                            }

                            // Update the new DOM storage value if needed.
                            switch (spinnerQuery.value(DOM_STORAGE).toInt())
                            {
                                // Disabled used to be 1.
                                case 1:
                                {
                                    // Update the new value to be 2.
                                    newDomStorageValue = DISABLED;

                                    break;
                                }

                                // Enabled used to be 2.
                                case 2:
                                {
                                    // Update the new value to be 1.
                                    newDomStorageValue = ENABLED;

                                    break;
                                }
                            }

                            // Create an update spinner query.
                            QSqlQuery updateSpinnerQuery(domainsDatabase);

                            // Prepare the update spinner query.
                            updateSpinnerQuery.prepare(QLatin1String("UPDATE ") + DOMAINS_TABLE + QLatin1String(" SET ") +
                                                 JAVASCRIPT + QLatin1String(" = :javascript , ") +
                                                 LOCAL_STORAGE + QLatin1String(" = :local_storage , ") +
                                                 DOM_STORAGE + QLatin1String(" = :dom_storage ") +
                                                 QLatin1String(" WHERE ") + ID + QLatin1String(" = :id"));

                            // Bind the values.
                            updateSpinnerQuery.bindValue(QLatin1String(":javascript"), newJavaScriptValue);
                            updateSpinnerQuery.bindValue(QLatin1String(":local_storage"), newLocalStorageValue);
                            updateSpinnerQuery.bindValue(QLatin1String(":dom_storage"), newDomStorageValue);
                            updateSpinnerQuery.bindValue(QLatin1String(":id"), spinnerQuery.value(ID));

                            // Execute the query.
                            updateSpinnerQuery.exec();
                        }

                        // Fall through to the next case.
                        [[fallthrough]];
                    }

                    // Upgrade from schema version 6 to schema version 7, first used in Privacy Browser PC 0.6.
                    case 6:
                    {
                        // Create an add filter lists query.
                        QSqlQuery addFilterListsQuery(domainsDatabase);

                        // Add the filter list columns.
                        addFilterListsQuery.exec(QLatin1String("ALTER TABLE ") + DOMAINS_TABLE + QLatin1String(" ADD COLUMN ") + ULTRAPRIVACY + QLatin1String(" INTEGER DEFAULT 0"));
                        addFilterListsQuery.exec(QLatin1String("ALTER TABLE ") + DOMAINS_TABLE + QLatin1String(" ADD COLUMN ") + ULTRALIST + QLatin1String(" INTEGER DEFAULT 0"));
                        addFilterListsQuery.exec(QLatin1String("ALTER TABLE ") + DOMAINS_TABLE + QLatin1String(" ADD COLUMN ") + EASYPRIVACY + QLatin1String(" INTEGER DEFAULT 0"));
                        addFilterListsQuery.exec(QLatin1String("ALTER TABLE ") + DOMAINS_TABLE + QLatin1String(" ADD COLUMN ") + EASYLIST + QLatin1String(" INTEGER DEFAULT 0"));
                        addFilterListsQuery.exec(QLatin1String("ALTER TABLE ") + DOMAINS_TABLE + QLatin1String(" ADD COLUMN ") + FANBOYS_ANNOYANCE_LIST + QLatin1String(" INTEGER DEFAULT 0"));

                        // Fall through to the next case.
                        // [[fallthrough]];
                    }
                }

                // Create an update scheme version query.
                QSqlQuery updateSchemaVersionQuery(domainsDatabase);

                // Update the schema version.
                updateSchemaVersionQuery.exec(QLatin1String("PRAGMA user_version = ") + QString::number(SCHEMA_VERSION));
            }
        }
        else  // The domains table does not exist.
        {
            // Instantiate a create table query.
            QSqlQuery createTableQuery(domainsDatabase);

            // Prepare the create table query.
            createTableQuery.prepare(QLatin1String("CREATE TABLE ") + DOMAINS_TABLE + QLatin1String("(") +
                                     ID + QLatin1String(" INTEGER PRIMARY KEY, ") +
                                     DOMAIN_NAME + QLatin1String(" TEXT, ") +
                                     JAVASCRIPT + QLatin1String(" INTEGER DEFAULT 0, ") +
                                     LOCAL_STORAGE + QLatin1String(" INTEGER DEFAULT 0, ") +
                                     DOM_STORAGE + QLatin1String(" INTEGER DEFAULT 0, ") +
                                     USER_AGENT + QLatin1String(" TEXT DEFAULT '") + UserAgentHelper::SYSTEM_DEFAULT_DATABASE + QLatin1String("', ") +
                                     ULTRAPRIVACY + QLatin1String(" INTEGER DEFAULT 0, ") +
                                     ULTRALIST + QLatin1String(" INTEGER DEFAULT 0, ") +
                                     EASYPRIVACY + QLatin1String(" INTEGER DEFAULT 0, ") +
                                     EASYLIST + QLatin1String(" INTEGER DEFAULT 0, ") +
                                     FANBOYS_ANNOYANCE_LIST + QLatin1String(" INTEGER DEFAULT 0, ") +
                                     ZOOM_FACTOR + QLatin1String(" INTEGER DEFAULT 0, ") +
                                     CUSTOM_ZOOM_FACTOR + QLatin1String(" REAL DEFAULT 1.0)"));

            // Execute the query.
            if (!createTableQuery.exec())
            {
                // Log any errors.
                qDebug().noquote().nospace() << "Error creating table:  " << domainsDatabase.lastError();
            }

            // Create an update schema version query.
            QSqlQuery updateSchemaVersionQuery(domainsDatabase);

            // Set the schema version.
            updateSchemaVersionQuery.exec(QLatin1String("PRAGMA user_version = ") + QString::number(SCHEMA_VERSION));
        }
    }
    else  // Opening the database failed.
    {
        // Write the last database error message to the debug output.Settings::zoom
        qDebug().noquote().nospace() << "Error opening database:  " << domainsDatabase.lastError();
    }
};

void DomainsDatabase::addDomain(const QString &domainName)
{
    // Add the domain:
    addDomain(domainName, SYSTEM_DEFAULT, SYSTEM_DEFAULT, SYSTEM_DEFAULT, UserAgentHelper::SYSTEM_DEFAULT_DATABASE, SYSTEM_DEFAULT, SYSTEM_DEFAULT, SYSTEM_DEFAULT, SYSTEM_DEFAULT, SYSTEM_DEFAULT,
              SYSTEM_DEFAULT, Settings::zoomFactor());
}

void DomainsDatabase::addDomain(const QString &domainName, const int javaScriptInt, const int localStorageInt, const int domStorageInt, const QString &userAgentDatabaseString,
                                const int ultraPrivacyInt, const int ultraListInt, const int easyPrivacyInt, const int easyListInt, const int fanboysAnnoyanceListInt, const int zoomFactorInt,
                                const double currentZoomFactorDouble)
{
    // Get a handle for the domains database.
    QSqlDatabase domainsDatabase = QSqlDatabase::database(CONNECTION_NAME);

    // Instantiate an add domain settings query.
    QSqlQuery addDomainSettingsQuery(domainsDatabase);

    // Prepare the query.
    addDomainSettingsQuery.prepare(QLatin1String("INSERT INTO ") + DOMAINS_TABLE + QLatin1String(" (") +
                                    DOMAIN_NAME + QLatin1String(", ") +
                                    JAVASCRIPT + QLatin1String(", ") +
                                    LOCAL_STORAGE + QLatin1String(", ") +
                                    DOM_STORAGE + QLatin1String(", ") +
                                    USER_AGENT + QLatin1String(", ") +
                                    ULTRAPRIVACY + QLatin1String(", ") +
                                    ULTRALIST + QLatin1String(", ") +
                                    EASYPRIVACY + QLatin1String(", ") +
                                    EASYLIST + QLatin1String(", ") +
                                    FANBOYS_ANNOYANCE_LIST + QLatin1String(", ") +
                                    ZOOM_FACTOR + QLatin1String(", ") +
                                    CUSTOM_ZOOM_FACTOR + QLatin1String(") ") +
                                    QLatin1String("VALUES (:domain_name, :javascript, :local_storage, :dom_storage, :user_agent, :ultraprivacy, :ultralist, :easyprivacy, :easylist,") +
                                    QLatin1String(":fanboys_annoyance_list, :zoom_factor, :custom_zoom_factor)"));

    // Bind the query values.
    addDomainSettingsQuery.bindValue(QLatin1String(":domain_name"), domainName);
    addDomainSettingsQuery.bindValue(QLatin1String(":javascript"), javaScriptInt);
    addDomainSettingsQuery.bindValue(QLatin1String(":local_storage"), localStorageInt);
    addDomainSettingsQuery.bindValue(QLatin1String(":dom_storage"), domStorageInt);
    addDomainSettingsQuery.bindValue(QLatin1String(":user_agent"), userAgentDatabaseString);
    addDomainSettingsQuery.bindValue(QLatin1String(":ultraprivacy"), ultraPrivacyInt);
    addDomainSettingsQuery.bindValue(QLatin1String(":ultralist"), ultraListInt);
    addDomainSettingsQuery.bindValue(QLatin1String(":easyprivacy"), easyPrivacyInt);
    addDomainSettingsQuery.bindValue(QLatin1String(":easylist"), easyListInt);
    addDomainSettingsQuery.bindValue(QLatin1String(":fanboys_annoyance_list"), fanboysAnnoyanceListInt);
    addDomainSettingsQuery.bindValue(QLatin1String(":zoom_factor"), zoomFactorInt);
    addDomainSettingsQuery.bindValue(QLatin1String(":custom_zoom_factor"), currentZoomFactorDouble);

    // Execute the query.
    addDomainSettingsQuery.exec();
}

QSqlQuery DomainsDatabase::getDomainQuery(const QString &hostname)
{
    // Get a handle for the domains database.
    QSqlDatabase domainsDatabase = QSqlDatabase::database(CONNECTION_NAME);

    // Instantiate the all domain names query.
    QSqlQuery allDomainNamesQuery(domainsDatabase);

    // Set the query to be forward only (increases performance while iterating over the query).
    allDomainNamesQuery.setForwardOnly(true);

    // Prepare the query.
    allDomainNamesQuery.prepare(QLatin1String("SELECT ") + ID + QLatin1String(",") + DOMAIN_NAME + QLatin1String(" FROM ") + DOMAINS_TABLE);

    // Execute the query.
    allDomainNamesQuery.exec();

    // Create a domains settings map.
    QMap<QString, int> domainSettingsMap;

    // Populate the domain settings map.
    while (allDomainNamesQuery.next())
    {
        // Add the domain name and database ID to the map.
        domainSettingsMap.insert(allDomainNamesQuery.value(DOMAIN_NAME).toString(), allDomainNamesQuery.value(ID).toInt());
    }

    // Initialize the database ID tracker.
    int databaseId = -1;

    // Get the database ID if the hostname is found in the domain settings set.
    if (domainSettingsMap.contains(hostname))
    {
        databaseId = domainSettingsMap.value(hostname);
    }

    // Create a subdomain string.
    QString subdomain = hostname;

    // Check all the subdomains of the hostname.
    while ((databaseId == -1) && subdomain.contains(QLatin1String(".")))  // Stop checking when a match is found or there are no more `.` in the hostname.
    {
        // Check to see if the domain settings map contains the subdomain with a `*.` prepended.
        if (domainSettingsMap.contains(QLatin1String("*.") + subdomain))
        {
            // Get the database ID.
            databaseId = domainSettingsMap.value(QLatin1String("*.") + subdomain);
        }

        // Strip out the first subdomain.
        subdomain = subdomain.section(QLatin1Char('.'), 1);
    }

    // Instantiate the domain lookup query.
    QSqlQuery domainLookupQuery(domainsDatabase);

    // Prepare the domain lookup query.
    domainLookupQuery.prepare(QLatin1String("SELECT * FROM ") + DOMAINS_TABLE + QLatin1String(" WHERE ") + ID + QLatin1String(" = ") + QString::number(databaseId));

    // Execute the query.
    domainLookupQuery.exec();

    // Move to the first entry.
    domainLookupQuery.first();

    // Return the query.
    return domainLookupQuery;
}
