#include "snippetmgr.h"

#include <QDir>
#include <QJsonDocument>
#include <QDebug>
#include <QFileInfo>
#include <QSet>
#include <QTextCursor>
#include <QRegularExpression>
#include <QDateTime>

#include <core/configmgr.h>
#include <buffer/buffer.h>
#include <utils/fileutils.h>
#include <utils/utils.h>
#include <utils/pathutils.h>
#include <vtextedit/vtextedit.h>
#include <vtextedit/texteditutils.h>
#include <vtextedit/textutils.h>

using namespace vnotex;

const QChar SnippetMgr::c_snippetSymbolGuard = QLatin1Char('%');

const QString SnippetMgr::c_snippetSymbolRegExp = QString("%1([^%]+)%1").arg(c_snippetSymbolGuard);

SnippetMgr::SnippetMgr()
{
    loadSnippets();
}

QString SnippetMgr::getSnippetFolder() const
{
    return ConfigMgr::getInst().getUserSnippetFolder();
}

const QVector<QSharedPointer<Snippet>> &SnippetMgr::getSnippets() const
{
    return m_snippets;
}

void SnippetMgr::loadSnippets()
{
    Q_ASSERT(m_snippets.isEmpty());

    auto builtInSnippets = loadBuiltInSnippets();

    QSet<QString> names;
    for (const auto &snippet : builtInSnippets) {
        Q_ASSERT(!names.contains(snippet->getName()));
        names.insert(snippet->getName());
    }

    // Look for all the *.json files.
    QDir dir(getSnippetFolder());
    dir.setFilter(QDir::Files | QDir::NoSymLinks);
    const auto jsonFiles = dir.entryList(QStringList() << "*.json");
    for (const auto &jsonFile : jsonFiles) {
        auto snip = loadSnippet(dir.filePath(jsonFile));
        if (snip->isValid()) {
            if (names.contains(snip->getName())) {
                qWarning() << "skip loading snippet with name conflict" << snip->getName() << jsonFile;
                continue;
            }

            names.insert(snip->getName());

            addOneSnippet(snip);
        }
    }

    m_snippets.append(builtInSnippets);

    qDebug() << "loaded" << m_snippets.size() << "snippets";
}

QVector<int> SnippetMgr::getAvailableShortcuts(int p_exemption) const
{
    QVector<int> shortcuts;

    for (int i = 0; i < 100; ++i) {
        if (!m_shortcutToSnippet.contains(i) || i == p_exemption) {
            shortcuts.push_back(i);
        }
    }

    return shortcuts;
}

QSharedPointer<Snippet> SnippetMgr::find(const QString &p_name, Qt::CaseSensitivity p_cs) const
{
    if (p_cs == Qt::CaseInsensitive) {
        const auto lowerName = p_name.toLower();
        for (const auto &snip : m_snippets) {
            if (snip->getName().toLower() == lowerName) {
                return snip;
            }
        }
    } else {
        for (const auto &snip : m_snippets) {
            if (snip->getName() == p_name) {
                return snip;
            }
        }
    }

    return nullptr;
}

void SnippetMgr::addSnippet(const QSharedPointer<Snippet> &p_snippet)
{
    Q_ASSERT(!find(p_snippet->getName(), Qt::CaseInsensitive));
    saveSnippet(p_snippet);
    addOneSnippet(p_snippet);
}

void SnippetMgr::addOneSnippet(const QSharedPointer<Snippet> &p_snippet)
{
    m_snippets.push_back(p_snippet);
    addSnippetToShortcutMap(p_snippet);
}

QSharedPointer<Snippet> SnippetMgr::loadSnippet(const QString &p_snippetFile) const
{
    const auto obj = FileUtils::readJsonFile(p_snippetFile);
    auto snip = QSharedPointer<Snippet>::create(QFileInfo(p_snippetFile).completeBaseName());
    snip->fromJson(obj);
    return snip;
}

void SnippetMgr::saveSnippet(const QSharedPointer<Snippet> &p_snippet)
{
    Q_ASSERT(p_snippet->isValid()
             && !p_snippet->isReadOnly()
             && p_snippet->getType() != Snippet::Type::Dynamic);
    FileUtils::writeFile(getSnippetFile(p_snippet), p_snippet->toJson());
}

void SnippetMgr::removeSnippet(const QString &p_name)
{
    auto snippet = find(p_name);
    if (!snippet || snippet->isReadOnly()) {
        return;
    }

    removeSnippetFromShortcutMap(snippet);
    m_snippets.removeAll(snippet);
    FileUtils::removeFile(getSnippetFile(snippet));
}

QString SnippetMgr::getSnippetFile(const QSharedPointer<Snippet> &p_snippet) const
{
    return PathUtils::concatenateFilePath(getSnippetFolder(), p_snippet->getName() + QStringLiteral(".json"));
}

void SnippetMgr::updateSnippet(const QString &p_name, const QSharedPointer<Snippet> &p_snippet)
{
    auto snippet = find(p_name);
    Q_ASSERT(snippet);

    // If renamed, remove the old file first.
    if (p_name != p_snippet->getName()) {
        FileUtils::removeFile(getSnippetFile(snippet));
    }

    removeSnippetFromShortcutMap(snippet);

    *snippet = *p_snippet;
    saveSnippet(snippet);

    addSnippetToShortcutMap(snippet);
}

void SnippetMgr::removeSnippetFromShortcutMap(const QSharedPointer<Snippet> &p_snippet)
{
    if (p_snippet->getShortcut() != Snippet::InvalidShortcut) {
        auto iter = m_shortcutToSnippet.find(p_snippet->getShortcut());
        Q_ASSERT(iter != m_shortcutToSnippet.end());
        if (iter.value() == p_snippet) {
            // There may exist conflict in shortcut.
            m_shortcutToSnippet.erase(iter);
        }
    }
}

void SnippetMgr::addSnippetToShortcutMap(const QSharedPointer<Snippet> &p_snippet)
{
    if (p_snippet->getShortcut() != Snippet::InvalidShortcut) {
        m_shortcutToSnippet.insert(p_snippet->getShortcut(), p_snippet);
    }
}

void SnippetMgr::applySnippet(const QString &p_name,
                              vte::VTextEdit *p_textEdit,
                              const OverrideMap &p_overrides) const
{
    auto snippet = find(p_name);
    if (!snippet) {
        return;
    }
    Q_ASSERT(snippet->isValid());

    auto cursor = p_textEdit->textCursor();
    cursor.beginEditBlock();

    // Get selected text.
    const auto selectedText = p_textEdit->selectedText();
    p_textEdit->removeSelectedText();

    QString appliedText;
    int cursorOffset = 0;

    auto it = p_overrides.find(p_name);
    if (it != p_overrides.end()) {
        appliedText = it.value();
        cursorOffset = appliedText.size();
    } else {
        // Fetch indentation of first line.
        QString indentationSpaces;
        if (snippet->isIndentAsFirstLineEnabled()) {
            indentationSpaces = vte::TextEditUtils::fetchIndentationSpaces(cursor.block());
        }

        appliedText = snippet->apply(selectedText, indentationSpaces, cursorOffset);
        appliedText = applySnippetBySymbol(appliedText, selectedText, cursorOffset, p_overrides);
    }

    const int beforePos = cursor.position();
    cursor.insertText(appliedText);
    cursor.setPosition(beforePos + cursorOffset);

    cursor.endEditBlock();
    p_textEdit->setTextCursor(cursor);
}

QString SnippetMgr::applySnippetBySymbol(const QString &p_content,
                                         const OverrideMap &p_overrides) const
{
    int offset = 0;
    return applySnippetBySymbol(p_content, QString(), offset, p_overrides);
}

QString SnippetMgr::applySnippetBySymbol(const QString &p_content,
                                         const QString &p_selectedText,
                                         int &p_cursorOffset,
                                         const OverrideMap &p_overrides) const
{
    QString content(p_content);

    int maxTimesAtSamePos = 100;

    QRegularExpression regExp(c_snippetSymbolRegExp);
    int pos = 0;
    while (pos < content.size()) {
        QRegularExpressionMatch match;
        int idx = content.indexOf(regExp, pos, &match);
        if (idx == -1) {
            break;
        }

        const auto snippetName = match.captured(1);
        auto snippet = find(snippetName);
        if (!snippet) {
            // Skip it.
            pos = idx + match.capturedLength(0);
            continue;
        }

        QString afterText;

        auto it = p_overrides.find(snippetName);
        if (it != p_overrides.end()) {
            afterText = it.value();
        } else {
            const auto indentationSpaces = vte::TextUtils::fetchIndentationSpacesInMultiLines(content, idx);

            // Ignore the cursor mark.
            int ignoredCursorOffset = 0;
            afterText = snippet->apply(p_selectedText, indentationSpaces, ignoredCursorOffset);
        }

        content.replace(idx, match.capturedLength(0), afterText);

        // Maintain the cursor offset.
        if (p_cursorOffset > idx) {
            if (p_cursorOffset < idx + match.capturedLength(0)) {
                p_cursorOffset = idx;
            } else {
                p_cursorOffset += (afterText.size() - match.capturedLength(0));
            }
        }

        // @afterText may still contains snippet symbol.
        if (pos == idx) {
            if (--maxTimesAtSamePos == 0) {
                break;
            }
        } else {
            maxTimesAtSamePos = 100;
        }
        pos = idx;
    }

    return content;
}

// Used as the function template for some date/time related dynamic snippets.
static QString formattedDateTime(const QString &p_format)
{
    return QDateTime::currentDateTime().toString(p_format);
}

QVector<QSharedPointer<Snippet>> SnippetMgr::loadBuiltInSnippets() const
{
    QVector<QSharedPointer<Snippet>> snippets;

    addDynamicSnippet(snippets,
                      "d",
                      tr("the day as number without a leading zero (`1` to `31`)"),
                      std::bind(formattedDateTime, "d"));
    addDynamicSnippet(snippets,
                      "dd",
                      tr("the day as number with a leading zero (`01` to `31`)"),
                      std::bind(formattedDateTime, "dd"));
    addDynamicSnippet(snippets,
                      "ddd",
                      tr("the abbreviated localized day name (e.g. `Mon` to `Sun`)"),
                      std::bind(formattedDateTime, "ddd"));
    addDynamicSnippet(snippets,
                      "dddd",
                      tr("the long localized day name (e.g. `Monday` to `Sunday`)"),
                      std::bind(formattedDateTime, "dddd"));
    addDynamicSnippet(snippets,
                      "M",
                      tr("the month as number without a leading zero (`1` to `12`)"),
                      std::bind(formattedDateTime, "M"));
    addDynamicSnippet(snippets,
                      "MM",
                      tr("the month as number with a leading zero (`01` to `12`)"),
                      std::bind(formattedDateTime, "MM"));
    addDynamicSnippet(snippets,
                      "MMM",
                      tr("the abbreviated localized month name (e.g. `Jan` to `Dec`)"),
                      std::bind(formattedDateTime, "MMM"));
    addDynamicSnippet(snippets,
                      "MMMM",
                      tr("the long localized month name (e.g. `January` to `December`)"),
                      std::bind(formattedDateTime, "MMMM"));
    addDynamicSnippet(snippets,
                      "yy",
                      tr("the year as two digit numbers (`00` to `99`)"),
                      std::bind(formattedDateTime, "yy"));
    addDynamicSnippet(snippets,
                      "yyyy",
                      tr("the year as four digit numbers"),
                      std::bind(formattedDateTime, "yyyy"));
    addDynamicSnippet(snippets,
                      "w",
                      tr("the week number without a leading zero (`1` to `53`)"),
                      [](const QString &) {
                          return QString::number(QDate::currentDate().weekNumber());
                      });
    addDynamicSnippet(snippets,
                      "ww",
                      tr("the week number with a leading zero (`01` to `53`)"),
                      [](const QString &) {
                          return Utils::intToString(QDate::currentDate().weekNumber(), 2);
                      });
    addDynamicSnippet(snippets,
                      "H",
                      tr("the hour without a leading zero (`0` to `23` even with AM/PM display)"),
                      std::bind(formattedDateTime, "H"));
    addDynamicSnippet(snippets,
                      "HH",
                      tr("the hour with a leading zero (`00` to `23` even with AM/PM display)"),
                      std::bind(formattedDateTime, "HH"));
    addDynamicSnippet(snippets,
                      "m",
                      tr("the minute without a leading zero (`0` to `59`)"),
                      std::bind(formattedDateTime, "m"));
    addDynamicSnippet(snippets,
                      "mm",
                      tr("the minute with a leading zero (`00` to `59`)"),
                      std::bind(formattedDateTime, "mm"));
    addDynamicSnippet(snippets,
                      "s",
                      tr("the second without a leading zero (`0` to `59`)"),
                      std::bind(formattedDateTime, "s"));
    addDynamicSnippet(snippets,
                      "ss",
                      tr("the second with a leading zero (`00` to `59`)"),
                      std::bind(formattedDateTime, "ss"));
    addDynamicSnippet(snippets,
                      "date",
                      tr("date (`2021-02-24`)"),
                      std::bind(formattedDateTime, "yyyy-MM-dd"));
    addDynamicSnippet(snippets,
                      "da",
                      tr("the abbreviated date (`20210224`)"),
                      std::bind(formattedDateTime, "yyyyMMdd"));
    addDynamicSnippet(snippets,
                      "time",
                      tr("time (`16:51:02`)"),
                      std::bind(formattedDateTime, "hh:mm:ss"));
    addDynamicSnippet(snippets,
                      "datetime",
                      tr("date and time (`2021-02-24_16:51:02`)"),
                      std::bind(formattedDateTime, "yyyy-MM-dd_hh:mm:ss"));

    // These snippets need override to fill the real value.
    // Check generateOverrides().
    addDynamicSnippet(snippets,
                      QStringLiteral("note"),
                      tr("name of current note"),
                      [](const QString &) {
                          return tr("[Value Not Available]");
                      });
    addDynamicSnippet(snippets,
                      QStringLiteral("no"),
                      tr("complete base name of current note"),
                      [](const QString &) {
                          return tr("[Value Not Available]");
                      });
    return snippets;
}

void SnippetMgr::addDynamicSnippet(QVector<QSharedPointer<Snippet>> &p_snippets,
                                   const QString &p_name,
                                   const QString &p_description,
                                   const DynamicSnippet::Callback &p_callback)
{
    auto snippet = QSharedPointer<DynamicSnippet>::create(p_name, p_description, p_callback);
    p_snippets.push_back(snippet);
}

SnippetMgr::OverrideMap SnippetMgr::generateOverrides(const Buffer *p_buffer)
{
    OverrideMap overrides;
    if (p_buffer) {
        overrides.insert(QStringLiteral("note"), p_buffer->getName());
        overrides.insert(QStringLiteral("no"), QFileInfo(p_buffer->getName()).completeBaseName());
    }
    return overrides;
}

SnippetMgr::OverrideMap SnippetMgr::generateOverrides(const QString &p_fileName)
{
    OverrideMap overrides;
    overrides.insert(QStringLiteral("note"), p_fileName);
    overrides.insert(QStringLiteral("no"), QFileInfo(p_fileName).completeBaseName());
    return overrides;
}

QString SnippetMgr::generateSnippetSymbol(const QString &p_snippetName)
{
    return c_snippetSymbolGuard + p_snippetName + c_snippetSymbolGuard;
}
