/*
  Copyright (c) 2012-2017 Montel Laurent <montel@kde.org>
  code based on calligra autocorrection.

  This program is free software; you can redistribute it and/or modify it
  under the terms of the GNU General Public License, version 2, as
  published by the Free Software Foundation.

  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, write to the Free Software Foundation, Inc.,
  51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include "autocorrection.h"

#include "settings/pimcommonsettings.h"
#include "import/importkmailautocorrection.h"
#include <KColorScheme>
#include <QLocale>
#include "pimcommon_debug.h"
#include <QTextBlock>
#include <QTextDocument>
#include <QFile>
#include <QDir>
#include <QStandardPaths>
#include <QFileInfo>
#include <QRegularExpression>
#include <QXmlStreamWriter>

using namespace PimCommon;

AutoCorrection::AutoCorrection()
    : mSingleSpaces(true)
    , mUppercaseFirstCharOfSentence(false)
    , mFixTwoUppercaseChars(false)
    , mAutoFractions(true)
    , mCapitalizeWeekDays(false)
    , mReplaceDoubleQuotes(false)
    , mReplaceSingleQuotes(false)
    , mEnabled(false)
    , mSuperScriptAppendix(false)
    , mAddNonBreakingSpace(false)
    , mMaxFindStringLenght(0)
    , mMinFindStringLenght(0)
{
    mNonBreakingSpace = QChar(QChar::Nbsp);
    // default double quote open 0x201c
    // default double quote close 0x201d
    // default single quote open 0x2018
    // default single quote close 0x2019
    mTypographicSingleQuotes.begin = QChar(0x2018);
    mTypographicSingleQuotes.end = QChar(0x2019);
    mTypographicDoubleQuotes.begin = QChar(0x201c);
    mTypographicDoubleQuotes.end = QChar(0x201d);

    readConfig();

    auto locale = QLocale::system();
    mCacheNameOfDays.reserve(7);
    for (int i = 1; i <= 7; ++i) {
        mCacheNameOfDays.append(locale.dayName(i).toLower());
    }
}

AutoCorrection::~AutoCorrection()
{
}

void AutoCorrection::selectStringOnMaximumSearchString(QTextCursor &cursor, int cursorPosition)
{
    cursor.setPosition(cursorPosition);

    QTextBlock block = cursor.block();
    int pos = qMax(block.position(), cursorPosition - mMaxFindStringLenght);

    //TODO verify that pos == block.position() => it's a full line => not a piece of word
    //TODO if not => check if pos -1 is a space => not a piece of word
    //TODO otherwise move cursor until we detect a space
    //TODO otherwise we must not autoconvert it.
    if (pos != block.position()) {
        const QString text = block.text();
        const int currentPos = (pos - block.position());
        if (!text.at(currentPos - 1).isSpace()) {
            //qDebug() << " current Text " << text << " currentPos "<< currentPos << " pos " << pos;
            //qDebug() << "selected text " << text.right(text.length() - currentPos);
            //qDebug() << "  text after " << text.at(currentPos - 1);
            bool foundWord = false;
            const int textLength(text.length());
            for (int i = currentPos; i < textLength; ++i) {
                if (text.at(i).isSpace()) {
                    pos = qMin(cursorPosition, pos + 1 + block.position());
                    foundWord = true;
                    break;
                }
            }
            if (!foundWord) {
                pos = cursorPosition;
            }
        }
    }
    cursor.setPosition(pos);
    cursor.setPosition(cursorPosition, QTextCursor::KeepAnchor);
}

void AutoCorrection::selectPreviousWord(QTextCursor &cursor, int cursorPosition)
{
    cursor.setPosition(cursorPosition);
    QTextBlock block = cursor.block();
    cursor.setPosition(block.position());
    cursorPosition -= block.position();
    QString string = block.text();
    int pos = 0;
    bool space = false;
    QString::Iterator iter = string.begin();
    while (iter != string.end()) {
        if (iter->isSpace()) {
            if (space) {
                // double spaces belong to the previous word
            } else if (pos < cursorPosition) {
                cursor.setPosition(pos + block.position() + 1);    // +1 because we don't want to set it on the space itself
            } else {
                space = true;
            }
        } else if (space) {
            break;
        }
        ++pos;
        ++iter;
    }
    cursor.setPosition(pos + block.position(), QTextCursor::KeepAnchor);
}

bool AutoCorrection::autocorrect(bool htmlMode, QTextDocument &document, int &position)
{
    if (mEnabled) {
        mCursor = QTextCursor(&document);
        mCursor.setPosition(position);

        //If we already have a space not necessary to look at other autocorrect feature.
        if (!singleSpaces()) {
            return false;
        }

        int oldPosition = position;
        selectPreviousWord(mCursor, position);
        mWord = mCursor.selectedText();
        if (mWord.isEmpty()) {
            return true;
        }
        mCursor.beginEditBlock();
        bool done = false;
        if (htmlMode) {
            done = autoFormatURLs();
            if (!done) {
                done = autoBoldUnderline();
                //We replace */- by format => remove cursor position by 2
                if (done) {
                    oldPosition -= 2;
                }
            }
            if (!done) {
                superscriptAppendix();
            }
        }
        if (!done) {
            done = autoFractions();
            //We replace three characters with 1
            if (done) {
                oldPosition -= 2;
            }
        }
        if (!done) {
            uppercaseFirstCharOfSentence();
            fixTwoUppercaseChars();
            capitalizeWeekDays();
            replaceTypographicQuotes();
            if (mWord.length() <= 2) {
                addNonBreakingSpace();
            }
        }

        if (mCursor.selectedText() != mWord) {
            mCursor.insertText(mWord);
        }
        position = oldPosition;

        if (!done) {
            selectStringOnMaximumSearchString(mCursor, position);
            mWord = mCursor.selectedText();
            if (!mWord.isEmpty()) {
                const int newPos = advancedAutocorrect();
                if (newPos != -1) {
                    if (mCursor.selectedText() != mWord) {
                        mCursor.insertText(mWord);
                    }
                    position = newPos;
                }
            }
        }
        mCursor.endEditBlock();
    }
    return true;
}

void AutoCorrection::readConfig()
{
    mAutoBoldUnderline = PimCommon::PimCommonSettings::self()->autoBoldUnderline();
    mAutoFormatUrl = PimCommon::PimCommonSettings::self()->autoFormatUrl();
    mUppercaseFirstCharOfSentence = PimCommon::PimCommonSettings::self()->uppercaseFirstCharOfSentence();
    mFixTwoUppercaseChars = PimCommon::PimCommonSettings::self()->fixTwoUppercaseChars();
    mSingleSpaces = PimCommon::PimCommonSettings::self()->singleSpaces();
    mAutoFractions = PimCommon::PimCommonSettings::self()->autoFractions();
    mCapitalizeWeekDays = PimCommon::PimCommonSettings::self()->capitalizeWeekDays();
    mAdvancedAutocorrect = PimCommon::PimCommonSettings::self()->advancedAutocorrect();
    mReplaceDoubleQuotes = PimCommon::PimCommonSettings::self()->replaceDoubleQuotes();
    mReplaceSingleQuotes = PimCommon::PimCommonSettings::self()->replaceSingleQuotes();
    mEnabled = PimCommon::PimCommonSettings::self()->enabled();
    mSuperScriptAppendix = PimCommon::PimCommonSettings::self()->superScript();
    mAddNonBreakingSpace = PimCommon::PimCommonSettings::self()->addNonBreakingSpaceInFrench();
    readAutoCorrectionXmlFile();
}

void AutoCorrection::writeConfig()
{
    PimCommon::PimCommonSettings::self()->setAutoBoldUnderline(mAutoBoldUnderline);
    PimCommon::PimCommonSettings::self()->setAutoFormatUrl(mAutoFormatUrl);
    PimCommon::PimCommonSettings::self()->setUppercaseFirstCharOfSentence(mUppercaseFirstCharOfSentence);
    PimCommon::PimCommonSettings::self()->setFixTwoUppercaseChars(mFixTwoUppercaseChars);
    PimCommon::PimCommonSettings::self()->setSingleSpaces(mSingleSpaces);
    PimCommon::PimCommonSettings::self()->setAutoFractions(mAutoFractions);
    PimCommon::PimCommonSettings::self()->setCapitalizeWeekDays(mCapitalizeWeekDays);
    PimCommon::PimCommonSettings::self()->setAdvancedAutocorrect(mAdvancedAutocorrect);
    PimCommon::PimCommonSettings::self()->setReplaceDoubleQuotes(mReplaceDoubleQuotes);
    PimCommon::PimCommonSettings::self()->setReplaceSingleQuotes(mReplaceSingleQuotes);
    PimCommon::PimCommonSettings::self()->setEnabled(mEnabled);
    PimCommon::PimCommonSettings::self()->setSuperScript(mSuperScriptAppendix);
    PimCommon::PimCommonSettings::self()->setAddNonBreakingSpaceInFrench(mAddNonBreakingSpace);
    PimCommon::PimCommonSettings::self()->requestSync();
    writeAutoCorrectionXmlFile();
}

bool AutoCorrection::addAutoCorrect(const QString &currentWord, const QString &replaceWord)
{
    if (!mAutocorrectEntries.contains(currentWord)) {
        mAutocorrectEntries.insert(currentWord, replaceWord);
        writeAutoCorrectionXmlFile();
        return true;
    } else {
        return false;
    }
}

void AutoCorrection::setUpperCaseExceptions(const QSet<QString> &exceptions)
{
    mUpperCaseExceptions = exceptions;
}

void AutoCorrection::setTwoUpperLetterExceptions(const QSet<QString> &exceptions)
{
    mTwoUpperLetterExceptions = exceptions;
}

void AutoCorrection::setAutocorrectEntries(const QHash<QString, QString> &entries)
{
    mMaxFindStringLenght = 0;
    mMinFindStringLenght = 0;
    QHashIterator<QString, QString> i(entries);
    while (i.hasNext()) {
        i.next();
        const int findStringLenght(i.key().length());
        mMaxFindStringLenght = qMax(mMaxFindStringLenght, findStringLenght);
        mMinFindStringLenght = qMin(mMinFindStringLenght, findStringLenght);
    }
    mAutocorrectEntries = entries;
}

void AutoCorrection::setAutoFormatUrl(bool b)
{
    mAutoFormatUrl = b;
}

void AutoCorrection::setAutoBoldUnderline(bool b)
{
    mAutoBoldUnderline = b;
}

void AutoCorrection::setSuperScript(bool b)
{
    mSuperScriptAppendix = b;
}

void AutoCorrection::setAddNonBreakingSpace(bool b)
{
    mAddNonBreakingSpace = b;
}

bool AutoCorrection::isEnabledAutoCorrection() const
{
    return mEnabled;
}

bool AutoCorrection::isUppercaseFirstCharOfSentence() const
{
    return mUppercaseFirstCharOfSentence;
}

bool AutoCorrection::isFixTwoUppercaseChars() const
{
    return mFixTwoUppercaseChars;
}

bool AutoCorrection::isSingleSpaces() const
{
    return mSingleSpaces;
}

bool AutoCorrection::isAutoFractions() const
{
    return mAutoFractions;
}

bool AutoCorrection::isCapitalizeWeekDays() const
{
    return mCapitalizeWeekDays;
}

bool AutoCorrection::isReplaceDoubleQuotes() const
{
    return mReplaceDoubleQuotes;
}

bool AutoCorrection::isReplaceSingleQuotes() const
{
    return mReplaceSingleQuotes;
}

bool AutoCorrection::isAdvancedAutocorrect() const
{
    return mAdvancedAutocorrect;
}

bool AutoCorrection::isAutoFormatUrl() const
{
    return mAutoFormatUrl;
}

bool AutoCorrection::isAutoBoldUnderline() const
{
    return mAutoBoldUnderline;
}

bool AutoCorrection::isSuperScript() const
{
    return mSuperScriptAppendix;
}

bool AutoCorrection::isAddNonBreakingSpace() const
{
    return mAddNonBreakingSpace;
}

AutoCorrection::TypographicQuotes AutoCorrection::typographicDefaultSingleQuotes() const
{
    AutoCorrection::TypographicQuotes quote;
    quote.begin = QChar(0x2018);
    quote.end = QChar(0x2019);
    return quote;
}

AutoCorrection::TypographicQuotes AutoCorrection::typographicDefaultDoubleQuotes() const
{
    AutoCorrection::TypographicQuotes quote;
    quote.begin = QChar(0x201c);
    quote.end = QChar(0x201d);
    return quote;
}

QSet<QString> AutoCorrection::upperCaseExceptions() const
{
    return mUpperCaseExceptions;
}

QSet<QString> AutoCorrection::twoUpperLetterExceptions() const
{
    return mTwoUpperLetterExceptions;
}

QHash<QString, QString> AutoCorrection::autocorrectEntries() const
{
    return mAutocorrectEntries;
}

void AutoCorrection::superscriptAppendix()
{
    if (!mSuperScriptAppendix) {
        return;
    }

    const QString trimmed = mWord.trimmed();
    int startPos = -1;
    int endPos = -1;
    const int trimmedLenght(trimmed.length());

    QHash<QString, QString>::const_iterator i = mSuperScriptEntries.constBegin();
    while (i != mSuperScriptEntries.constEnd()) {
        if (i.key() == trimmed) {
            startPos = mCursor.selectionStart() + 1;
            endPos = startPos - 1 + trimmedLenght;
            break;
        } else if (i.key() == QLatin1String("othernb")) {
            const int pos = trimmed.indexOf(i.value());
            if (pos > 0) {
                QString number = trimmed.left(pos);
                QString::ConstIterator constIter = number.constBegin();
                bool found = true;
                // don't apply superscript to 1th, 2th and 3th
                const int numberLength(number.length());
                if (numberLength == 1
                    && (*constIter == QLatin1Char('1')
                        || *constIter == QLatin1Char('2')
                        || *constIter == QLatin1Char('3'))) {
                    found = false;
                }
                if (found) {
                    while (constIter != number.constEnd()) {
                        if (!constIter->isNumber()) {
                            found = false;
                            break;
                        }
                        ++constIter;
                    }
                }
                if (found && numberLength + i.value().length() == trimmedLenght) {
                    startPos = mCursor.selectionStart() + pos;
                    endPos = startPos - pos + trimmedLenght;
                    break;
                }
            }
        }
        ++i;
    }

    if (startPos != -1 && endPos != -1) {
        QTextCursor cursor(mCursor);
        cursor.setPosition(startPos);
        cursor.setPosition(endPos, QTextCursor::KeepAnchor);

        QTextCharFormat format;
        format.setVerticalAlignment(QTextCharFormat::AlignSuperScript);
        cursor.mergeCharFormat(format);
    }
}

void AutoCorrection::addNonBreakingSpace()
{
    if (mAddNonBreakingSpace && isFrenchLanguage()) {
        const QTextBlock block = mCursor.block();
        const QString text = block.text();
        const QChar lastChar = text.at(mCursor.position() - 1 - block.position());

        if (lastChar == QLatin1Char(':')
            || lastChar == QLatin1Char(';')
            || lastChar == QLatin1Char('!')
            || lastChar == QLatin1Char('?')
            || lastChar == QLatin1Char('%')) {
            const int pos = mCursor.position() - 2 - block.position();
            if (pos >= 0) {
                const QChar previousChar = text.at(pos);
                if (previousChar.isSpace()) {
                    QTextCursor cursor(mCursor);
                    cursor.setPosition(pos);
                    cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
                    cursor.deleteChar();
                    mCursor.insertText(mNonBreakingSpace);
                }
            }
        } else {
            //°C (degres)
            const QChar previousChar = text.at(mCursor.position() - 2 - block.position());
            if (lastChar == QLatin1Char('C') && previousChar == QChar(0x000B0)) {
                const int pos = mCursor.position() - 3 - block.position();
                if (pos >= 0) {
                    const QChar previousChar = text.at(pos);
                    if (previousChar.isSpace()) {
                        QTextCursor cursor(mCursor);
                        cursor.setPosition(pos);
                        cursor.setPosition(pos + 1, QTextCursor::KeepAnchor);
                        cursor.deleteChar();
                        mCursor.insertText(mNonBreakingSpace);
                    }
                }
            }
        }
    }
}

bool AutoCorrection::autoBoldUnderline()
{
    if (!mAutoBoldUnderline) {
        return false;
    }
    const QString trimmed = mWord.trimmed();

    if (trimmed.length() < 3) {
        return false;
    }

    const QChar trimmedFirstChar(trimmed.at(0));
    const QChar trimmedLastChar(trimmed.at(trimmed.length() - 1));
    const bool underline = (trimmedFirstChar == QLatin1Char('_') && trimmedLastChar == QLatin1Char('_'));
    const bool bold = (trimmedFirstChar == QLatin1Char('*') && trimmedLastChar == QLatin1Char('*'));
    const bool strikeOut = (trimmedFirstChar == QLatin1Char('-') && trimmedLastChar == QLatin1Char('-'));
    if (underline || bold || strikeOut) {
        const int startPos = mCursor.selectionStart();
        const QString replacement = trimmed.mid(1, trimmed.length() - 2);
        bool foundLetterNumber = false;

        QString::ConstIterator constIter = replacement.constBegin();
        while (constIter != replacement.constEnd()) {
            if (constIter->isLetterOrNumber()) {
                foundLetterNumber = true;
                break;
            }
            ++constIter;
        }

        // if no letter/number found, don't apply autocorrection like in OOo 2.x
        if (!foundLetterNumber) {
            return false;
        }
        mCursor.setPosition(startPos);
        mCursor.setPosition(startPos + trimmed.length(), QTextCursor::KeepAnchor);
        mCursor.insertText(replacement);
        mCursor.setPosition(startPos);
        mCursor.setPosition(startPos + replacement.length(), QTextCursor::KeepAnchor);

        QTextCharFormat format;
        format.setFontUnderline(underline ? true : mCursor.charFormat().fontUnderline());
        format.setFontWeight(bold ? QFont::Bold : mCursor.charFormat().fontWeight());
        format.setFontStrikeOut(strikeOut ? true : mCursor.charFormat().fontStrikeOut());
        mCursor.mergeCharFormat(format);

        // to avoid the selection being replaced by mWord
        mWord = mCursor.selectedText();
        return true;
    } else {
        return false;
    }

    return true;
}

QColor AutoCorrection::linkColor()
{
    if (mLinkColor.isValid()) {
        return mLinkColor;
    }
    mLinkColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color();
    return mLinkColor;
}

bool AutoCorrection::autoFormatURLs()
{
    if (!mAutoFormatUrl) {
        return false;
    }

    const QString link = autoDetectURL(mWord);
    if (link.isNull()) {
        return false;
    }

    const QString trimmed = mWord.trimmed();
    const int startPos = mCursor.selectionStart();
    mCursor.setPosition(startPos);
    mCursor.setPosition(startPos + trimmed.length(), QTextCursor::KeepAnchor);

    QTextCharFormat format;
    format.setAnchorHref(link);
    format.setFontItalic(true);
    format.setAnchor(true);
    format.setUnderlineStyle(QTextCharFormat::SingleUnderline);
    format.setUnderlineColor(linkColor());
    format.setForeground(linkColor());
    mCursor.mergeCharFormat(format);

    mWord = mCursor.selectedText();
    return true;
}

QString AutoCorrection::autoDetectURL(const QString &_word) const
{
    QString word = _word;

    /* this method is ported from lib/kotext/KoAutoFormat.cpp KoAutoFormat::doAutoDetectUrl
     * from Calligra 1.x branch */
    // qCDebug(PIMCOMMON_LOG) <<"link:" << word;

    bool secure = false;
    int link_type = 0;
    int pos = word.indexOf(QStringLiteral("http://"));
    int tmp_pos = word.indexOf(QStringLiteral("https://"));

    if (tmp_pos != -1 && pos == -1) {
        secure = true;
    }

    if (tmp_pos < pos && tmp_pos != -1) {
        pos = tmp_pos;
    }

    tmp_pos = word.indexOf(QStringLiteral("mailto:/"));
    if ((tmp_pos < pos || pos == -1) && tmp_pos != -1) {
        pos = tmp_pos;
    }
    tmp_pos = word.indexOf(QStringLiteral("ftp://"));
    const int secureftp = word.indexOf(QStringLiteral("ftps://"));
    if (secureftp != -1 && tmp_pos == -1) {
        secure = true;
    }

    if ((tmp_pos < pos || pos == -1) && tmp_pos != -1) {
        pos = tmp_pos;
    }
    tmp_pos = word.indexOf(QStringLiteral("ftp."));

    if ((tmp_pos < pos || pos == -1) && tmp_pos != -1) {
        pos = tmp_pos;
        link_type = 3;
    }
    tmp_pos = word.indexOf(QStringLiteral("file:/"));
    if ((tmp_pos < pos || pos == -1) && tmp_pos != -1) {
        pos = tmp_pos;
    }
    tmp_pos = word.indexOf(QStringLiteral("news:"));
    if ((tmp_pos < pos || pos == -1) && tmp_pos != -1) {
        pos = tmp_pos;
    }
    tmp_pos = word.indexOf(QStringLiteral("www."));

    if ((tmp_pos < pos || pos == -1) && tmp_pos != -1 && word.indexOf(QLatin1Char('.'), tmp_pos + 4) != -1) {
        pos = tmp_pos;
        link_type = 2;
    }
    tmp_pos = word.indexOf(QLatin1Char('@'));
    if (pos == -1 && tmp_pos != -1) {
        pos = tmp_pos - 1;
        QChar c;

        while (pos >= 0) {
            c = word.at(pos);
            if (c.isPunct() && c != QLatin1Char('.') && c != QLatin1Char('_')) {
                break;
            } else {
                --pos;
            }
        }
        if (pos == tmp_pos - 1) { // not a valid address
            pos = -1;
        } else {
            ++pos;
        }
        link_type = 1;
    }

    if (pos != -1) {
        // A URL inside e.g. quotes (like "http://www.calligra.org" with the quotes) shouldn't include the quote in the URL.
        while (!word.at(word.length() - 1).isLetter() && !word.at(word.length() - 1).isDigit() && word.at(word.length() - 1) != QLatin1Char('/')) {
            word.truncate(word.length() - 1);
        }
        word.remove(0, pos);
        QString newWord = word;

        switch (link_type) {
        case 1:
            newWord = QLatin1String("mailto:") + word;
            break;
        case 2:
            newWord = (secure ? QStringLiteral("https://") : QStringLiteral("http://")) + word;
            break;
        case 3:
            newWord = (secure ? QStringLiteral("ftps://") : QStringLiteral("ftp://")) + word;
            break;
        }
        //qCDebug(PIMCOMMON_LOG) <<"newWord:" << newWord;
        return newWord;
    }

    return QString();
}

void AutoCorrection::fixTwoUppercaseChars()
{
    if (!mFixTwoUppercaseChars) {
        return;
    }
    if (mWord.length() <= 2) {
        return;
    }

    if (mTwoUpperLetterExceptions.contains(mWord.trimmed())) {
        return;
    }

    const QChar firstChar = mWord.at(0);
    const QChar secondChar = mWord.at(1);

    if (secondChar.isUpper() && firstChar.isUpper()) {
        const QChar thirdChar = mWord.at(2);

        if (thirdChar.isLower()) {
            mWord.replace(1, 1, secondChar.toLower());
        }
    }
}

//Return true if we can add space
bool AutoCorrection::singleSpaces()
{
    if (!mSingleSpaces) {
        return true;
    }
    if (!mCursor.atBlockStart()) {
        // then when the prev char is also a space, don't insert one.
        const QTextBlock block = mCursor.block();
        const QString text = block.text();
        if (text.at(mCursor.position() - 1 - block.position()) == QLatin1Char(' ')) {
            return false;
        }
    }
    return true;
}

void AutoCorrection::capitalizeWeekDays()
{
    if (!mCapitalizeWeekDays) {
        return;
    }

    const QString trimmed = mWord.trimmed();
    for (const QString &name : qAsConst(mCacheNameOfDays)) {
        if (trimmed == name) {
            const int pos = mWord.indexOf(name);
            mWord.replace(pos, 1, name.at(0).toUpper());
            return;
        }
    }
}

bool AutoCorrection::excludeToUppercase(const QString &word) const
{
    if (word.startsWith(QStringLiteral("http://"))
        || word.startsWith(QStringLiteral("www."))
        || word.startsWith(QStringLiteral("mailto:"))
        || word.startsWith(QStringLiteral("ftp://"))
        || word.startsWith(QStringLiteral("https://"))
        || word.startsWith(QStringLiteral("ftps://"))) {
        return true;
    }
    return false;
}

void AutoCorrection::setNonBreakingSpace(const QChar &nonBreakingSpace)
{
    mNonBreakingSpace = nonBreakingSpace;
}

void AutoCorrection::uppercaseFirstCharOfSentence()
{
    if (!mUppercaseFirstCharOfSentence) {
        return;
    }

    int startPos = mCursor.selectionStart();
    QTextBlock block = mCursor.block();

    mCursor.setPosition(block.position());
    mCursor.setPosition(startPos, QTextCursor::KeepAnchor);

    int position = mCursor.selectionEnd();

    const QString text = mCursor.selectedText();

    if (text.isEmpty()) {// start of a paragraph
        if (!excludeToUppercase(mWord)) {
            mWord.replace(0, 1, mWord.at(0).toUpper());
        }
    } else {
        QString::ConstIterator constIter = text.constEnd();
        --constIter;

        while (constIter != text.constBegin()) {
            while (constIter != text.begin() && constIter->isSpace()) {
                --constIter;
                --position;
            }

            if (constIter != text.constBegin() && (*constIter == QLatin1Char('.') || *constIter == QLatin1Char('!') || *constIter == QLatin1Char('?'))) {
                constIter--;
                while (constIter != text.constBegin() && !(constIter->isLetter())) {
                    --position;
                    --constIter;
                }
                selectPreviousWord(mCursor, --position);
                const QString prevWord = mCursor.selectedText();

                // search for exception
                if (mUpperCaseExceptions.contains(prevWord.trimmed())) {
                    break;
                }
                if (excludeToUppercase(mWord)) {
                    break;
                }

                mWord.replace(0, 1, mWord.at(0).toUpper());
                break;
            } else {
                break;
            }
        }
    }

    mCursor.setPosition(startPos);
    mCursor.setPosition(startPos + mWord.length(), QTextCursor::KeepAnchor);
}

bool AutoCorrection::autoFractions()
{
    if (!mAutoFractions) {
        return false;
    }

    const QString trimmed = mWord.trimmed();
    if (trimmed.length() > 3) {
        const QChar x = trimmed.at(3);
        const uchar xunicode = x.unicode();
        if (!(xunicode == '.' || xunicode == ',' || xunicode == '?' || xunicode == '!'
              || xunicode == ':' || xunicode == ';')) {
            return false;
        }
    } else if (trimmed.length() < 3) {
        return false;
    }

    if (trimmed.startsWith(QStringLiteral("1/2"))) {
        mWord.replace(0, 3, QStringLiteral("½"));
    } else if (trimmed.startsWith(QStringLiteral("1/4"))) {
        mWord.replace(0, 3, QStringLiteral("¼"));
    } else if (trimmed.startsWith(QStringLiteral("3/4"))) {
        mWord.replace(0, 3, QStringLiteral("¾"));
    } else {
        return false;
    }

    return true;
}

int AutoCorrection::advancedAutocorrect()
{
    if (!mAdvancedAutocorrect) {
        return -1;
    }
    if (mAutocorrectEntries.isEmpty()) {
        return -1;
    }

    const QString trimmedWord = mWord.trimmed();
    if (trimmedWord.isEmpty()) {
        return -1;
    }

    QString actualWord = trimmedWord;

    const int actualWordLength(actualWord.length());
    if (actualWordLength < mMinFindStringLenght) {
        return -1;
    }
    if (actualWordLength > mMaxFindStringLenght) {
        return -1;
    }

    const int startPos = mCursor.selectionStart();
    const int length = mWord.length();
    // If the last char is punctuation, drop it for now
    bool hasPunctuation = false;
    const QChar lastChar = actualWord.at(actualWord.length() - 1);
    const ushort charUnicode = lastChar.unicode();
    if (charUnicode == '.' || charUnicode == ',' || charUnicode == '?'
        || charUnicode == '!' || charUnicode == ':' || charUnicode == ';') {
        hasPunctuation = true;
        actualWord.chop(1);
    }

    QString actualWordWithFirstUpperCase = actualWord;
    actualWordWithFirstUpperCase[0] = actualWordWithFirstUpperCase[0].toUpper();
    QHashIterator<QString, QString> i(mAutocorrectEntries);
    while (i.hasNext()) {
        i.next();
        if (i.key().length() > actualWordLength) {
            continue;
        }
        if (actualWord.endsWith(i.key())
            || actualWord.toLower().endsWith(i.key())
            || actualWordWithFirstUpperCase.endsWith(i.key())) {
            int pos = mWord.lastIndexOf(i.key());
            if (pos == -1) {
                pos = actualWord.toLower().lastIndexOf(i.key());
            }
            if (pos == -1) {
                pos = actualWordWithFirstUpperCase.lastIndexOf(i.key());
            }
            QString replacement = i.value();

            // Keep capitalized words capitalized.
            // (Necessary to make sure the first letters match???)
            const QChar actualWordFirstChar = actualWord.at(pos);
            const QChar replacementFirstChar = replacement[0];
            if (actualWordFirstChar.isUpper() && replacementFirstChar.isLower()) {
                replacement[0] = replacementFirstChar.toUpper();
            } else if (actualWordFirstChar.isLower() && replacementFirstChar.isUpper()) {
                replacement[0] = replacementFirstChar.toLower();
            }

            // If a punctuation mark was on the end originally, add it back on
            if (hasPunctuation) {
                replacement.append(lastChar);
            }

            mWord.replace(pos, pos + trimmedWord.length(), replacement);

            // We do replacement here, since the length of new word might be different from length of
            // the old world. Length difference might affect other type of autocorrection
            mCursor.setPosition(startPos);
            mCursor.setPosition(startPos + length, QTextCursor::KeepAnchor);
            mCursor.insertText(mWord);
            mCursor.setPosition(startPos); // also restore the selection
            const int newPosition = startPos + mWord.length();
            mCursor.setPosition(newPosition, QTextCursor::KeepAnchor);
            return newPosition;
        }
    }
    return -1;
}

void AutoCorrection::replaceTypographicQuotes()
{
    /* this method is ported from lib/kotext/KoAutoFormat.cpp KoAutoFormat::doTypographicQuotes
     * from Calligra 1.x branch */

    if (!(mReplaceDoubleQuotes && mWord.contains(QLatin1Char('"')))
        && !(mReplaceSingleQuotes && mWord.contains(QLatin1Char('\'')))) {
        return;
    }

    const bool addNonBreakingSpace = (isFrenchLanguage() && isAddNonBreakingSpace());

    // Need to determine if we want a starting or ending quote.
    // we use a starting quote in three cases:
    //  1. if the previous character is a space
    //  2. if the previous character is some kind of opening punctuation (e.g., "(", "[", or "{")
    //     a. and the character before that is not an opening quote (so that we get quotations of single characters
    //        right)
    //  3. if the previous character is an opening quote (so that we get nested quotations right)
    //     a. and the character before that is not an opening quote (so that we get quotations of single characters
    //         right)
    //     b. and the previous quote of a different kind (so that we get empty quotations right)

    bool ending = true;
    for (int i = mWord.length(); i > 1; --i) {
        const QChar c = mWord.at(i - 1);
        if (c == QLatin1Char('"') || c == QLatin1Char('\'')) {
            const bool doubleQuotes = (c == QLatin1Char('"'));
            if (i > 2) {
                QChar::Category c1 = mWord.at(i - 1).category();

                // case 1 and 2
                if (c1 == QChar::Separator_Space || c1 == QChar::Separator_Line || c1 == QChar::Separator_Paragraph
                    || c1 == QChar::Punctuation_Open || c1 == QChar::Other_Control) {
                    ending = false;
                }

                // case 3
                if (c1 == QChar::Punctuation_InitialQuote) {
                    QChar openingQuote;

                    if (doubleQuotes) {
                        openingQuote = mTypographicDoubleQuotes.begin;
                    } else {
                        openingQuote = mTypographicSingleQuotes.begin;
                    }

                    // case 3b
                    if (mWord.at(i - 1) != openingQuote) {
                        ending = false;
                    }
                }
            }
            // case 2a and 3a
            if (i > 3 && !ending) {
                const QChar::Category c2 = (mWord.at(i - 2)).category();
                ending = (c2 == QChar::Punctuation_InitialQuote);
            }

            if (doubleQuotes && mReplaceDoubleQuotes) {
                if (ending) {
                    if (addNonBreakingSpace) {
                        mWord.replace(i - 1, 2, QString(mNonBreakingSpace + mTypographicDoubleQuotes.end));
                    } else {
                        mWord[i - 1] = mTypographicDoubleQuotes.end;
                    }
                } else {
                    if (addNonBreakingSpace) {
                        mWord.replace(i - 1, 2, QString(mNonBreakingSpace + mTypographicDoubleQuotes.begin));
                    } else {
                        mWord[i - 1] = mTypographicDoubleQuotes.begin;
                    }
                }
            } else if (mReplaceSingleQuotes) {
                if (ending) {
                    if (addNonBreakingSpace) {
                        mWord.replace(i - 1, 2, QString(mNonBreakingSpace + mTypographicSingleQuotes.end));
                    } else {
                        mWord[i - 1] = mTypographicSingleQuotes.end;
                    }
                } else {
                    if (addNonBreakingSpace) {
                        mWord.replace(i - 1, 2, QString(mNonBreakingSpace + mTypographicSingleQuotes.begin));
                    } else {
                        mWord[i - 1] = mTypographicSingleQuotes.begin;
                    }
                }
            }
        }
    }

    // first character
    if (mWord.at(0) == QLatin1Char('"') && mReplaceDoubleQuotes) {
        mWord[0] = mTypographicDoubleQuotes.begin;
        if (addNonBreakingSpace) {
            mWord.insert(1, mNonBreakingSpace);
        }
    } else if (mWord.at(0) == QLatin1Char('\'') && mReplaceSingleQuotes) {
        mWord[0] = mTypographicSingleQuotes.begin;
        if (addNonBreakingSpace) {
            mWord.insert(1, mNonBreakingSpace);
        }
    }
}

void AutoCorrection::readAutoCorrectionXmlFile(bool forceGlobal)
{
    QString kdelang = QStringLiteral("en_US");
    const QStringList lst = QLocale::system().uiLanguages();
    if (!lst.isEmpty()) {
        kdelang = lst.at(0);
        if (kdelang == QLatin1String("C")) {
            kdelang = QStringLiteral("en_US");
        }
    }
    kdelang.remove(QRegularExpression(QStringLiteral("@.*")));

    mUpperCaseExceptions.clear();
    mAutocorrectEntries.clear();
    mTwoUpperLetterExceptions.clear();
    mSuperScriptEntries.clear();

    QString localFileName;
    //Look at local file:
    if (!forceGlobal) {
        if (!mAutoCorrectLang.isEmpty()) {
            localFileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("autocorrect/custom-") + mAutoCorrectLang + QLatin1String(".xml"));
        } else {
            if (!kdelang.isEmpty()) {
                localFileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("autocorrect/custom-") + kdelang + QLatin1String(".xml"));
            }
            if (localFileName.isEmpty() && kdelang.contains(QLatin1Char('_'))) {
                kdelang.remove(QRegularExpression(QStringLiteral("_.*")));
                localFileName = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("autocorrect/custom-") + kdelang + QLatin1String(".xml"));
            }
        }
    }
    QString fname;
    //Load Global directly
    if (!mAutoCorrectLang.isEmpty()) {
        if (mAutoCorrectLang == QLatin1String("en_US")) {
            fname = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("autocorrect/autocorrect.xml"));
        } else {
            fname = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("autocorrect/") + mAutoCorrectLang + QLatin1String(".xml"));
        }
    } else {
        if (fname.isEmpty() && !kdelang.isEmpty()) {
            fname = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("autocorrect/") + kdelang + QLatin1String(".xml"));
        }
        if (fname.isEmpty() && kdelang.contains(QLatin1Char('_'))) {
            kdelang.remove(QRegularExpression(QStringLiteral("_.*")));
            fname = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("autocorrect/") + kdelang + QLatin1String(".xml"));
        }
    }
    if (fname.isEmpty()) {
        fname = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("autocorrect/autocorrect.xml"));
    }

    if (mAutoCorrectLang.isEmpty()) {
        mAutoCorrectLang = kdelang;
    }
    //qCDebug(PIMCOMMON_LOG)<<" fname :"<<fname;
    //qCDebug(PIMCOMMON_LOG)<<" LocalFile:"<<LocalFile;

    if (localFileName.isEmpty()) {
        loadGlobalFileName(fname, forceGlobal);
    } else {
        loadLocalFileName(localFileName, fname);
    }
}

void AutoCorrection::loadGlobalFileName(const QString &fname, bool forceGlobal)
{
    if (fname.isEmpty()) {
        mTypographicSingleQuotes = typographicDefaultSingleQuotes();
        mTypographicDoubleQuotes = typographicDefaultDoubleQuotes();
    } else {
        ImportKMailAutocorrection import;
        if (import.import(fname, ImportAbstractAutocorrection::All)) {
            mUpperCaseExceptions = import.upperCaseExceptions();
            mTwoUpperLetterExceptions = import.twoUpperLetterExceptions();
            mAutocorrectEntries = import.autocorrectEntries();
            mTypographicSingleQuotes = import.typographicSingleQuotes();
            mTypographicDoubleQuotes = import.typographicDoubleQuotes();
            mSuperScriptEntries = import.superScriptEntries();
            if (forceGlobal) {
                mTypographicSingleQuotes = typographicDefaultSingleQuotes();
                mTypographicDoubleQuotes = typographicDefaultDoubleQuotes();
            }
            mMaxFindStringLenght = import.maxFindStringLenght();
            mMinFindStringLenght = import.minFindStringLenght();
        }
    }
}

void AutoCorrection::loadLocalFileName(const QString &localFileName, const QString &fname)
{
    ImportKMailAutocorrection import;
    if (import.import(localFileName, ImportAbstractAutocorrection::All)) {
        mUpperCaseExceptions = import.upperCaseExceptions();
        mTwoUpperLetterExceptions = import.twoUpperLetterExceptions();
        mAutocorrectEntries = import.autocorrectEntries();
        mTypographicSingleQuotes = import.typographicSingleQuotes();
        mTypographicDoubleQuotes = import.typographicDoubleQuotes();
        //Don't import it in local
        //mSuperScriptEntries = import.superScriptEntries();
    }
    if (!fname.isEmpty() && import.import(fname, ImportAbstractAutocorrection::SuperScript)) {
        mSuperScriptEntries = import.superScriptEntries();
    }
    mMaxFindStringLenght = import.maxFindStringLenght();
    mMinFindStringLenght = import.minFindStringLenght();
}

void AutoCorrection::writeAutoCorrectionXmlFile(const QString &filename)
{
    const QString fname = filename.isEmpty() ? QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/autocorrect/custom-")
                          + (mAutoCorrectLang == QLatin1String("en_US") ? QStringLiteral("autocorrect") : mAutoCorrectLang) + QLatin1String(".xml") : filename;
    QFileInfo fileInfo(fname);
    QDir().mkpath(fileInfo.absolutePath());
    QFile file(fname);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        qCDebug(PIMCOMMON_LOG) << "We can't save in file :" << fname;
        return;
    }

    QXmlStreamWriter streamWriter(&file);

    streamWriter.setAutoFormatting(true);
    streamWriter.setAutoFormattingIndent(2);
    streamWriter.writeStartDocument();

    streamWriter.writeDTD(QStringLiteral("<!DOCTYPE autocorrection>"));

    streamWriter.writeStartElement(QStringLiteral("Word"));

    streamWriter.writeStartElement(QStringLiteral("items"));
    QHashIterator<QString, QString> i(mAutocorrectEntries);
    while (i.hasNext()) {
        i.next();
        streamWriter.writeStartElement(QStringLiteral("item"));
        streamWriter.writeAttribute(QStringLiteral("find"), i.key());
        streamWriter.writeAttribute(QStringLiteral("replace"), i.value());
        streamWriter.writeEndElement();
    }
    streamWriter.writeEndElement();

    streamWriter.writeStartElement(QStringLiteral("UpperCaseExceptions"));
    QSet<QString>::const_iterator upper = mUpperCaseExceptions.constBegin();
    while (upper != mUpperCaseExceptions.constEnd()) {
        streamWriter.writeStartElement(QStringLiteral("word"));
        streamWriter.writeAttribute(QStringLiteral("exception"), *upper);
        ++upper;
        streamWriter.writeEndElement();
    }
    streamWriter.writeEndElement();

    streamWriter.writeStartElement(QStringLiteral("TwoUpperLetterExceptions"));
    QSet<QString>::const_iterator twoUpper = mTwoUpperLetterExceptions.constBegin();
    while (twoUpper != mTwoUpperLetterExceptions.constEnd()) {
        streamWriter.writeStartElement(QStringLiteral("word"));
        streamWriter.writeAttribute(QStringLiteral("exception"), *twoUpper);
        ++twoUpper;
        streamWriter.writeEndElement();
    }
    streamWriter.writeEndElement();

    streamWriter.writeStartElement(QStringLiteral("DoubleQuote"));
    streamWriter.writeStartElement(QStringLiteral("doublequote"));
    streamWriter.writeAttribute(QStringLiteral("begin"), mTypographicDoubleQuotes.begin);
    streamWriter.writeAttribute(QStringLiteral("end"), mTypographicDoubleQuotes.end);
    streamWriter.writeEndElement();
    streamWriter.writeEndElement();

    streamWriter.writeStartElement(QStringLiteral("SimpleQuote"));
    streamWriter.writeStartElement(QStringLiteral("simplequote"));
    streamWriter.writeAttribute(QStringLiteral("begin"), mTypographicSingleQuotes.begin);
    streamWriter.writeAttribute(QStringLiteral("end"), mTypographicSingleQuotes.end);
    streamWriter.writeEndElement();
    streamWriter.writeEndElement();

    streamWriter.writeEndDocument();
}

QString AutoCorrection::language() const
{
    return mAutoCorrectLang;
}

AutoCorrection::TypographicQuotes AutoCorrection::typographicSingleQuotes() const
{
    return mTypographicSingleQuotes;
}

AutoCorrection::TypographicQuotes AutoCorrection::typographicDoubleQuotes() const
{
    return mTypographicDoubleQuotes;
}

void AutoCorrection::setLanguage(const QString &lang, bool forceGlobal)
{
    if (mAutoCorrectLang != lang || forceGlobal) {
        mAutoCorrectLang = lang;
        //Re-read xml file
        readAutoCorrectionXmlFile(forceGlobal);
    }
}

void AutoCorrection::setEnabledAutoCorrection(bool b)
{
    mEnabled = b;
}

void AutoCorrection::setUppercaseFirstCharOfSentence(bool b)
{
    mUppercaseFirstCharOfSentence = b;
}

void AutoCorrection::setFixTwoUppercaseChars(bool b)
{
    mFixTwoUppercaseChars = b;
}

void AutoCorrection::setSingleSpaces(bool b)
{
    mSingleSpaces = b;
}

void AutoCorrection::setAutoFractions(bool b)
{
    mAutoFractions = b;
}

void AutoCorrection::setCapitalizeWeekDays(bool b)
{
    mCapitalizeWeekDays = b;
}

void AutoCorrection::setReplaceDoubleQuotes(bool b)
{
    mReplaceDoubleQuotes = b;
}

void AutoCorrection::setReplaceSingleQuotes(bool b)
{
    mReplaceSingleQuotes = b;
}

void AutoCorrection::setAdvancedAutocorrect(bool b)
{
    mAdvancedAutocorrect = b;
}

void AutoCorrection::setTypographicSingleQuotes(AutoCorrection::TypographicQuotes singleQuote)
{
    mTypographicSingleQuotes = singleQuote;
}

void AutoCorrection::setTypographicDoubleQuotes(AutoCorrection::TypographicQuotes doubleQuote)
{
    mTypographicDoubleQuotes = doubleQuote;
}

bool AutoCorrection::isFrenchLanguage() const
{
    return mAutoCorrectLang == QLatin1String("FR_fr") || mAutoCorrectLang == QLatin1String("fr");
}
