#include "scriptengine.h"
#include "batchprocessing.h"
#include "fitswrapper.h"
#include "libxisf.h"
#include "loadimage.h"
#include "loadrunable.h"
#include "rawimage.h"
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QInputDialog>
#include <QJSValueIterator>
#include <QJsonValue>
#include <fitsio2.h>
#ifdef PLATESOLVER
#include "solver.h"
#endif // PLATESOLVER

namespace Script
{

ScriptEngine::ScriptEngine(Database *database, BatchProcessing *parent)
  : _jsEngine(new QJSEngine(this))
  , _database(database)
  , _parent(parent)
  , _pool(new QThreadPool(this))
{
    QJSValue core = _jsEngine->newQObject(this);
    _jsEngine->globalObject().setProperty("core", core);
    QJSValue fitsRecordObject = _jsEngine->newQMetaObject(&FITSRecordModify::staticMetaObject);
    QJSValue textFile = _jsEngine->newQMetaObject(&TextFile::staticMetaObject);
    _jsEngine->globalObject().setProperty("FITSRecordModify", fitsRecordObject);
    _jsEngine->globalObject().setProperty("TextFile", textFile);
    _semaphore.release(_pool->maxThreadCount());
    _pool->setThreadPriority(QThread::LowPriority);

#ifdef PLATESOLVER
    _solver = new Solver(this);
#endif // PLATESOLVER
}

void ScriptEngine::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir, const QString &arg)
{
    _scriptPath = scriptPath;
    if(!arg.isNull())
        _jsEngine->globalObject().setProperty("scriptarg", arg);
    setPaths(paths);
    _outputDir = outputDir + "/";
}

void ScriptEngine::reportError(const QString &message)
{
    _jsEngine->throwError(message);
}

const QString &ScriptEngine::outputDir() const
{
    return _outputDir;
}

void ScriptEngine::interrupt()
{
#ifdef PLATESOLVER
    if(_solver)_solver->abort();
#endif
    _jsEngine->setInterrupted(true);
}

void ScriptEngine::logError(const QString &message)
{
    emit newMessage(message, true);
}

void ScriptEngine::log(const QString &message)
{
    emit newMessage(message, false);
}

void ScriptEngine::mark(File *file)
{
    QString path = file->absoluteFilePath();
    QMetaObject::invokeMethod(_database, [this, path](){ _database->mark(path); }, Qt::QueuedConnection);
}

void ScriptEngine::unmark(File *file)
{
    QString path = file->absoluteFilePath();
    QMetaObject::invokeMethod(_database, [this, path](){ _database->unmark(path); }, Qt::QueuedConnection);
}

bool ScriptEngine::isMarked(const File *file)
{
    bool ret;
    QString path = file->absoluteFilePath();
    QMetaObject::invokeMethod(_database, [this, path](){ return _database->isMarked(path); }, Qt::BlockingQueuedConnection, &ret);
    return ret;
}

QJSValue ScriptEngine::getObjects(double ra, double dec, double distance)
{
    QVector<SkyObject> objects;
    QMetaObject::invokeMethod(_database, [this, ra, dec, distance](){
        return _database->getObjects(ra - distance, ra + distance, dec - distance, dec + distance); }, Qt::BlockingQueuedConnection, &objects);

    QJSValue ret = newArray(objects.size());
    qint32 i = 0;
    for(auto &object : objects)
    {
        QJSValue jsObj = newObject();
        jsObj.setProperty("name", object.name);
        jsObj.setProperty("name2", object.name2);
        jsObj.setProperty("ra", object.skyPoint.RA());
        jsObj.setProperty("dec", object.skyPoint.DEC());
        jsObj.setProperty("mag", object.mag);
        ret.setProperty(i++, jsObj);
    }

    return ret;
}

QJSValue ScriptEngine::getObjects(const QJSValue &bounds)
{
    QVector<SkyObject> objects;
    double minRa = bounds.property("minRA").toNumber();
    double maxRa = bounds.property("maxRA").toNumber();
    double minDec = bounds.property("minDEC").toNumber();
    double maxDec = bounds.property("maxDEC").toNumber();

    QMetaObject::invokeMethod(_database, [this, minRa, maxRa, minDec, maxDec](){
        return _database->getObjects(minRa, maxRa, minDec, maxDec); }, Qt::BlockingQueuedConnection, &objects);

    QJSValue ret = newArray(objects.size());
    qint32 i = 0;
    for(auto &object : objects)
    {
        QJSValue jsObj = newObject();
        jsObj.setProperty("name", object.name);
        jsObj.setProperty("name2", object.name2);
        jsObj.setProperty("ra", object.skyPoint.RA());
        jsObj.setProperty("dec", object.skyPoint.DEC());
        jsObj.setProperty("mag", object.mag);
        ret.setProperty(i++, jsObj);
    }

    return ret;
}

void ScriptEngine::setMaxThread(int maxthread)
{
    int newval = std::max(std::min(QThread::idealThreadCount(), maxthread), 1);
    int oldval = _pool->maxThreadCount();
    if(newval > oldval)
        _semaphore.release(newval - oldval);
    else if(newval < oldval)
        _semaphore.acquire(oldval - newval);
    _pool->setMaxThreadCount(newval);
}

void ScriptEngine::sync()
{
    _pool->waitForDone();
}

QJSValue ScriptEngine::getString(const QString &label, const QString &text) const
{
    QJSValue ret;
    QMetaObject::invokeMethod(_parent, "getString", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, label), Q_ARG(QString, text));
    return ret;
}

QJSValue ScriptEngine::getInt(const QString &label, int value)
{
    QJSValue ret;
    QMetaObject::invokeMethod(_parent, "getInt", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, label), Q_ARG(int, value));
    return ret;
}

QJSValue ScriptEngine::getFloat(const QString &label, double value, int decimals) const
{
    QJSValue ret;
    QMetaObject::invokeMethod(_parent, "getFloat", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, label), Q_ARG(double, value), Q_ARG(int, decimals));
    return ret;
}

QJSValue ScriptEngine::getItem(const QStringList &items, const QString &label, int current) const
{
    QJSValue ret;
    QMetaObject::invokeMethod(_parent, "getItem", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QStringList, items), Q_ARG(QString, label), Q_ARG(int, current));
    return ret;
}

QJSValue ScriptEngine::question(const QString &question, const QStringList &buttons, const QString &title) const
{
    QJSValue ret;
    QMetaObject::invokeMethod(_parent, "question", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QJSValue, ret), Q_ARG(QString, question), Q_ARG(QStringList, buttons), Q_ARG(QString, title));
    return ret;
}

void ScriptEngine::plot(const QJSValue &graph)
{
    QVariant graphV = graph.toVariant(QJSValue::ConvertJSObjects);
    if(graphV.isValid())
        QMetaObject::invokeMethod(_parent, "plot", Qt::QueuedConnection, Q_ARG(QVariant, graphV));
    else
        logError("Invalid value to be plotted");
}

QJSValue ScriptEngine::openFile(const QString &fileName, const QString &mode)
{
    QFileInfo info(fileName);
    if(!info.isAbsolute())
        info = QFileInfo(outputDir() + fileName);

    TextFile *textFile = new TextFile;
    if(textFile->open(info.absoluteFilePath(), mode))
    {
        return _jsEngine->newQObject(textFile);
    }
    else
    {
        logError("Failed to open file " + fileName);
        delete textFile;
        return false;
    }
}

bool ScriptEngine::convert(File *file, QString &outpath, const QString &format, const QVariantMap &params, bool async)
{
    QString path;
    QDir dir(_outputDir);
    QFileInfo info(outpath);
    QString suffix = info.suffix();
    if(info.isAbsolute())
        path = info.absolutePath();
    else
        path = dir.absoluteFilePath(outpath);

    QString f = format.toLower();
    if(f != "xisf" && f != "fits" && f != "png" && f != "bmp" && f != "jpg" && f != "tiff")
    {
        logError("Output format must be one of xisf fits jpg png bmp tiff");
        return false;
    }

    info.setFile(path);
    outpath = info.absolutePath() + "/" + info.completeBaseName() + "." + f;
    if(async)
    {
        _semaphore.acquire();
        _pool->start(new ConvertRunable(file->absoluteFilePath(), outpath, f, params, &_semaphore));
    }
    else
    {
        ConvertRunable crun(file->absoluteFilePath(), outpath, f, params, nullptr);
        crun.run();
    }
    return true;
}

#ifdef PLATESOLVER
void ScriptEngine::setSolverProfile(int index)
{
    index -= 1;
    if(_solver && index >= SSolver::Parameters::DEFAULT && index < SSolver::Parameters::BIG_STARS)
    {
        _solver->setParameters((SSolver::Parameters::ParametersProfile)index);
    }
}

void ScriptEngine::setSolverProfile(const QVariantMap &profile)
{
    if(_solver)
    {
        SSolver::Parameters params = SSolver::Parameters::convertFromMap(profile);
        _solver->setParameters(params);
    }
}

QJSValue ScriptEngine::getSolverProfile() const
{
    if(_solver)
    {
        QMap<QString, QVariant> params = SSolver::Parameters::convertToMap(_solver->getProfile());
        QJSValue ret = _jsEngine->newObject();
        for(auto i = params.begin(); i != params.end(); i++)
        {
            switch(i.value().metaType().id())
            {
            case QMetaType::Int:
                ret.setProperty(i.key(), i.value().toInt());
                break;
            case QMetaType::Double:
                ret.setProperty(i.key(), i.value().toDouble());
                break;
            case QMetaType::Bool:
                ret.setProperty(i.key(), i.value().toBool());
                break;
            case QMetaType::QString:
                ret.setProperty(i.key(), i.value().toString());
                break;
            default:
                qWarning() << "unhandled metatype" << i.key() << i.value();
                break;
            }
        }
        return ret;
    }
    else
    {
        return QJSValue();
    }
}

void ScriptEngine::setStartingSolution(const QJSValue &solution)
{
    if(solution.isObject())
    {
        if(solution.hasProperty("ra") && solution.hasProperty("dec") && solution.property("ra").isNumber() && solution.property("dec").isNumber())
            _solver->setSearchPosition(solution.property("ra").toNumber() / 15.0, solution.property("dec").toNumber());

        if(solution.hasProperty("pixscale") && solution.property("pixscale").isNumber())
        {
            double scale = solution.property("pixscale").toNumber();
            _solver->setSearchScale(scale * 0.8, scale * 1.2, SSolver::ScaleUnits::ARCSEC_PER_PIX);
        }
    }
    else
    {
        _solver->clearStartingPositionAndScale();
    }
}

QJSValue ScriptEngine::solveImage(File *file, bool updateHeader)
{
    QString path = file->absoluteFilePath();
    QJSValue ret = newObject();

    if(_solver->loadImage(path))
    {
        if(_solver->solveImage(true))
        {
            auto solution = _solver->getSolution();
            ret.setProperty("fieldWidth", solution.fieldWidth);
            ret.setProperty("fieldHeight", solution.fieldHeight);
            ret.setProperty("ra", solution.ra);
            ret.setProperty("dec", solution.dec);
            ret.setProperty("orientation", solution.orientation);
            ret.setProperty("pixscale", solution.pixscale);
            ret.setProperty("parity", solution.parity == FITSImage::Parity::POSITIVE);
            ret.setProperty("raError", solution.raError);
            ret.setProperty("decError", solution.decError);
            if(updateHeader)
            {
                QString error;
                if(!_solver->updateHeader(error))
                    logError(error);
            }
        }
        else
        {
            logError("Failed to plate solve image " + path);
        }
    }
    else
    {
        logError("Failed to load image " + path);
    }
    return ret;
}

QJSValue ScriptEngine::extractStars(File *file, bool hfr)
{
    QJSValue ret;
    QString path = file->absoluteFilePath();
    if(_solver->loadImage(path))
    {
        if(_solver->extractSources(hfr, true))
        {
            auto stars = _solver->getStars();
            ret = newArray(stars.size());
            int i = 0;
            for(auto &star : stars)
            {
                QJSValue starj = newObject();
                starj.setProperty("x", star.x);
                starj.setProperty("y", star.y);
                starj.setProperty("mag", star.mag);
                starj.setProperty("flux", star.flux);
                starj.setProperty("peak", star.peak);
                starj.setProperty("HFR", star.HFR);
                starj.setProperty("a", star.a);
                starj.setProperty("b", star.b);
                starj.setProperty("theta", star.theta);
                starj.setProperty("ra", star.ra);
                starj.setProperty("dec", star.dec);
                starj.setProperty("numPixels", star.numPixels);
                ret.setProperty(i++, starj);
            }
        }
        else
        {
            logError("Failed to extract sources from " + path);
        }
    }
    else
    {
        logError("Failed to load image " + path);
    }

    return ret;
}
#endif // PLATESOLVER

QJSValue ScriptEngine::newObject()
{
    return _jsEngine->newObject();
}

QJSValue ScriptEngine::newArray(uint size)
{
    return _jsEngine->newArray(size);
}

QJSValue ScriptEngine::eval(const QString &program)
{
    QStringList stackTrace;
    QJSValue result = _jsEngine->evaluate(program, QString(), 1, &stackTrace);

    if(result.isError())
    {
        QString error = result.property("name").toString() + " on line " + result.property("lineNumber").toString() + " : " + result.toString();
        error += "\n" + result.property("stack").toString();
        emit newMessage(error, true);
    }
    else if(!result.isUndefined())
    {
        emit newMessage(result.toString(), false);
    }

    return result;
}

QStringList ScriptEngine::complete(const QString &line)
{
    QStringList complete;

    QJSValue globObj = _jsEngine->globalObject();

    QRegularExpression reg("[a-zA-Z_][a-zA-Z0-9_]*");
    auto match = reg.match(line);
    if(match.hasMatch())
    {
        QString var = match.captured();
        if(globObj.hasProperty(var))
        {
            complete.clear();
            QJSValueIterator it(globObj.property(var));
            while(it.hasNext())
            {
                it.next();
                if(it.name() != "constructor" && it.name() != "objectNameChanged")
                    complete.append(var + "." + it.name());
            }
        }
    }
    else
    {
        QJSValueIterator it(globObj);
        while(it.hasNext())
        {
            it.next();
            complete.append(it.name());
        }
    }

    return complete;
}

void ScriptEngine::setPaths(const QList<QPair<QString, QString> > &paths)
{
    _paths = paths;
    QJSValue jsPaths = _jsEngine->newArray(paths.size());
    for(qsizetype i=0; i<paths.size(); i++)
        jsPaths.setProperty(i, _jsEngine->newQObject(new File(paths[i].first, paths[i].second, this)));

    _jsEngine->globalObject().setProperty("files", jsPaths);
}

void ScriptEngine::run()
{
    QFile scriptFile(_scriptPath);
    if(!scriptFile.open(QIODevice::ReadOnly))
    {
        emit newMessage("Failed to open " + _scriptPath, true);
        emit finished();
        return;
    }

    QTextStream stream(&scriptFile);
    QString contents = stream.readAll();
    scriptFile.close();
    QJSValue result = _jsEngine->evaluate(contents, _scriptPath);
    qDebug() << result.isError() << result.toString();
    _pool->waitForDone();
    if(result.isError())
    {
        QString error = result.property("name").toString() + " on line " + result.property("lineNumber").toString() + " : " + result.toString();
        error += "\n" + result.property("stack").toString();
        emit newMessage(error, true);
    }

    emit finished();
}

void File::loadFitsKeywords()
{
    if(!_fitsKeywordsLoaded)
    {
        _fitsKeywordsLoaded = true;
        ImageInfoData info;
        if(isXISF(suffix()))
        {
            readXISFHeader(_path, info);
        }
        else if(isFITS(suffix()))
        {
            readFITSHeader(_path, info);
        }
        else return;

        _wcs = info.wcs;

        for(auto &record : info.fitsHeader)
        {
            _fitsKeywords.append(record.key);
            _fitsRecords.insert(record.key, record);
        }
    }
}

bool File::mkpath(const QString &path) const
{
    QFileInfo info(path);
    if(!info.isRelative())
    {
        _engine->logError("Destination path is not relative");
        return false;
    }
    QDir dir(_engine->outputDir());
    if(dir.mkpath(info.path()))
    {
        return true;
    }
    else
    {
        _engine->logError("Failed to create dir " + info.path());
        return false;
    }
}

File::File(const QString &path, Script::ScriptEngine *engine) : File(path, QString(), engine)
{
}

File::File(const QString &path, const QString &root, ScriptEngine *engine) :
    _engine(engine),
    _path(path),
    _root(root),
    _info(path)
{
}

QString File::fileName() const
{
    return _info.fileName();
}

QString File::absoluteFilePath() const
{
    return _info.absoluteFilePath();
}

QString File::absolutePath() const
{
    return _info.absolutePath();
}

QString File::relativeFilePath() const
{
    QDir dir(_root);
    return dir.relativeFilePath(_info.absoluteFilePath());
}

QString File::relativePath() const
{
    QDir dir(_root);
    return dir.relativeFilePath(_info.absolutePath());
}

QString File::baseName() const
{
    return _info.baseName();
}

QString File::completeBaseName() const
{
    return _info.completeBaseName();
}

QString File::suffix() const
{
    return _info.suffix();
}

qint64 File::size() const
{
    return _info.size();
}

QStringList File::fitsKeywords()
{
    loadFitsKeywords();
    return _fitsKeywords;
}

QString File::fitsValue(const QString &key)
{
    loadFitsKeywords();
    if(_fitsRecords.contains(key))
        return _fitsRecords[key].value.toString();
    else
        return QString();
}

QJSValue File::fitsValues(const QString &key)
{
    loadFitsKeywords();
    if(_fitsRecords.contains(key))
    {
        QList<FITSRecord> values =  _fitsRecords.values(key);
        QJSValue array = _engine->newArray(values.size());
        for(qsizetype i=0; i<values.size(); i++)
            array.setProperty(i, values[i].value.toString());
        return array;
    }
    else
        return QString();
}

QJSValue File::fitsRecords()
{
    loadFitsKeywords();

    QJSValue array = _engine->newArray(_fitsRecords.size());
    uint i = 0;
    for(auto &record : _fitsRecords)
    {
        QJSValue item = _engine->newObject();
        item.setProperty("key", QString::fromUtf8(record.key));
        item.setProperty("value", record.value.toString());
        item.setProperty("comment", QString::fromUtf8(record.comment));
        item.setProperty("xisf", record.xisf);
        array.setProperty(i++, item);
    }

    return array;
}

bool File::modifyFITSRecords(const FITSRecordModify *modify)
{
    _fitsKeywordsLoaded = false;
    _fitsKeywords.clear();

    if(isFITS(suffix()))
    {
        int status = 0;
        QString path = makeUNCPath(_path);
        FITSWrapper file(path, READWRITE, true);
        status = file.status();

        int num = 0;
        fits_get_num_hdus(file, &num, &status);
        if(status)
        {
            if(_engine)_engine->newMessage("Failed to open FITS file", true);
            return false;
        }
        int imgtype;
        int naxis;
        long naxes[3] = {0};
        int type = -1;
        std::vector<int> imageIdxs;
        for(int i=1; i <= num; i++)
        {
            fits_movabs_hdu(file, i, IMAGE_HDU, &status);
            fits_get_hdu_type(file, &type, &status);
            if(type == IMAGE_HDU)
            {
                fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);
                if(naxis >= 2 && naxis <= 3)imageIdxs.push_back(i);
            }
        }

        if(modify->_imageIdx >= imageIdxs.size())return false;
        fits_movabs_hdu(file, imageIdxs[modify->_imageIdx], &type, &status);
        fits_get_img_param(file, 3, &imgtype, &naxis, naxes, &status);

        for(auto &remove : modify->_remove)
        {
            int status = 0;//we ignore errors from here
            fits_delete_key(file, remove.toLatin1().data(), &status);
        }
        for(auto &record : modify->_update)
        {
            switch(record.value.typeId())
            {
            case QMetaType::Bool:
            {
                int val = record.value.toBool();
                fits_update_key(file, TLOGICAL, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
                break;
            }
            case QMetaType::Int:
            case QMetaType::UInt:
            {
                long long val = record.value.toLongLong();
                fits_update_key(file, TLONGLONG, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
                break;
            }
            case QMetaType::QString:
            {
                QByteArray val = record.value.toString().toLatin1();
                fits_update_key(file, TSTRING, record.key.data(), val.isEmpty() ? nullptr : val.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
                break;
            }
            case QMetaType::Float:
            case QMetaType::Double:
            {
                double val = record.value.toDouble();
                fits_update_key(file, TDOUBLE, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
                break;
            }
            default:
                if(_engine)_engine->newMessage("Unknown type for KEY " + record.key, true);
                return false;
                break;
            }
            if(status)
            {
                char error[100];
                fits_get_errstatus(status, error);
                if(_engine)_engine->newMessage(QString("Error when updating KEY %1 %2").arg(record.key).arg(error), true);
                return false;
            }
        }

        for(auto &record : modify->_add)
        {
            switch(record.value.typeId())
            {
            case QMetaType::Bool:
            {
                int val = record.value.toBool();
                fits_write_key(file, TLOGICAL, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
                break;
            }
            case QMetaType::Int:
            case QMetaType::UInt:
            {
                long long val = record.value.toLongLong();
                fits_write_key(file, TLONGLONG, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
                break;
            }
            case QMetaType::QString:
            {
                QByteArray val = record.value.toString().toLatin1();
                fits_write_key(file, TSTRING, record.key.data(), val.isEmpty() ? nullptr : val.data(), record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
                break;
            }
            case QMetaType::Float:
            case QMetaType::Double:
            {
                double val = record.value.toDouble();
                fits_write_key(file, TDOUBLE, record.key.data(), &val, record.comment.isEmpty() ? nullptr : record.comment.data(), &status);
                break;
            }
            default:
                if(_engine)_engine->newMessage("Unknown type for KEY " + record.key, true);
                return false;
                break;
            }
            if(status)
            {
                char error[100];
                fits_get_errstatus(status, error);
                if(_engine)_engine->newMessage(QString("Error when adding KEY {} {}").arg(record.key).arg(error), true);
                return false;
            }
        }
        if(status)qWarning() << "Failed to modify FITS header" << status;
        return status == 0;
    }
    else if(isXISF(suffix()))
    {
        try
        {
            LibXISF::XISFModify modifyXISF;
            QFileInfo in(makeUNCPath(absoluteFilePath()));
            QFileInfo out(in.absoluteFilePath() + "~");
            modifyXISF.open(in.filesystemFilePath());
            qDebug() << "modify" << in << out;

            for(auto &remove : modify->_remove)
                modifyXISF.removeFITSKeyword(modify->_imageIdx, remove.toStdString());

            for(auto &record : modify->_update)
                modifyXISF.updateFITSKeyword(modify->_imageIdx, {record.key.toStdString(), record.value.toString().toStdString(), record.comment.toStdString()}, true);

            for(auto &record : modify->_add)
                modifyXISF.addFITSKeyword(modify->_imageIdx, {record.key.toStdString(), record.value.toString().toStdString(), record.comment.toStdString()});

            for(auto &property : modify->_property)
                modifyXISF.updateProperty(modify->_imageIdx, property);

            modifyXISF.save(out.filesystemFilePath());
            modifyXISF.close();
            std::filesystem::rename(out.filesystemFilePath(), in.filesystemFilePath());
            return true;
        }
        catch(std::filesystem::filesystem_error &err)
        {
            if(_engine)_engine->newMessage("Failed to modify file " + _path + " " + err.what(), true);
            return false;
        }
        catch(LibXISF::Error &err)
        {
            if(_engine)_engine->newMessage("Failed to modify file " + _path + " " + err.what(), true);
            return false;
        }
    }

    return false;
}

bool File::isMarked() const
{
    return _engine->isMarked(this);
}

File* File::copy(const QString &newpath) const
{
    if(mkpath(newpath))
    {
        if(QFile::copy(_path, _engine->outputDir() + newpath))
            return new File(_engine->outputDir() + newpath, _engine);
        _engine->logError("Failed copy to " + newpath);
        return nullptr;
    }
    return nullptr;
}

bool File::move(const QString &newpath)
{
    if(mkpath(newpath))
    {
        if(QFile::rename(_path, _engine->outputDir() + newpath))
        {
            _path = _engine->outputDir() + newpath;
            return true;
        }
        _engine->logError("Failed move to " + newpath);
        return false;
    }
    return false;
}

File* File::convert(const QString &outpath, const QString &format, const QVariantMap &params)
{
    QString path = outpath;
    if(_engine->convert(this, path, format, params, false))
        return new File(path, _engine);
    else
        return nullptr;
}

File* File::convertAsync(const QString &outpath, const QString &format, const QVariantMap &params)
{
    QString path = outpath;
    if(_engine->convert(this, path, format, params, true))
        return new File(path, _engine);
    else
        return nullptr;
}

QJSValue File::stats()
{
    if(_stats.isUndefined())
    {
        ImageInfoData info;
        std::shared_ptr<RawImage> rawImage;
        loadImage(_path, info, rawImage, 0);
        rawImage->calcStats();
        RawImage::Stats stats = rawImage->imageStats();
        _stats = _engine->newObject();
        _stats.setProperty("mean", stats.m_mean[0]);
        _stats.setProperty("stddev", stats.m_stdDev[0]);
        _stats.setProperty("median", stats.m_median[0]);
        _stats.setProperty("min", stats.m_min[0]);
        _stats.setProperty("max", stats.m_max[0]);
        _stats.setProperty("mad", stats.m_mean[0]);
    }
    return _stats;
}

QJSValue File::calculatedBounds()
{
    QJSValue ret = _engine->newObject();
    loadFitsKeywords();
    if(_wcs)
    {
        double minRa, maxRa, minDec, maxDec, crVal1, crVal2;
        _wcs->calculateBounds(minRa, maxRa, minDec, maxDec, crVal1, crVal2);
        ret.setProperty("minRA", minRa);
        ret.setProperty("maxRA", maxRa);
        ret.setProperty("minDEC", minDec);
        ret.setProperty("maxDEC", maxDec);
        ret.setProperty("crVal1", crVal1);
        ret.setProperty("crVal2", crVal2);
    }
    return ret;
}

#ifdef PLATESOLVER
QJSValue File::solve(bool updateHeader)
{
    if(_solution.isUndefined() || updateHeader)
        _solution = _engine->solveImage(this, updateHeader);

    return _solution;
}

QJSValue File::extractStars(bool hfr)
{
    if(_stars.isUndefined())
        _stars = _engine->extractStars(this, hfr);

    return _stars;
}
#endif // PLATESOLVER

ScriptEngineThread::ScriptEngineThread(Database *database, BatchProcessing *parent) : QObject(parent)
{
    _thread = new QThread();
    _thread->setObjectName("ScriptEngine");
    _engine = new ScriptEngine(database, parent);
    _engine->moveToThread(_thread);
    connect(_engine, &ScriptEngine::finished, _thread, &QThread::quit);
    connect(_engine, &ScriptEngine::newMessage, this, &ScriptEngineThread::newMessage);
    connect(_thread, &QThread::started, _engine, &ScriptEngine::run);
    connect(_thread, &QThread::finished, _engine, &ScriptEngine::deleteLater);
    connect(_engine, &ScriptEngine::destroyed, [this](){ _engine = nullptr; });
    connect(_thread, &QThread::finished, _thread, &QThread::deleteLater);
    connect(_thread, &QThread::finished, this, &ScriptEngineThread::finished);
}

ScriptEngineThread::~ScriptEngineThread()
{
    if(_engine)_engine->interrupt();
}

void ScriptEngineThread::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir, const QString &arg)
{
    _engine->setParams(scriptPath, paths, outputDir, arg);
}

void ScriptEngineThread::start()
{
    _thread->start();
}

void ScriptEngineThread::interrupt()
{
    if(_engine)_engine->interrupt();
}

void FITSRecordModify::removeKeyword(const QString &key)
{
    if(!_remove.contains(key))
        _remove.append(key);
}

void FITSRecordModify::updateKeyword(const QString &key, const QVariant &value, const QString &comment)
{
    _update.append({key.toLatin1(), value, comment.toLatin1()});
}

void FITSRecordModify::addKeyword(const QString &key, const QVariant &value, const QString &comment)
{
    _update.append({key.toLatin1(), value, comment.toLatin1()});
}

void FITSRecordModify::updateProperty(const QString &id, const LibXISF::Variant &value)
{
    _property.append(LibXISF::Property(id.toStdString(), value));
}

uint32_t FITSRecordModify::imageIndex() const
{
    return _imageIdx;
}

void FITSRecordModify::setImageIndex(uint32_t idx)
{
    _imageIdx = idx;
}

bool TextFile::open(const QString &path, const QString &mode)
{
    _fr.setFileName(path);
    QIODevice::OpenMode openMode;
    if(mode == "r")
        openMode = QIODevice::ReadOnly;
    else if(mode == "w")
        openMode = QIODevice::WriteOnly;
    else if(mode == "a")
        openMode = QIODevice::WriteOnly | QIODevice::Append;
    else if(mode == "r+")
        openMode = QIODevice::ReadWrite | QIODevice::ExistingOnly;
    else if(mode == "w+")
        openMode = QIODevice::ReadWrite;
    else if(mode == "a+")
        openMode = QIODevice::ReadWrite | QIODevice::Append;
    else
        return false;

    openMode |= QIODevice::Text;//always open as text
    return _fr.open(openMode);
}

void TextFile::write(const QString &data)
{
    _fr.write(data.toUtf8());
}

QString TextFile::read(int maxlen)
{
    QByteArray data = _fr.read(maxlen);
    return data;
}

QString TextFile::readLine()
{
    QByteArray data = _fr.readLine();
    return QString::fromUtf8(data);
}

QString TextFile::readAll()
{
    QByteArray data = _fr.readAll();
    return QString::fromUtf8(data);
}

bool TextFile::seek(qint64 offset)
{
    return _fr.seek(offset);
}

qint64 TextFile::pos()
{
    return _fr.pos();
}

}
