/***************************************************************************
 *   Copyright (C) 2004-2017 by Pere Constans
 *   constans@molspaces.com
 *   cb2Bib version 1.9.7. Licensed under the GNU GPL version 3.
 *   See the LICENSE file that comes with this distribution.
 ***************************************************************************/
#ifndef CB2BIB_UTILITIES_H
#define CB2BIB_UTILITIES_H

#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QRegExp>
#include <QString>
#include <QStringMatcher>
#include <QtDebug>


/**
    cb2Bib core utilities
*/

namespace c2bUtils
{

enum AsciiConversion {Cleanup, Collation, FromBibTeX, KeepWords, Transliterate};
extern QString setCapitalization(const QString& str);
extern QString toAscii(const QString& str, const AsciiConversion type);
extern QString& bibToC2b(QString& str);
extern QString& c2bToBib(QString& str);
extern QString& fillString(QString& str, const QRegExp& pattern, const QChar& ch);
extern QString& fillString(QString& str, const QRegularExpression& pattern, const QChar& ch);
extern QString& fillString(QString& str, const QString& pattern, const QChar& ch);
extern QString& fillString(QString& str, const QStringMatcher& pattern, const QChar& ch);
extern QString& simplifyString(QString& str);
extern QString& stripDiacritics(QString& str);

static const QRegExp pnasGreekLetters("[\\{\\[\\(](alpha|beta|gamma|delta|varepsilon|"
                                      "zeta|eta|theta|iota|kappa|lambda|mu|nu|xi|"
                                      "omicron|pi|rho|sigmaf|varsigma|sigma|tau|"
                                      "upsilon|phi|chi|psi|omega|vartheta|varphi|"
                                      "varpi|digamma|varkappa|varrho|epsilon)[\\}\\]\\)]",
                                      Qt::CaseInsensitive); // Only used in QString::replace which keeps QRegExp const
static const QRegExp isBibToC2b("[\\{\\[\\$\\(\\\\<>]");
static const QRegExp nonAsciiLetter("[^A-Za-z]");
static const QRegExp nonLetter("\\W");
static const QStringMatcher hasLatexDiacritic("{\\");
static const QStringMatcher hasLatexSymbol("$\\");

// Link set according to: "Creating a Web Link to the Entrez Databases",
// http://www.ncbi.nlm.nih.gov/books/bv.fcgi?rid=helplinks.chapter.linkshelp#linkshelp.Retrieve_PubMed_Cita
static const QString pubmedUrl("http://www.ncbi.nlm.nih.gov/pubmed/%1");

inline QString& fullBibToC2b(QString& str)
{
    // Escape TeX and other special characters to Unicode
    if (!str.contains(isBibToC2b))
        return str;
    // PNAS Style
    str.replace(pnasGreekLetters, "$\\\\1$");
    str.replace("{micro}", "$\\mu$");
    str.replace("{middle dot}", "$\\cdot$");
    // Sub and superscripts
    str.replace(QRegExp("<sub>([^<]*)</sub>"), "$_{\\1}$");
    str.replace(QRegExp("<sup>([^<]*)</sup>"), "$^{\\1}$");
    str.replace(QRegExp("\\[sub ([^\\]]*)\\]"), "$_{\\1}$");
    str.replace(QRegExp("\\[sup ([^\\]]*)\\]"), "$^{\\1}$");
    str.replace(QRegExp("\\}\\$\\$([_\\^])\\{"), "}\\1{"); // merge if both
    // TeX to Unicode
    bibToC2b(str);
    return str;
}

inline QString& cleanEquations(QString& str)
{
    // Simplify equations
    const QChar eq('$');
    int i(str.indexOf(eq));
    if (i == -1)
        return str;
    const QChar ob('{');
    const QChar cb('}');
    const QChar ca('^');
    const QChar sl('\\');
    const QChar us('_');
    const int n(str.length());
    int p(i);
    while (i < n)
    {
        if (str.at(i) == eq)
        {
            bool has_eq(false);
            for (int j = i + 1; j < qMin(i + 70, n); ++j) // Set a max equation length
                if (str.at(j) == eq)
                {
                    has_eq = true;
                    for (int k = i + 1; k < j; ++k)
                    {
                        const QChar& ck = str.at(k);
                        if (ck != us && ck != ca && ck != sl && ck != ob && ck != cb)
                            str[p++] = str.at(k);
                    }
                    i = j;
                    break;
                }
            if (!has_eq)
                str[p++] = str.at(i);
        }
        else
            str[p++] = str.at(i);
        ++i;
    }
    str.chop(n - p);
    return str;
}

inline QString& cleanTitle(QString& title, const bool clean_period = false)
{
    // Remove TeX braces and equation $
    cleanEquations(title);
    title.remove('{');
    title.remove('}');
    title = title.trimmed();
    if (clean_period && title.length() > 0)
        if (title[title.length() - 1] == '.')
            title.chop(1);
    return title;
}

inline QString toHtmlString(QString str)
{
    // Encode some symbols to HTML for proper browser display
    str.replace('<', "&#060;");
    str.replace('>', "&#062;");
    str.replace('%', "&#037;");
    str.replace(QRegExp("_\\{([^\\}]*)\\}"), "<sub>\\1</sub>");
    str.replace(QRegExp("\\^\\{([^\\}]*)\\}"), "<sup>\\1</sup>");
    cleanEquations(str);
    return str;
}

inline QString firstPage(QString pages)
{
    simplifyString(pages);
    pages.remove(QRegExp("[\\s\\-].*$"));
    return pages;
}

inline bool isLowerCaseString(const QString& str, const int pos0 = 0, int posn = -1)
{
    if (str.isEmpty())
        return false;
    if (posn < 0)
        posn = str.length();
    for (int i = pos0; i < posn; ++i)
        if (str.at(i).isLetter())
            if (str.at(i).category() == QChar::Letter_Uppercase)
                return false;
    return true;
}

inline bool isUpperCaseString(const QString& str, const int pos0 = 0, int posn = -1)
{
    if (str.isEmpty())
        return false;
    if (posn < 0)
        posn = str.length();
    for (int i = pos0; i < posn; ++i)
        if (str.at(i).isLetter())
            if (str.at(i).category() == QChar::Letter_Lowercase)
                return false;
    return true;
}

/**
    Gives a document absolute, full path-name
*/
inline QString documentAbsoluteName(const bool bibtexrelative, const QString& bibtexfn, const QString& docdir, const QString& docfn)
{
    QString name;
    if (bibtexrelative)
    {
        // The bibtexfn might be relative in USB mode, calling absolutePath will restore its fullname.
        QDir ddir(QFileInfo(bibtexfn).absolutePath());
        // When bibtexrelative, should only be relative to bibtexfn, (safely) disregard if it is absolute.
        if (QDir(docdir).isRelative())
            name = ddir.absolutePath() + '/' + docdir + '/' + QFileInfo(docfn).fileName();
        else
            name = ddir.absolutePath() + '/' + QFileInfo(docfn).fileName();
    }
    else
        // The docdir should be absolute here, whether or not working in USB mode. If it was relative,
        // this will set it with respect the application directory.
        name = QDir(docdir).absolutePath() + '/' + QFileInfo(docfn).fileName();
    return QDir::toNativeSeparators(QDir::cleanPath(name));
}

/**
    Gives a document filename, as it will be written to BibTeX file tag
*/
inline QString documentFilename(const bool bibtexrelative, const QString& bibtexfn, const QString& docdir, const QString& docfn)
{
    QString name;
    if (bibtexrelative)
    {
        QDir ddir(QFileInfo(bibtexfn).absolutePath());
        if (QDir(docdir).isRelative())
            name = ddir.absolutePath() + '/' + docdir + '/' + QFileInfo(docfn).fileName();
        else
            name = ddir.absolutePath() + '/' + QFileInfo(docfn).fileName();
        name = ddir.relativeFilePath(name);
    }
    else
        name = QDir(docdir).absolutePath() + '/' + QFileInfo(docfn).fileName();
    return QDir::toNativeSeparators(QDir::cleanPath(name));
}

/**
    Gives a document path as it will be written to BibTeX file tag
*/
inline QString documentDirName(const bool bibtexrelative, const QString& bibtexfn, const QString& docdir)
{
    QString name;
    if (bibtexrelative)
    {
        QDir ddir(QFileInfo(bibtexfn).absolutePath());
        if (QDir(docdir).isRelative())
            name = ddir.absolutePath() + '/' + docdir;
        else
            name = docdir + '/';
        name = ddir.relativeFilePath(name);
    }
    else
        name = QDir(docdir).absolutePath();
    name = QDir::toNativeSeparators(QDir::cleanPath(name));
    return name;
}

inline QString fileToString(const QString& fn, bool delete_on_close = false)
{
    if (fn.isEmpty())
        return QString();
    QFile f(fn);
    if (f.open(QIODevice::ReadOnly | QIODevice::Text))
    {
        QTextStream stream(&f);
        stream.setCodec("UTF-8");
        stream.setAutoDetectUnicode(true);
        const QString contents(stream.readAll());
        if (delete_on_close)
            f.remove();
        else
            f.close();
        return (contents);
    }
    else
        return QString();
}

inline bool stringToFile(const QString& str, const QString& fn)
{
    if (fn.isEmpty())
        return false;
    QFile f(fn);
    if (f.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        QTextStream stream(&f);
        stream.setCodec("UTF-8");
        stream.setAutoDetectUnicode(true);
        stream << str;
        f.close();
        return true;
    }
    else
        return false;
}

inline QStringList filesInDir(const QString& dir, const QStringList& filters)
{
    QStringList files;
    QFileInfo fi(dir);
    if (!fi.exists())
        return files;
    if (fi.isDir())
    {
        QDir d(dir);
        const QFileInfoList dil(d.entryInfoList(filters));
        for (int i = 0; i < dil.count(); ++i)
            files.append(QDir::toNativeSeparators(QDir::cleanPath(dil.at(i).absoluteFilePath())));
    }
    else if (fi.isFile())
        files.append(QDir::toNativeSeparators(QDir::cleanPath(fi.absoluteFilePath())));
    return files;
}

inline int nearInteger(double a)
{
    int ia = int(a);
    a = a - double(ia);
    if (a >= 0.5)
        ia++;
    else if (a <= -0.5)
        ia--;
    return ia;
}

inline bool inBraces(const int pos, const QString& str, QString* in)
{
    if (str.at(pos - 1) == '{') // pos > 0 always
    {
        int open_braces(1);
        const QChar open('{');
        const QChar close('}');
        QChar si;
        for (int i = pos; i < str.length(); ++i)
        {
            si = str.at(i);
            if (si == open)
                open_braces++;
            else if (si == close)
                open_braces--;
            if (open_braces == 0)
            {
                *in = str.mid(pos, i - pos);
                return true;
            }
        }
    }
    else // in Quotes
    {
        const QChar quote('"');
        const QChar comma(',');
        for (int i = pos; i < str.length() - 1; ++i)
            if (str.at(i) == quote)
                if (str.at(i + 1) == comma)
                {
                    *in = str.mid(pos, i - pos);
                    return true;
                }
    }
    return false;
}

inline bool patternLengthMoreThan(const QRegExp& s1, const QRegExp& s2)
{
    return s1.pattern().length() > s2.pattern().length();
}

inline QString fromQtXmlString(QString str)
{
    // Workaround (It will be fixed in Qt 4.5)
    // Escape toXmlString already done by ExifTool
    str.replace("%%#39;", "'");
    str.replace("%%amp;", "&");
    str.replace("%%gt;", ">");
    str.replace("%%lt;", "<");
    str.replace("%%quot;", "\"");
    return (str);
}

inline QString toQtXmlString(QString str)
{
    // Workaround (It will be fixed in Qt 4.5)
    // Escape toXmlString already done by ExifTool
    str.replace("&#39;", "%%#39;");
    str.replace("&amp;", "%%amp;");
    str.replace("&gt;", "%%gt;");
    str.replace("&lt;", "%%lt;");
    str.replace("&quot;", "%%quot;");
    return (str);
}

inline void warn(const QString& ms)
{
    qWarning("[cb2bib] %s.", (const char*)ms.toLatin1());
}

inline void debug(const QString&
#ifdef C2B_DEBUG
                  debug_ms
#endif
                 )
{
#ifdef C2B_DEBUG
    qDebug() << "[cb2bib] " << debug_ms;
#endif
}

extern const QString fromUtf8(const QByteArray& ba);

#ifdef C2B_USE_LZO
namespace lzo
{
static const int HEADER_LENGTH = 10;
extern const QByteArray compress(const QByteArray& data);
extern const QByteArray uncompress(const QByteArray& data);
} // namespace lzo

inline const QByteArray compress(const QByteArray& data)
{
    return lzo::compress(data);
}

inline const QByteArray uncompress(const QByteArray& data)
{
    return lzo::uncompress(data);
}
#else
inline const QByteArray compress(const QByteArray& data)
{
    return qCompress(data);
}

inline const QByteArray uncompress(const QByteArray& data)
{
    return qUncompress(data);
}
#endif

} // namespace c2bUtils

#endif
