#include <QDebug>
#include <QDir>
#include <QLabel>
#include <QMessageBox>
#include <QOperatingSystemVersion>
#include <QPixmap>
#include <QTextStream>
#include <cstdlib>
#include <iostream>

#include "file.h"
#include "vymprocess.h"

#if defined(Q_OS_WINDOWS)
#include "mkdtemp.h"
#include <windows.h>
#endif

#if defined(Q_OS_MACX)
#include "unistd.h"
#endif

using namespace File;

extern QString zipToolPath;
extern QString unzipToolPath;
extern bool zipToolAvailable;
extern bool unzipToolAvailable;

QString convertToRel(const QString &src, const QString &dst)
{
    // Creates a relative path pointing from src to dst

    QString s = src;
    QString d = dst;
    int i;

    if (s == d) {
        // Special case, we just need the name of the file,
        // not the complete path
        i = d.lastIndexOf("/");
        d = d.right(d.length() - i - 1);
    }
    else {
        // remove identical left parts
        while (s.section("/", 0, 0) == d.section("/", 0, 0)) {
            i = s.indexOf("/");
            s = s.right(s.length() - i - 1);
            d = d.right(d.length() - i - 1);
        }

        // Now take care of paths where we have to go back first
        int srcsep = s.count("/");
        while (srcsep > 0) {
            d = "../" + d;
            srcsep--;
        }
    }
    return d;
}

QString convertToAbs(const QString &src, const QString &dst)
{
    // Creates a relative path pointing from src to dst
    QDir dd(src);
    return dd.absoluteFilePath(dst);
}

QString basename(const QString &path) { return path.section('/', -1); }

QString dirname(const QString &path) { return path.section('/', 0, -2); }

extern QString vymName;
bool confirmDirectoryOverwrite(const QDir &dir)
{
    if (!dir.exists()) {
        qWarning() << "Directory does not exist: " << dir.path();
        return false;
    }

    QStringList eList = dir.entryList();
    while (!eList.isEmpty() && (eList.first() == "." || eList.first() == ".."))
        eList.pop_front(); // remove "." and ".."

    if (!eList.isEmpty()) {
        QMessageBox mb(vymName,
                       QObject::tr("The directory %1 is not empty.\nDo you "
                                   "risk to overwrite its contents?",
                                   "write directory")
                           .arg(dir.path()),
                       QMessageBox::Warning, QMessageBox::Yes,
                       QMessageBox::Cancel | QMessageBox::Default,
                       QMessageBox::NoButton);

        mb.setButtonText(QMessageBox::Yes, QObject::tr("Overwrite"));
        mb.setButtonText(QMessageBox::No, QObject::tr("Cancel"));
        switch (mb.exec()) {
        case QMessageBox::Yes:
            // save
            return true;
        case QMessageBox::Cancel:
            // do nothing
            return false;
        }
    }
    return true;
}

QString makeTmpDir(bool &ok, const QString &dirPath,
                   const QString &prefix)
{
    QString path = makeUniqueDir(ok, dirPath + "/" + prefix + "-XXXXXX");
    return path;
}

QString makeTmpDir(bool &ok, const QString &prefix)
{
    return makeTmpDir(ok, QDir::tempPath(), prefix);
}

bool isInTmpDir(QString fn)
{
    QString temp = QDir::tempPath();
    int l = temp.length();
    return fn.left(l) == temp;
}

QString makeUniqueDir(bool &ok, QString s) // FIXME-3 use QTemporaryDir
{
    ok = true;

    QString r;

#if defined(Q_OS_WINDOWS)
    r = mkdtemp(s);
#else
    // On Linux and friends use cstdlib

    // Convert QString to string
    ok = true;
    char *p;
    int bytes = s.length();
    p = (char *)malloc(bytes + 1);
    int i;
    for (i = 0; i < bytes; i++)
        p[i] = s.at(i).unicode();
    p[bytes] = 0;

    r = mkdtemp(p);
    free(p);
#endif

    if (r.isEmpty())
        ok = false;
    return r;
}

bool removeDir(QDir d)
{
    // This check should_ not be necessary, but proved to be useful ;-)
    if (!isInTmpDir(d.path())) {
        qWarning() << "file.cpp::removeDir should remove " + d.path() +
                          " - aborted.";
        return false;
    }

    return d.removeRecursively();
}

bool copyDir(QDir src, QDir dst, const bool &override)
{
    QStringList dirs =
        src.entryList(QDir::AllDirs | QDir::Hidden | QDir::NoDotAndDotDot);
    QStringList files = src.entryList(QDir::Files);

    // Check if dst is a subdir of src, which would cause endless recursion...
    if (dst.absolutePath().contains(src.absolutePath()))
        return false;

    // Traverse directories
    QList<QString>::iterator d, f;
    for (d = dirs.begin(); d != dirs.end(); ++d) {
        if (!QFileInfo(src.path() + "/" + (*d)).isDir())
            continue;

        QDir cdir(dst.path() + "/" + (*d));
        cdir.mkpath(cdir.path());

        if (!copyDir(QDir(src.path() + "/" + (*d)),
                     QDir(dst.path() + "/" + (*d)), override))
            return false;
    }

    // Traverse files
    for (f = files.begin(); f != files.end(); ++f) {
        QFile cfile(src.path() + "/" + (*f));
        QFile destFile(dst.path() + "/" +
                       src.relativeFilePath(cfile.fileName()));
        if (destFile.exists() && override)
            destFile.remove();

        if (!cfile.copy(dst.path() + "/" +
                        src.relativeFilePath(cfile.fileName())))
            return false;
    }
    return true;
}

bool subDirsExist()
{
    QStringList dirList;
    dirList << "images";
    dirList << "flags";
    dirList << "flags/user";
    dirList << "flags/standard";
    foreach (QString d, dirList)
        if (QDir(d).exists() ) return true;

    return false;
}

void makeSubDirs(const QString &s)
{
    QDir d(s);
    d.mkdir(s);
    d.mkdir("images");
    d.mkdir("flags");
    d.mkdir("flags/user");
    d.mkdir("flags/standard");
}

bool checkZipTool()
{
    zipToolAvailable = false;
#if defined(Q_OS_WINDOWS)
    if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows10)
        zipToolAvailable = true;
#else

    QFile tool(zipToolPath);

    zipToolAvailable = tool.exists();
#endif
    return zipToolAvailable;
}

bool checkUnzipTool()
{
    unzipToolAvailable = false;
#if defined(Q_OS_WINDOWS)
    if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows10)
        zipToolAvailable = true;
#else
    QFile tool(unzipToolPath);

    unzipToolAvailable = tool.exists();
#endif
    return unzipToolAvailable;
}

ErrorCode zipDir(QDir zipInputDir, QString zipName)
{
    zipName = QDir::toNativeSeparators(zipName);
    ErrorCode err = Success;

    QString symLinkTarget;

    QString newName;
    // Move existing file away
    QFile file(zipName);
    if (file.exists()) {
        symLinkTarget = file.symLinkTarget();
        QString zipNameTmp = zipName + ".tmp";
        newName = zipNameTmp;
        int n = 0;
        while (!file.rename(newName) && n < 5) {
            newName =
                zipNameTmp + QString().setNum(n);
            n++;
        }
        if (n >= 5) {
            QMessageBox::critical(0, QObject::tr("Critical Error"),
                                  QObject::tr("Couldn't move existing file out "
                                              "of the way before saving."));
            return Aborted;
        }
    }

    // zip the temporary directory
    VymProcess *zipProc = new VymProcess();
    QStringList args;

#if defined(Q_OS_WINDOWS)
    zipProc->setWorkingDirectory(
        QDir::toNativeSeparators(zipInputDir.path() + "\\"));

    args << "-a" << "-c" << "--format" << "zip" << "-f" << zipName << "*";

    zipProc->start(zipToolPath, args);

    if (!zipProc->waitForStarted()) {
        // zip could not be started
        QMessageBox::critical(
            0, QObject::tr("Critical Error"),
            QObject::tr("Couldn't start %1 tool to compress data!\n"
                        "The map could not be saved, please check if "

                        "backup file is available or export as XML file!")
                    .arg("Windows zip") +
                "\n\nziptoolpath: " + zipToolPath +
                "\nargs: " + args.join(" "));
        err = Aborted;
    }
    else {
        // zip could be started
        zipProc->waitForFinished();
        if (zipProc->exitStatus() != QProcess::NormalExit) {
            QMessageBox::critical(0, QObject::tr("Critical Error"),
                                  QObject::tr("zip didn't exit normally") +
                                      "\n" + zipProc->getErrout());
            err = Aborted;
        }
        else {
            /*
            QMessageBox::information( 0, QObject::tr( "Debug" ),
                               "Called:" + zipToolPath + "\n" +
                               "Args: "  + args.join(" ") + "\n" +
                               "Exit: "  + zipProc->exitCode() + "\n" +
                               "Err: " + zipProc->getErrout()  + "\n" +
                               "Std: " + zipProc->getStdout() );
            */
            if (zipProc->exitCode() > 1) {
                QMessageBox::critical(
                    0, QObject::tr("Error"),
                    "Called:" + zipToolPath + "\n" + "Args: " + args.join(" ") +
                        "\n" + "Exit: " + zipProc->exitCode() + "\n" +
                        "Err: " + zipProc->getErrout() + "\n" +
                        "Std: " + zipProc->getStdout());
                err = Aborted;
            }
            else if (zipProc->exitCode() == 1) {
                // Non fatal according to internet, but for example
                // some file was locked and could not be compressed
                QMessageBox::warning(
                    0, QObject::tr("Error"),
                    "Called:" + zipToolPath + "\n" + "Args: " + args.join(" ") +
                        "\n" + "Exit: " + zipProc->exitCode() + "\n" +
                        "Err: " + zipProc->getErrout() + "\n" +
                        "Std: " + zipProc->getStdout() +
                        "\n"
                        "Please check the saved map, e.g. by opening in "
                        "another tab.\n" +
                        "Workaround if save failed: Export as xml");
            }
        }
    }
    // qDebug() <<"Output: " << zipProc->getStdout()<<flush;
#else
    zipProc->setWorkingDirectory(QDir::toNativeSeparators(zipInputDir.path()));
    args << "-r";
    args << zipName;
    args << ".";

    zipProc->start(zipToolPath, args);
    if (!zipProc->waitForStarted()) {
        // zip could not be started
        QMessageBox::critical(
            0, QObject::tr("Critical Error"),
            QObject::tr("Couldn't start %1 tool to compress data!\n"
                        "The map could not be saved, please check if "
                        "backup file is available or export as XML file!")
                    .arg("zip") +
                "\n\nziptoolpath: " + zipToolPath +
                "\nargs: " + args.join(" "));
        err = Aborted;
    }
    else {
        // zip could be started
        zipProc->waitForFinished();
        if (zipProc->exitStatus() != QProcess::NormalExit) {
            QMessageBox::critical(0, QObject::tr("Critical Error"),
                                  QObject::tr("zip didn't exit normally") +
                                      "\n" + zipProc->getErrout());
            err = Aborted;
        }
        else {
            if (zipProc->exitCode() > 0) {
                QMessageBox::critical(
                    0, QObject::tr("Critical Error"),
                    QString("zip exit code:  %1").arg(zipProc->exitCode()) +
                        "\n" + zipProc->getErrout());
                err = Aborted;
            }
        }
    }
#endif
    // Try to restore previous file, if zipping failed
    if (err == Aborted && !newName.isEmpty() && !file.rename(zipName))
        QMessageBox::critical(0, QObject::tr("Critical Error"),
                              QObject::tr("Couldn't rename %1 back to %2")
                                  .arg(newName)
                                  .arg(zipName));
    else {
        // Take care of symbolic link
        if (!symLinkTarget.isEmpty()) {
            if (!QFile(symLinkTarget).remove()) {
                QMessageBox::critical(
                    0, QObject::tr("Critical Error"),
                    QObject::tr(
                        "Couldn't remove target of old symbolic link %1")
                        .arg(symLinkTarget));
                err = Aborted;
                return err;
            }

            if (!QFile(zipName).rename(symLinkTarget)) {
                QMessageBox::critical(
                    0, QObject::tr("Critical Error"),
                    QObject::tr("Couldn't rename output to target of old "
                                "symbolic link %1")
                        .arg(symLinkTarget));
                err = Aborted;
                return err;
            }
            if (!QFile(symLinkTarget).link(zipName)) {
                QMessageBox::critical(
                    0, QObject::tr("Critical Error"),
                    QObject::tr("Couldn't link from %1 to target of old "
                                "symbolic link %2")
                        .arg(zipName)
                        .arg(symLinkTarget));
                err = Aborted;
                return err;
            }
        }

        // Remove temporary file
        if (!newName.isEmpty() && !file.remove())
            QMessageBox::critical(
                0, QObject::tr("Critical Error"),
                QObject::tr("Saved %1, but couldn't remove %2")
                    .arg(zipName)
                    .arg(newName));
    }

    return err;
}

File::ErrorCode unzipDir(QDir zipOutputDir, QString zipName)
{
    ErrorCode err = Success;

    VymProcess *zipProc = new VymProcess();
    QStringList args;

#if defined(Q_OS_WINDOWS)
    zipProc->setWorkingDirectory(
        QDir::toNativeSeparators(zipOutputDir.path() + "\\"));
    args << "-x" << "-f" << zipName.toUtf8() << "-C" << zipOutputDir.path();
    zipProc->start(zipToolPath, args);
#else
    zipProc->setWorkingDirectory(QDir::toNativeSeparators(zipOutputDir.path()));
    args << "-o"; // overwrite existing files!
    args << zipName;
    args << "-d";
    args << zipOutputDir.path();

    zipProc->start(unzipToolPath, args);
#endif
    if (!zipProc->waitForStarted()) {
        QMessageBox::critical(
            0, QObject::tr("Critical Error"),
            QObject::tr("Couldn't start %1 tool to decompress data!\n")
                    .arg("Windows zip") +
                "\n\nziptoolpath: " + zipToolPath +
                "\nargs: " + args.join(" "));
        err = Aborted;
    }
    else {
        zipProc->waitForFinished();
        if (zipProc->exitStatus() != QProcess::NormalExit) {
            QMessageBox::critical(
                0, QObject::tr("Critical Error"),
                QObject::tr("%1 didn't exit normally").arg(zipToolPath) +
                    zipProc->getErrout());
            err = Aborted;
        }
        else {
            /*
            QMessageBox::information( 0, QObject::tr( "Debug" ),
                               "Called:" + zipToolPath + "\n" +
                               "Args: "  + args.join(" ") + "\n" +
                               "Exit: "  + zipProc->exitCode() + "\n" +
                               "Err: " + zipProc->getErrout()  + "\n" +
                               "Std: " + zipProc->getStdout() );
            */
            if (zipProc->exitCode() > 1) {
                QMessageBox::critical(
                    0, QObject::tr("Error"),
                    "Called:" + zipToolPath + "\n" + "Args: " + args.join(" ") +
                        "\n" + "Exit: " + zipProc->exitCode() + "\n" +
                        "Err: " + zipProc->getErrout() + "\n" +
                        "Std: " + zipProc->getStdout());
                err = Aborted;
            }
            else if (zipProc->exitCode() == 1) {
                // Non fatal according to internet, but for example
                // some file was locked and could not be compressed
                QMessageBox::warning(0, QObject::tr("Error"),
                                     "Called:" + zipToolPath + "\n" +
                                         "Args: " + args.join(" ") + "\n" +
                                         "Exit: " + zipProc->exitCode() + "\n" +
                                         "Err: " + zipProc->getErrout() + "\n" +
                                         "Std: " + zipProc->getStdout() + "\n");
            }
        }
    }
    return err;
}

bool loadStringFromDisk(const QString &fname, QString &s)
{
    s = "";
    QFile file(fname);
    if (!file.open(QFile::ReadOnly | QFile::Text)) {
        qWarning() << QString("loadStringFromDisk: Cannot read file %1\n%2")
                          .arg(fname)
                          .arg(file.errorString());
        return false;
    }

    QTextStream in(&file);
    s = in.readAll();
    return true;
}

bool saveStringToDisk(const QString &fname, const QString &s)
{
    QFile file(fname);
    // Write as binary (default), QFile::Text would convert linebreaks
    if (!file.open(QFile::WriteOnly)) {
        qWarning() << QString("saveStringToDisk: Cannot write file %1:\n%2.")
                          .arg(fname)
                          .arg(file.errorString());
        return false;
    }

    QTextStream out(&file);
    out.setCodec("UTF-8");
    out << s;

    return true;
}

FileType getMapType(const QString &fn)
{
    int i = fn.lastIndexOf(".");
    if (i >= 0) {
        QString postfix = fn.mid(i + 1);
        if (postfix == "vym" || postfix == "vyp" || postfix == "xml" ||
            postfix == "vym~")
            return VymMap;
        if (postfix == "mm")
            return FreemindMap;
    }
    return UnknownMap;
}

ImageIO::ImageIO()
{
    // Create list with supported image types
    // foreach (QByteArray format, QImageWriter::supportedImageFormats())
    // imageTypes.append( tr("%1...").arg(QString(format).toUpper()));
    imageFilters.append(
        "Images (*.png *.jpg *.jpeg *.bmp *.bmp *.ppm *.xpm *.xbm)");
    imageTypes.append("PNG");
    imageFilters.append("Portable Network Graphics (*.png)");
    imageTypes.append("PNG");
    imageFilters.append("Joint Photographic Experts Group (*.jpg)");
    imageTypes.append("JPG");
    imageFilters.append("Joint Photographic Experts Group (*.jpeg)");
    imageTypes.append("JPG");
    imageFilters.append("Windows Bitmap (*.bmp)");
    imageTypes.append("BMP");
    imageFilters.append("Portable Pixmap (*.ppm)");
    imageTypes.append("PPM");
    imageFilters.append("X11 Bitmap (*.xpm)");
    imageTypes.append("XPM");
    imageFilters.append("X11 Bitmap (*.xbm)");
    imageTypes.append("XBM");
}

QStringList ImageIO::getFilters() { return imageFilters; }

QString ImageIO::getType(QString filter)
{
    for (int i = 0; i < imageFilters.count() + 1; i++)
        if (imageFilters.at(i) == filter)
            return imageTypes.at(i);
    return QString();
}

QString ImageIO::guessType(QString fn)
{
    int i = fn.lastIndexOf(".");
    if (i >= 0) {
        QString postfix = fn.mid(i + 1);
        for (int i = 1; i < imageFilters.count(); i++)
            if (imageFilters.at(i).contains(postfix))
                return imageTypes.at(i);
    }
    return QString();
}
