#include "htmlexport.h"
#include "services/notifymanager.h"
#include "schemaresolver.h"
#include <QFile>
#include <QDebug>
#include <QRegularExpression>
#include <QStringEncoder>

QString HtmlExport::getFormatName() const
{
    return "HTML";
}

ExportManager::StandardConfigFlags HtmlExport::standardOptionsToEnable() const
{
    return ExportManager::CODEC;
}

QString HtmlExport::getExportConfigFormName() const
{
    return "HtmlExportConfig";
}

void HtmlExport::validateOptions()
{
    bool header = cfg.HtmlExport.PrintHeader.get();
    EXPORT_MANAGER->updateVisibilityAndEnabled(cfg.HtmlExport.PrintDataTypes, true, header);
}

QString HtmlExport::defaultFileExtension() const
{
    return "html";
}

CfgMain* HtmlExport::getConfig()
{
    return &cfg;
}

bool HtmlExport::beforeExportQueryResults(const QString& query, QList<QueryExecutor::ResultColumnPtr>& columns, const QHash<ExportManager::ExportProviderFlag, QVariant> providedData)
{
    Q_UNUSED(query);
    Q_UNUSED(providedData);

    if (!beginDoc(tr("SQL query results")))
        return false;

    columnTypes = QueryExecutor::resolveColumnTypes(db, columns, true);

    writeln("<table>");
    incrIndent();
    if (printHeader)
    {
        QString viewLabel = tr("Query:");
        int colSpan = printRownum ? columns.size() + 1 : columns.size();
        writeln("<tr class=\"title\">");
        incrIndent();
        writeln(QString("<td colspan=\"%1\" align=\"center\"><b>%2</b></td>").arg(colSpan).arg(viewLabel));
        decrIndent();
        writeln("</tr>");
        writeln("<tr>");
        incrIndent();
        writeln(QString("<td colspan=\"%1\">").arg(colSpan));
        incrIndent();
        writeln(QString("<pre>%1</pre>").arg(escape(query)));
        decrIndent();
        writeln("</td>");
        decrIndent();
        writeln("</tr>");

        writeln("<tr class=\"header\">");
        incrIndent();
        if (printRownum)
        {
            writeln("<th align=\"right\">");
            incrIndent();
            writeln("<b><i>#</i></b>");
            decrIndent();
            writeln("</th>");
        }

        QString column;
        int i = 0;
        for (const QueryExecutor::ResultColumnPtr& col : columns)
        {
            writeln("<th>");
            incrIndent();
            column = QString("<b>%1</b>").arg(col->displayName);
            if (printDatatypes)
            {
                if (!columnTypes[i].isNull())
                    column.append("<br/>" + columnTypes[i].toFullTypeString());
                else
                    column.append("<br/>" + tr("no type"));
            }
            writeln(column);
            decrIndent();
            writeln("</th>");
            i++;
        }
        decrIndent();
        writeln("</tr>");
    }

    currentDataRow = 0;
    return true;
}

bool HtmlExport::exportQueryResultsRow(SqlResultsRowPtr row)
{
    return exportDataRow(row);
}

bool HtmlExport::afterExportQueryResults()
{
    decrIndent();
    writeln("</table>");
    writeln("<br/><br/>");
    return true;
}

bool HtmlExport::beforeExportSingleTable(const QString& database, const QString& table)
{
    Q_UNUSED(database);
    return beginDoc(tr("Exported table: %1").arg(table));
}

bool HtmlExport::afterExportSingleTable()
{
    return true;
}

bool HtmlExport::beforeExportSingleView(const QString& database, const QString& name)
{
    Q_UNUSED(database);
    return beginDoc(tr("Exported view: %1").arg(name));
}

bool HtmlExport::afterExportSingleView()
{
    return true;
}

bool HtmlExport::exportTable(const QString& database, const QString& table, const QStringList& columnNames, const QString& ddl, SqliteCreateTablePtr createTable, const QHash<ExportManager::ExportProviderFlag, QVariant> providedData)
{
    Q_UNUSED(database);
    Q_UNUSED(ddl);
    Q_UNUSED(columnNames);
    Q_UNUSED(providedData);

    int colCount = createTable->columns.size();
    int colSpan = printRownum ? colCount + 1 : colCount;

    writeln("<table>");
    incrIndent();

    QString tableLabel = tr("Table: %1").arg(table);
    writeln("<tr class=\"title\">");
    incrIndent();
    writeln(QString("<td colspan=\"%1\" align=\"center\">%2</td>").arg(colSpan).arg(tableLabel));
    decrIndent();
    writeln("</tr>");

    if (printHeader)
    {
        writeln("<tr class=\"header\">");
        incrIndent();
        if (printRownum)
        {
            writeln("<th align=\"right\">");
            incrIndent();
            writeln("<b><i>#</i></b>");
            decrIndent();
            writeln("</th>");
        }

        QString column;
        for (SqliteCreateTable::Column* col : createTable->columns)
        {
            writeln("<th>");
            incrIndent();
            column = QString("<b>%1</b>").arg(col->name);
            if (printDatatypes)
            {
                if (col->type)
                    column.append("<br/>" + col->type->toDataType().toFullTypeString());
                else
                    column.append("<br/>" + tr("no type"));
            }
            writeln(column);
            decrIndent();
            writeln("</th>");
        }
        decrIndent();
        writeln("</tr>");
    }

    columnTypes.clear();
    for (SqliteCreateTable::Column* col : createTable->columns)
    {
        if (col->type)
            columnTypes << col->type->toDataType();
        else
            columnTypes << DataType();
    }

    currentDataRow = 0;
    return true;
}

bool HtmlExport::exportDataRow(SqlResultsRowPtr data)
{
    currentDataRow++;

    writeln("<tr class=\"data\">");
    incrIndent();
    if (printRownum)
    {
        writeln("<td class=\"rownum\">");
        incrIndent();
        writeln(QString("<i>%1</i>").arg(currentDataRow));
        decrIndent();
        writeln("</td>");
    }

    QString align;
    QString cellValue;
    QString cellStyle;
    int i = 0;
    for (const QVariant& value : data->valueList())
    {
        if (columnTypes[i].isNumeric())
            align = "right";
        else
            align = "left";

        if (isNull(value))
        {
            cellValue = "<i>NULL</i>";
            cellStyle = " class=\"null\"";
        }
        else
        {
            cellStyle = "";
            if (value.toString().trimmed().isEmpty())
                cellValue = "&nbsp;";
            else
            {
                cellValue = value.toString();
                cellValue.truncate(byteLengthLimit);
                cellValue = escape(cellValue);
            }
        }
        writeln(QString("<td align=\"%1\"%2>").arg(align, cellStyle));
        incrIndent();
        writeln(cellValue);
        decrIndent();
        writeln("</td>");
        i++;
    }
    decrIndent();
    writeln("</tr>");
    return true;
}

bool HtmlExport::exportVirtualTable(const QString& database, const QString& table, const QStringList& columnNames, const QString& ddl, SqliteCreateVirtualTablePtr createTable, const QHash<ExportManager::ExportProviderFlag, QVariant> providedData)
{
    Q_UNUSED(database);
    Q_UNUSED(ddl);
    Q_UNUSED(columnNames);
    Q_UNUSED(createTable);
    Q_UNUSED(providedData);

    int colCount = columnNames.size();
    int colSpan = printRownum ? colCount + 1 : colCount;

    writeln("<table>");
    incrIndent();

    QString tableLabel = tr("Table: %1").arg(table);
    writeln("<tr class=\"title\">");
    incrIndent();
    writeln(QString("<td colspan=\"%1\" align=\"center\">%2 <font color=\"#777777\">(%3)</font></td>").arg(colSpan).arg(tableLabel, tr("virtual")));
    decrIndent();
    writeln("</tr>");

    if (printHeader)
    {
        writeln("<tr class=\"header\">");
        incrIndent();
        if (printRownum)
        {
            writeln("<th align=\"right\">");
            incrIndent();
            writeln("<b><i>#</i></b>");
            decrIndent();
            writeln("</th>");
        }

        for (const QString& col : columnNames)
        {
            writeln("<th>");
            incrIndent();
            writeln(QString("<b>%1</b>").arg(col));
            decrIndent();
            writeln("</th>");
        }
        decrIndent();
        writeln("</tr>");
    }

    columnTypes.clear();
    for (int i = 0, total = columnNames.size(); i < total; ++i)
        columnTypes << DataType();

    currentDataRow = 0;
    return true;
}

bool HtmlExport::exportTableRow(SqlResultsRowPtr data)
{
    return exportDataRow(data);
}

bool HtmlExport::afterExportTable()
{
    decrIndent();
    writeln("</table>");
    writeln("<br/><br/>");
    return true;
}

bool HtmlExport::beforeExportDatabase(const QString& database)
{
    if (!beginDoc(tr("Exported database: %1").arg(database)))
        return false;

    return true;
}

bool HtmlExport::exportIndex(const QString& database, const QString& name, const QString& ddl, SqliteCreateIndexPtr createIndex)
{
    Q_UNUSED(database);
    Q_UNUSED(ddl);

    writeln("<table>");
    incrIndent();

    writeln("<tr class=\"title\">");
    incrIndent();
    writeln(QString("<td align=\"center\" colspan=\"3\">%1</td>").arg(tr("Index: %1").arg("<b>" + name + "</b>")));
    decrIndent();
    writeln("</tr>");

    writeln("<tr>");
    incrIndent();
    writeln(QString("<td align=\"right\" class=\"rownum\">%1</td>").arg(tr("For table:")));
    writeln(QString("<td colspan=\"2\">%1</td>").arg(createIndex->table));
    decrIndent();
    writeln("</tr>");

    writeln("<tr>");
    incrIndent();
    writeln(QString("<td align=\"right\" class=\"rownum\">%1</td>").arg(tr("Unique:")));
    writeln(QString("<td colspan=\"2\">%1</td>").arg(createIndex->uniqueKw ? tr("Yes") : tr("No")));
    decrIndent();
    writeln("</tr>");

    writeln("<tr class=\"header\">");
    incrIndent();
    writeln(QString("<td><b>%1</b></td>").arg(tr("Column")));
    writeln(QString("<td><b>%1</b></td>").arg(tr("Collating")));
    writeln(QString("<td><b>%1</b></td>").arg(tr("Sort order")));
    decrIndent();
    writeln("</tr>");

    QString collate;
    for (SqliteOrderBy* idxCol : createIndex->indexedColumns)
    {
        collate = idxCol->getCollation();

        writeln("<tr>");
        incrIndent();
        writeln(QString("<td>%1</td>").arg(escape(idxCol->getColumnString())));
        writeln(QString("<td align=\"center\">%1</td>").arg(collate.isNull() ? "&nbsp;" : escape(collate)));
        writeln(QString("<td align=\"center\">%1</td>").arg(idxCol->order == SqliteSortOrder::null ? "&nbsp;" : sqliteSortOrder(idxCol->order)));
        decrIndent();
        writeln("</tr>");
    }

    decrIndent();
    writeln("</table>");
    writeln("<br/><br/>");
    return true;
}

bool HtmlExport::exportTrigger(const QString& database, const QString& name, const QString& ddl, SqliteCreateTriggerPtr createTrigger)
{
    Q_UNUSED(database);
    Q_UNUSED(ddl);

    writeln("<table>");
    incrIndent();

    writeln("<tr class=\"title\">");
    incrIndent();
    writeln(QString("<td align=\"center\" colspan=\"2\">%1</td>").arg(tr("Trigger: %1").arg("<b>" + name + "</b>")));
    decrIndent();
    writeln("</tr>");

    writeln("<tr>");
    incrIndent();
    writeln(QString("<td align=\"right\" class=\"rownum\">%1</td>").arg(tr("Activated:")));
    writeln(QString("<td><code>%1</code></td>").arg(SqliteCreateTrigger::time(createTrigger->eventTime)));
    decrIndent();
    writeln("</tr>");

    QString event = createTrigger->event ? SqliteCreateTrigger::Event::typeToString(createTrigger->event->type) : "";
    writeln("<tr>");
    incrIndent();
    writeln(QString("<td align=\"right\" class=\"rownum\">%1</td>").arg(tr("Action:")));
    writeln(QString("<td><code>%1</code></td>").arg(event));
    decrIndent();
    writeln("</tr>");

    QString onObj;
    if (createTrigger->eventTime == SqliteCreateTrigger::Time::INSTEAD_OF)
        onObj = tr("On view:");
    else
        onObj = tr("On table:");

    writeln("<tr>");
    incrIndent();
    writeln(QString("<td align=\"right\" class=\"rownum\">%1</td>").arg(onObj));
    writeln(QString("<td><code>%1</code></td>").arg(createTrigger->table));
    decrIndent();
    writeln("</tr>");

    writeln("<tr>");
    incrIndent();
    writeln(QString("<td align=\"right\" class=\"rownum\">%1</td>").arg(tr("Activate condition:")));
    writeln(QString("<td><code>%1</code></td>").arg(createTrigger->precondition ? escape(createTrigger->precondition->detokenize()) : ""));
    decrIndent();
    writeln("</tr>");

    writeln("<tr>");
    incrIndent();
    writeln(QString("<td colspan=\"2\" class=\"separator\">%1</td>").arg(tr("Code executed:")));
    decrIndent();
    writeln("</tr>");

    QStringList queryStrings;
    for (SqliteQuery* q : createTrigger->queries)
        queryStrings << escape(q->detokenize());

    writeln("<tr>");
    incrIndent();
    writeln("<td colspan=\"2\">");
    incrIndent();
    writeln(QString("<pre>%1</pre>").arg(queryStrings.join("<br/>")));
    decrIndent();
    writeln("</td>");
    decrIndent();
    writeln("</tr>");

    decrIndent();
    writeln("</table>");
    writeln("<br/><br/>");
    return true;
}

bool HtmlExport::exportView(const QString& database, const QString& name, const QStringList& columnNames, const QString& ddl, SqliteCreateViewPtr createView,
                            const QHash<ExportManager::ExportProviderFlag, QVariant> providedData)
{
    Q_UNUSED(database);
    Q_UNUSED(ddl);
    Q_UNUSED(createView);
    Q_UNUSED(providedData);

    int colCount = columnNames.size();
    int colSpan = printRownum ? colCount + 1 : colCount;

    writeln("<table>");
    incrIndent();

    QString viewLabel = tr("View: %1").arg(name);
    writeln("<tr class=\"title\">");
    incrIndent();
    writeln(QString("<td colspan=\"%1\" align=\"center\"><b>%2</b></td>").arg(colSpan).arg(viewLabel));
    decrIndent();
    writeln("</tr>");

    writeln("<tr>");
    incrIndent();
    writeln(QString("<td colspan=\"%1\">").arg(colSpan));
    incrIndent();
    writeln(QString("<pre>%1</pre>").arg(escape(createView->select->detokenize())));
    decrIndent();
    writeln("</td>");
    decrIndent();
    writeln("</tr>");

    if (printHeader)
    {
        writeln("<tr class=\"header\">");
        if (printRownum)
        {
            writeln("<th align=\"right\">");
            incrIndent();
            writeln("<b><i>#</i></b>");
            decrIndent();
            writeln("</th>");
        }

        for (const QString& col : columnNames)
        {
            writeln("<th>");
            incrIndent();
            writeln(QString("<b>%1</b>").arg(col));
            decrIndent();
            writeln("</th>");
        }
        writeln("</tr>");
    }

    decrIndent();

    columnTypes.clear();
    SchemaResolver resolver(db);
    QList<QPair<QString, QString>> selectColumnTypes = resolver.getColumnsAndDataTypesUsingPragma(name);
    for (QPair<QString, QString>& colAndType : selectColumnTypes)
        columnTypes << DataType(colAndType.second);

    currentDataRow = 0;
    return true;
}

bool HtmlExport::exportViewRow(SqlResultsRowPtr data)
{
    return exportDataRow(data);
}

bool HtmlExport::afterExportView()
{
    decrIndent();
    writeln("</table>");
    writeln("<br/><br/>");
    return true;
}

bool HtmlExport::afterExport()
{
    static const QString bodyEndTpl = QStringLiteral("</body>");
    static const QString docEnd = QStringLiteral("</html>");

    writeCustomFooter();

    QFile file(":/htmlexport/htmlexport.js");
    if (!file.open(QIODevice::ReadOnly))
    {
        qCritical() << "Could not open htmlexport.js resource while exporting to HTML:" << file.errorString();
        return false;
    }

    writeln("<i>" + tr("Document generated by Letos v%1 on %2").arg(LETOS->getVersionString(), QDateTime::currentDateTime().toString()) + "</i>");
    writeln("<script>");
    writeln(indent ? file.readAll() : compressCssOrJs(file.readAll()));
    writeln("</script>");
    decrIndent();
    writeln(bodyEndTpl);
    decrIndent();
    writeln(docEnd);
    return true;
}

bool HtmlExport::beginDoc(const QString& title)
{
    static const QString docStart = QStringLiteral(
                R"(<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">)"
                "\n<html>"
                );

    static const QString metaCodecTpl = QStringLiteral(R"(<meta http-equiv="Content-Type" content="text/html; charset=%1"/>)");
    static const QString titletpl = QStringLiteral(R"(<title>%1</title>)");
    static const QString styleStartTpl = QStringLiteral(R"(<style type="text/css">)");
    static const QString styleEndTpl = QStringLiteral(R"(</style>)");
    static const QString bodyStartTpl = QStringLiteral(R"(<body>)");

    setupConfig();

    QFile file(":/htmlexport/htmlexport.css");
    if (!file.open(QIODevice::ReadOnly))
    {
        qCritical() << "Could not open htmlexport.css resource while exporting to HTML:" << file.errorString();
        return false;
    }

    writeln(docStart);
    incrIndent();
    writeln(metaCodecTpl.arg(codecName));
    writeln(titletpl.arg(title));
    writeln(styleStartTpl);
    incrIndent();
    writeln(indent ? file.readAll() : compressCssOrJs(file.readAll()));
    decrIndent();
    writeln(styleEndTpl);
    writeln(bodyStartTpl);
    incrIndent();

    if (!writeCustomHeader())
        return false;

    return true;
}

void HtmlExport::setupConfig()
{
    codecName = codec->name();
    indentDepth = 0;
    newLineStr = "";
    indentStr = "";
    indent = (cfg.HtmlExport.Format.get() == "format");
    if (indent)
        newLineStr = "\n";

    printRownum = cfg.HtmlExport.PrintRowNum.get();
    printHeader = cfg.HtmlExport.PrintHeader.get();
    printDatatypes = printHeader && cfg.HtmlExport.PrintDataTypes.get();
    byteLengthLimit = cfg.HtmlExport.ByteLengthLimit.get();
}

void HtmlExport::incrIndent()
{
    if (indent)
    {
        indentDepth++;
        updateIndent();
    }
}

void HtmlExport::decrIndent()
{
    if (indent)
    {
        indentDepth--;
        updateIndent();
    }
}

void HtmlExport::updateIndent()
{
    indentStr = QString("    ").repeated(indentDepth);
}

void HtmlExport::writeln(const QString& str)
{
    QString newStr;
    if (str.contains("\n"))
    {
        QStringList lines = str.split("\n");
        QMutableStringListIterator it(lines);
        while (it.hasNext())
            it.next().prepend(indentStr);

        newStr = lines.join("\n") + newLineStr;
    }
    else
    {
        newStr = indentStr + str + newLineStr;
    }
    GenericExportPlugin::write(newStr);
}

QString HtmlExport::escape(const QString& str)
{
    if (cfg.HtmlExport.DontEscapeHtml.get())
        return str;

    return str.toHtmlEscaped();
}

QString HtmlExport::getCustomHeader(QString& err)
{
    return getCustomHeaderOrFooter(err,
                                   &cfg.HtmlExport.CustomHeaderMode,
                                   &cfg.HtmlExport.CustomHdrContent,
                                   &cfg.HtmlExport.CustomHdrFile);
}

QString HtmlExport::getCustomFooter(QString& err)
{
    return getCustomHeaderOrFooter(err,
                                   &cfg.HtmlExport.CustomFooterMode,
                                   &cfg.HtmlExport.CustomFtrContent,
                                   &cfg.HtmlExport.CustomFtrFile);
}

QString HtmlExport::getCustomHeaderOrFooter(QString& err, CfgEntry* modeEntry, CfgEntry* contentEntry, CfgEntry* fileEntry)
{
    switch (modeEntry->get().toInt())
    {
        case 0: // No header/footer
            break;
        case 1: // Inline contents
            return contentEntry->get().toString();
        case 2: // File with contents
        {
            QString err;
            QString contents = readFileContents(fileEntry->get().toString(), &err);
            if (!err.isEmpty())
                notifyError(err);

            return contents;
        }
        default:
            qWarning() << "Unhandled header/footer mode:" << modeEntry->get();
    }
    return QString();
}

bool HtmlExport::writeCustomHeader()
{
    QString err;
    QString hdrContent = getCustomHeader(err);
    if (!err.isEmpty())
    {
        notifyError(err);
        return false;
    }

    if (!hdrContent.isEmpty())
    {
        writeln(hdrContent);
        writeln("<br><br>");
    }

    return true;
}

void HtmlExport::writeCustomFooter()
{
    QString err;
    QString ftrContent = getCustomFooter(err);
    if (!err.isEmpty())
        notifyError(err);

    if (!ftrContent.isEmpty())
    {
        writeln(ftrContent);
        writeln("<br><br>");
    }
}

QString HtmlExport::compressCssOrJs(QString css)
{
    static const QRegularExpression spacesLeftRe(R"REGEXP(([^a-zA-Z0-9_\s]+)\s+(\S+))REGEXP");
    static const QRegularExpression spacesRightRe(R"REGEXP((\S+)\s+([^a-zA-Z0-9_\s]+))REGEXP");
    static const QRegularExpression spacesBetweenWordsRe(R"REGEXP((\S+)\s{2,}(\S+))REGEXP");
    while (css.indexOf(spacesLeftRe) > -1)
        css.replace(spacesLeftRe, R"(\1\2)");

    while (css.indexOf(spacesRightRe) > -1)
        css.replace(spacesRightRe, R"(\1\2)");

    while (css.indexOf(spacesBetweenWordsRe) > -1)
        css.replace(spacesBetweenWordsRe, R"(\1 \2)");

    return css.trimmed();
}

bool HtmlExport::init()
{
    SQLS_INIT_RESOURCE(htmlexport);
    return GenericExportPlugin::init();
}

void HtmlExport::deinit()
{
    SQLS_CLEANUP_RESOURCE(htmlexport);
}
