/***************************************************************************
                          kstarsdata.cpp  -  K Desktop Planetarium
                             -------------------
    begin                : Sun Jul 29 2001
    copyright            : (C) 2001 by Heiko Evermann
    email                : heiko@evermann.de
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/

#include "kstarsdata.h"

#include "ksutils.h"
#include "Options.h"
#include "auxiliary/kspaths.h"
#include "skycomponents/supernovaecomponent.h"
#include "skycomponents/skymapcomposite.h"
#ifndef KSTARS_LITE
#include "fov.h"
#include "imageexporter.h"
#include "kstars.h"
#include "observinglist.h"
#include "skymap.h"
#include "dialogs/detaildialog.h"
#include "oal/execute.h"
#endif

#ifndef KSTARS_LITE
#include <KMessageBox>
#endif

#include <QSqlQuery>
#include <QSqlRecord>
#include <QtConcurrent>

#include "kstars_debug.h"

namespace
{
// Report fatal error during data loading to user
// Calls QApplication::exit
void fatalErrorMessage(QString fname)
{
#ifndef KSTARS_LITE

    KMessageBox::sorry(nullptr,
                       i18n("The file  %1 could not be found. "
                            "KStars cannot run properly without this file. "
                            "KStars searches for this file in following locations:\n\n\t"
                            "%2\n\n"
                            "It appears that your setup is broken.",
                            fname, QStandardPaths::standardLocations(QStandardPaths::DataLocation).join("\n\t")),
                       i18n("Critical File Not Found: %1", fname)); // FIXME: Must list locations depending on file type
#endif
    qDebug() << i18n("Critical File Not Found: %1", fname);
    qApp->exit(1);
}

// Report non-fatal error during data loading to user and ask
// whether he wants to continue.
// Calls QApplication::exit if he don't
#if 1
bool nonFatalErrorMessage(QString fname)
{
#ifdef KSTARS_LITE
    Q_UNUSED(fname)
#else
    int res = KMessageBox::warningContinueCancel(0,
              i18n("The file %1 could not be found. "
                   "KStars can still run without this file. "
                   "KStars search for this file in following locations:\n\n\t"
                   "%2\n\n"
                   "It appears that you setup is broken. Press Continue to run KStars without this file ",
                   fname, QStandardPaths::standardLocations( QStandardPaths::DataLocation ).join("\n\t") ),
              i18n( "Non-Critical File Not Found: %1", fname ));  // FIXME: Must list locations depending on file type
    if( res != KMessageBox::Continue )
        qApp->exit(1);
    return res == KMessageBox::Continue;
#endif
    return true;
}
#endif
}

KStarsData *KStarsData::pinstance = nullptr;

KStarsData *KStarsData::Create()
{
    // This method should never be called twice within a run, since a
    // lot of the code assumes that KStarsData, once created, is never
    // destroyed. They maintain local copies of KStarsData::Instance()
    // for efficiency (maybe this should change, but it is not
    // required to delete and reinstantiate KStarsData). Thus, when we
    // call this method, pinstance MUST be zero, i.e. this must be the
    // first (and last) time we are calling it. -- asimha
    Q_ASSERT(!pinstance);

    delete pinstance;
    pinstance = new KStarsData();
    return pinstance;
}

KStarsData::KStarsData()
    : m_Geo(dms(0), dms(0)), m_ksuserdb(), m_catalogdb(),
      temporaryTrail(false),
      //locale( new KLocale( "kstars" ) ),
      m_preUpdateID(0), m_updateID(0), m_preUpdateNumID(0), m_updateNumID(0), m_preUpdateNum(J2000), m_updateNum(J2000)
{
#ifndef KSTARS_LITE
    m_LogObject.reset(new OAL::Log);
#endif
    // at startup times run forward
    setTimeDirection(0.0);
}

KStarsData::~KStarsData()
{
    Q_ASSERT(pinstance);

    //delete locale;
    qDeleteAll(geoList);
    geoList.clear();
    qDeleteAll(ADVtreeList);
    ADVtreeList.clear();

    pinstance = nullptr;
}



bool KStarsData::initialize()
{
    //Initialize CatalogDB//
    catalogdb()->Initialize();

    //Load Time Zone Rules//
    emit progressText(i18n("Reading time zone rules"));
    if (!readTimeZoneRulebook())
    {
        fatalErrorMessage("TZrules.dat");
        return false;
    }


    emit progressText(i18n("Upgrade existing user city db to support geographic elevation."));


    QString dbfile = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QDir::separator() + "mycitydb.sqlite";

    /// This code to add Height column to table city in mycitydb.sqlite is a transitional measure to support a meaningful
    /// geographic elevation.
    if (QFile::exists(dbfile))
    {
        QSqlDatabase fixcitydb = QSqlDatabase::addDatabase("QSQLITE", "fixcitydb");

        fixcitydb.setDatabaseName(dbfile);
        fixcitydb.open();

        if (fixcitydb.tables().contains("city", Qt::CaseInsensitive))
        {
            QSqlRecord r = fixcitydb.record("city");
            if (!r.contains("Elevation"))
            {
                emit progressText(i18n("Adding \"Elevation\" column to city table."));

                QSqlQuery query(fixcitydb);
                if (query.exec("alter table city add column Elevation real default -10;") == false)
                {
                    emit progressText(QString("failed to add Elevation column to city table in mycitydb.sqlite: &1").arg(query.lastError().text()));
                }
            }
            else
            {
                emit progressText(i18n("City table already contains \"Elevation\"."));
            }
        }
        else
        {
            emit progressText(i18n("City table missing from database."));
        }
        fixcitydb.close();
    }


    //Load Cities//
    emit progressText(i18n("Loading city data"));
    if (!readCityData())
    {
        fatalErrorMessage("citydb.sqlite");
        return false;
    }

    //Initialize User Database//
    emit progressText(i18n("Loading User Information"));
    m_ksuserdb.Initialize();

    //Initialize SkyMapComposite//
    emit progressText(i18n("Loading sky objects"));
    m_SkyComposite.reset(new SkyMapComposite());
    //Load Image URLs//
    //#ifndef Q_OS_ANDROID
    //On Android these 2 calls produce segfault. WARNING
    emit progressText(i18n("Loading Image URLs"));

    if( !readURLData( "image_url.dat", 0 ) && !nonFatalErrorMessage( "image_url.dat" ) )
        return false;
    //QtConcurrent::run(this, &KStarsData::readURLData, QString("image_url.dat"), 0, false);

    //Load Information URLs//
    //emit progressText(i18n("Loading Information URLs"));
    if( !readURLData( "info_url.dat", 1 ) && !nonFatalErrorMessage( "info_url.dat" ) )
        return false;
    QtConcurrent::run(this, &KStarsData::readURLData, QString("info_url.dat"), 1, false);

    //#endif
    //emit progressText( i18n("Loading Variable Stars" ) );

#ifndef KSTARS_LITE
    //Initialize Observing List
    m_ObservingList = new ObservingList();
#endif

    readUserLog();

#ifndef KSTARS_LITE
    readADVTreeData();
#endif
    return true;
}

void KStarsData::updateTime(GeoLocation *geo, const bool automaticDSTchange)
{
    // sync LTime with the simulation clock
    LTime = geo->UTtoLT(ut());
    syncLST();

    //Only check DST if (1) TZrule is not the empty rule, and (2) if we have crossed
    //the DST change date/time.
    if (!geo->tzrule()->isEmptyRule())
    {
        if (TimeRunsForward)
        {
            // timedirection is forward
            // DST change happens if current date is bigger than next calculated dst change
            if (ut() > NextDSTChange)
                resetToNewDST(geo, automaticDSTchange);
        }
        else
        {
            // timedirection is backward
            // DST change happens if current date is smaller than next calculated dst change
            if (ut() < NextDSTChange)
                resetToNewDST(geo, automaticDSTchange);
        }
    }

    KSNumbers num(ut().djd());

    if (std::abs(ut().djd() - LastNumUpdate.djd()) > 1.0)
    {
        LastNumUpdate = KStarsDateTime(ut().djd());
        m_preUpdateNumID++;
        m_preUpdateNum = KSNumbers(num);
        skyComposite()->update(&num);
    }

    if (std::abs(ut().djd() - LastPlanetUpdate.djd()) > 0.01)
    {
        LastPlanetUpdate = KStarsDateTime(ut().djd());
        skyComposite()->updateSolarSystemBodies(&num);
    }

    // Moon moves ~30 arcmin/hr, so update its position every minute.
    if (std::abs(ut().djd() - LastMoonUpdate.djd()) > 0.00069444)
    {
        LastMoonUpdate = ut();
        skyComposite()->updateMoons(&num);
    }

    //Update Alt/Az coordinates.  Timescale varies with zoom level
    //If Clock is in Manual Mode, always update. (?)
    if (std::abs(ut().djd() - LastSkyUpdate.djd()) > 0.1 / Options::zoomFactor() || clock()->isManualMode())
    {
        LastSkyUpdate = ut();
        m_preUpdateID++;
        //omit KSNumbers arg == just update Alt/Az coords // <-- Eh? -- asimha. Looks like this behavior / ideology has changed drastically.
        skyComposite()->update(&num);

        emit skyUpdate(clock()->isManualMode());
    }
}

void KStarsData::syncUpdateIDs()
{
    m_updateID = m_preUpdateID;
    if (m_updateNumID == m_preUpdateNumID)
        return;
    m_updateNumID = m_preUpdateNumID;
    m_updateNum   = KSNumbers(m_preUpdateNum);
}

unsigned int KStarsData::incUpdateID()
{
    m_preUpdateID++;
    m_preUpdateNumID++;
    syncUpdateIDs();
    return m_updateID;
}

void KStarsData::setFullTimeUpdate()
{
    //Set the update markers to invalid dates to trigger updates in each category
    LastSkyUpdate    = KStarsDateTime(QDateTime());
    LastPlanetUpdate = KStarsDateTime(QDateTime());
    LastMoonUpdate   = KStarsDateTime(QDateTime());
    LastNumUpdate    = KStarsDateTime(QDateTime());
}

void KStarsData::syncLST()
{
    LST = geo()->GSTtoLST(ut().gst());
}

void KStarsData::changeDateTime(const KStarsDateTime &newDate)
{
    //Turn off animated slews for the next time step.
    setSnapNextFocus();

    clock()->setUTC(newDate);

    LTime = geo()->UTtoLT(ut());
    //set local sideral time
    syncLST();

    //Make sure Numbers, Moon, planets, and sky objects are updated immediately
    setFullTimeUpdate();

    // reset tzrules data with new local time and time direction (forward or backward)
    geo()->tzrule()->reset_with_ltime(LTime, geo()->TZ0(), isTimeRunningForward());

    // reset next dst change time
    setNextDSTChange(geo()->tzrule()->nextDSTChange());
}

void KStarsData::resetToNewDST(GeoLocation *geo, const bool automaticDSTchange)
{
    // reset tzrules data with local time, timezone offset and time direction (forward or backward)
    // force a DST change with option true for 3. parameter
    geo->tzrule()->reset_with_ltime(LTime, geo->TZ0(), TimeRunsForward, automaticDSTchange);
    // reset next DST change time
    setNextDSTChange(geo->tzrule()->nextDSTChange());
    //reset LTime, because TZoffset has changed
    LTime = geo->UTtoLT(ut());
}

void KStarsData::setTimeDirection(float scale)
{
    TimeRunsForward = scale >= 0;
}

GeoLocation *KStarsData::locationNamed(const QString &city, const QString &province, const QString &country)
{
    foreach (GeoLocation *loc, geoList)
    {
        if (loc->translatedName() == city && (province.isEmpty() || loc->translatedProvince() == province) &&
                (country.isEmpty() || loc->translatedCountry() == country))
        {
            return loc;
        }
    }
    return nullptr;
}

GeoLocation *KStarsData::nearestLocation(double longitude, double latitude)
{
    GeoLocation *nearest = nullptr;
    double distance = 1e6;

    dms lng(longitude), lat(latitude);
    for (auto oneCity : geoList)
    {
        double newDistance = oneCity->distanceTo(lng, lat);
        if (newDistance < distance)
        {
            distance = newDistance;
            nearest = oneCity;
        }
    }

    return nearest;
}

void KStarsData::setLocationFromOptions()
{
    setLocation(GeoLocation(dms(Options::longitude()), dms(Options::latitude()), Options::cityName(),
                            Options::provinceName(), Options::countryName(), Options::timeZone(),
                            &(Rulebook[Options::dST()]), Options::elevation(), false, 4));
}

void KStarsData::setLocation(const GeoLocation &l)
{
    m_Geo = GeoLocation(l);
    if (m_Geo.lat()->Degrees() >= 90.0)
        m_Geo.setLat(dms(89.99));
    if (m_Geo.lat()->Degrees() <= -90.0)
        m_Geo.setLat(dms(-89.99));

    //store data in the Options objects
    Options::setCityName(m_Geo.name());
    Options::setProvinceName(m_Geo.province());
    Options::setCountryName(m_Geo.country());
    Options::setTimeZone(m_Geo.TZ0());
    Options::setElevation(m_Geo.elevation());
    Options::setLongitude(m_Geo.lng()->Degrees());
    Options::setLatitude(m_Geo.lat()->Degrees());
    // set the rule from rulebook
    foreach (const QString &key, Rulebook.keys())
    {
        if (!key.isEmpty() && m_Geo.tzrule()->equals(&Rulebook[key]))
            Options::setDST(key);
    }

    emit geoChanged();
}

SkyObject *KStarsData::objectNamed(const QString &name)
{
    if ((name == "star") || (name == "nothing") || name.isEmpty())
        return nullptr;
    return skyComposite()->findByName(name);
}

bool KStarsData::readCityData()
{
    QSqlDatabase citydb = QSqlDatabase::addDatabase("QSQLITE", "citydb");
    QString dbfile      = KSPaths::locate(QStandardPaths::GenericDataLocation, "citydb.sqlite");
    citydb.setDatabaseName(dbfile);
    if (citydb.open() == false)
    {
        qCCritical(KSTARS) << "Unable to open city database file " << dbfile << citydb.lastError().text();
        return false;
    }

    QSqlQuery get_query(citydb);

    //get_query.prepare("SELECT * FROM city");
    if (!get_query.exec("SELECT * FROM city"))
    {
        qCCritical(KSTARS) << get_query.lastError();
        return false;
    }

    bool citiesFound = false;
    // get_query.size() always returns -1 so we set citiesFound if at least one city is found
    while (get_query.next())
    {
        citiesFound          = true;
        QString name         = get_query.value(1).toString();
        QString province     = get_query.value(2).toString();
        QString country      = get_query.value(3).toString();
        dms lat              = dms(get_query.value(4).toString());
        dms lng              = dms(get_query.value(5).toString());
        double TZ            = get_query.value(6).toDouble();
        TimeZoneRule *TZrule = &(Rulebook[get_query.value(7).toString()]);
        double elevation     = get_query.value(8).toDouble();

        // appends city names to list
        geoList.append(new GeoLocation(lng, lat, name, province, country, TZ, TZrule, elevation, true, 4));
    }
    citydb.close();

    // Reading local database
    QSqlDatabase mycitydb = QSqlDatabase::addDatabase("QSQLITE", "mycitydb");
    dbfile = KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + QDir::separator() + "mycitydb.sqlite";

    if (QFile::exists(dbfile))
    {
        mycitydb.setDatabaseName(dbfile);
        if (mycitydb.open())
        {
            QSqlQuery get_query(mycitydb);

            if (!get_query.exec("SELECT * FROM city"))
            {
                qDebug() << get_query.lastError();
                return false;
            }
            while (get_query.next())
            {
                QString name         = get_query.value(1).toString();
                QString province     = get_query.value(2).toString();
                QString country      = get_query.value(3).toString();
                dms lat              = dms(get_query.value(4).toString());
                dms lng              = dms(get_query.value(5).toString());
                double TZ            = get_query.value(6).toDouble();
                TimeZoneRule *TZrule = &(Rulebook[get_query.value(7).toString()]);
                double elevation     = get_query.value(8).toDouble();

                // appends city names to list
                geoList.append(new GeoLocation(lng, lat, name, province, country, TZ, TZrule, elevation, false, 4));
            }
            mycitydb.close();
        }
    }

    return citiesFound;
}

bool KStarsData::readTimeZoneRulebook()
{
    QFile file;

    if (KSUtils::openDataFile(file, "TZrules.dat"))
    {
        QTextStream stream(&file);

        while (!stream.atEnd())
        {
            QString line = stream.readLine().trimmed();
            if (line.length() && !line.startsWith('#')) //ignore commented and blank lines
            {
                QStringList fields = line.split(' ', QString::SkipEmptyParts);
                QString id         = fields[0];
                QTime stime        = QTime(fields[3].leftRef(fields[3].indexOf(':')).toInt(),
                                           fields[3].midRef(fields[3].indexOf(':') + 1, fields[3].length()).toInt());
                QTime rtime        = QTime(fields[6].leftRef(fields[6].indexOf(':')).toInt(),
                                           fields[6].midRef(fields[6].indexOf(':') + 1, fields[6].length()).toInt());

                Rulebook[id] = TimeZoneRule(fields[1], fields[2], stime, fields[4], fields[5], rtime);
            }
        }
        return true;
    }
    else
    {
        return false;
    }
}

bool KStarsData::openUrlFile(const QString &urlfile, QFile &file)
{
    //QFile file;
    QString localFile;
    bool fileFound = false;
    QFile localeFile;

    //if ( locale->language() != "en_US" )
    if (QLocale().language() != QLocale::English)
        //localFile = locale->language() + '/' + urlfile;
        localFile = QLocale().languageToString(QLocale().language()) + '/' + urlfile;

    if (!localFile.isEmpty() && KSUtils::openDataFile(file, localFile))
    {
        fileFound = true;
    }
    else
    {
        // Try to load locale file, if not successful, load regular urlfile and then copy it to locale.
        file.setFileName(KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + urlfile);
        if (file.open(QIODevice::ReadOnly))
        {
            //local file found.  Now, if global file has newer timestamp, then merge the two files.
            //First load local file into QStringList
            bool newDataFound(false);
            QStringList urlData;
            QTextStream lStream(&file);
            while (!lStream.atEnd())
                urlData.append(lStream.readLine());

            //Find global file(s) in findAllResources() list.
            QFileInfo fi_local(file.fileName());

            QStringList flist = KSPaths::locateAll(QStandardPaths::DataLocation, urlfile);

            for (int i = 0; i < flist.size(); i++)
            {
                if (flist[i] != file.fileName())
                {
                    QFileInfo fi_global(flist[i]);

                    //Is this global file newer than the local file?
                    if (fi_global.lastModified() > fi_local.lastModified())
                    {
                        //Global file has newer timestamp than local.  Add lines in global file that don't already exist in local file.
                        //be smart about this; in some cases the URL is updated but the image is probably the same if its
                        //label string is the same.  So only check strings up to last ":"
                        QFile globalFile(flist[i]);
                        if (globalFile.open(QIODevice::ReadOnly))
                        {
                            QTextStream gStream(&globalFile);
                            while (!gStream.atEnd())
                            {
                                QString line = gStream.readLine();

                                //If global-file line begins with "XXX:" then this line should be removed from the local file.
                                if (line.startsWith(QLatin1String("XXX:")) && urlData.contains(line.mid(4)))
                                {
                                    urlData.removeAt(urlData.indexOf(line.mid(4)));
                                }
                                else
                                {
                                    //does local file contain the current global file line, up to second ':' ?

                                    bool linefound(false);
                                    for (int j = 0; j < urlData.size(); ++j)
                                    {
                                        if (urlData[j].contains(line.left(line.indexOf(':', line.indexOf(':') + 1))))
                                        {
                                            //replace line in urlData with its equivalent in the newer global file.
                                            urlData.replace(j, line);
                                            if (!newDataFound)
                                                newDataFound = true;
                                            linefound = true;
                                            break;
                                        }
                                    }
                                    if (!linefound)
                                    {
                                        urlData.append(line);
                                        if (!newDataFound)
                                            newDataFound = true;
                                    }
                                }
                            }
                        }
                    }
                }
            }

            file.close();

            //(possibly) write appended local file
            if (newDataFound)
            {
                if (file.open(QIODevice::WriteOnly))
                {
                    QTextStream outStream(&file);
                    for (int i = 0; i < urlData.size(); i++)
                    {
                        outStream << urlData[i] << endl;
                    }
                    file.close();
                }
            }

            if (file.open(QIODevice::ReadOnly))
                fileFound = true;
        }
        else
        {
            if (KSUtils::openDataFile(file, urlfile))
            {
                if (QLocale().language() != QLocale::English)
                    qDebug() << "No localized URL file; using default English file.";
                // we found urlfile, we need to copy it to locale
                localeFile.setFileName(KSPaths::writableLocation(QStandardPaths::GenericDataLocation) + urlfile);
                if (localeFile.open(QIODevice::WriteOnly))
                {
                    QTextStream readStream(&file);
                    QTextStream writeStream(&localeFile);
                    while (!readStream.atEnd())
                    {
                        QString line = readStream.readLine();
                        if (!line.startsWith(QLatin1String("XXX:"))) //do not write "deleted" lines
                            writeStream << line << endl;
                    }

                    localeFile.close();
                    file.reset();
                }
                else
                {
                    qDebug() << "Failed to copy default URL file to locale folder, modifying default object links is "
                             "not possible";
                }
                fileFound = true;
            }
        }
    }
    return fileFound;
}

// FIXME: This is a significant contributor to KStars start-up time
bool KStarsData::readURLData(const QString &urlfile, int type, bool deepOnly)
{
#ifndef KSTARS_LITE
    if (KStars::Closing)
        return true;
#endif

    QFile file;
    if (!openUrlFile(urlfile, file))
        return false;

    QTextStream stream(&file);

    while (!stream.atEnd())
    {
        QString line = stream.readLine();

        //ignore comment lines
        if (!line.startsWith('#'))
        {
#ifndef KSTARS_LITE
            if (KStars::Closing)
            {
                file.close();
                return true;
            }
#endif

            int idx      = line.indexOf(':');
            QString name = line.left(idx);
            if (name == "XXX")
                continue;
            QString sub   = line.mid(idx + 1);
            idx           = sub.indexOf(':');
            QString title = sub.left(idx);
            QString url   = sub.mid(idx + 1);
            // Dirty hack to fix things up for planets
            SkyObject *o;
            if (name == "Mercury" || name == "Venus" || name == "Mars" || name == "Jupiter" || name == "Saturn" ||
                    name == "Uranus" || name == "Neptune" /* || name == "Pluto" */)
                o = skyComposite()->findByName(i18n(name.toLocal8Bit().data()));
            else
                o = skyComposite()->findByName(name);

            if (!o)
            {
                qCWarning(KSTARS) << i18n("Object named %1 not found", name);
            }
            else
            {
                if (!deepOnly || (o->type() > 2 && o->type() < 9))
                {
                    if (type == 0) //image URL
                    {
                        o->ImageList().append(url);
                        o->ImageTitle().append(title);
                    }
                    else if (type == 1) //info URL
                    {
                        o->InfoList().append(url);
                        o->InfoTitle().append(title);
                    }
                }
            }
        }
    }
    file.close();
    return true;
}

// FIXME: Improve the user log system

// Note: It might be very nice to keep the log in plaintext files, for
// portability, human-readability, and greppability. However, it takes
// a lot of time to parse and look up, is very messy from the
// reliability and programming point of view, needs to be parsed at
// start, can become corrupt easily because of a missing bracket...

// An SQLite database is a good compromise. A user can easily view it
// using an SQLite browser. There is no need to read at start-up, one
// can read the log when required. Easy to edit logs / update logs
// etc. Will not become corrupt. Needn't be parsed.

// However, IMHO, it is best to put these kinds of things in separate
// databases, instead of unifying them as a table under the user
// database. This ensures portability and a certain robustness that if
// a user opens it, they cannot incorrectly edit a part of the DB they
// did not intend to edit.

// --asimha 2016 Aug 17

// FIXME: This is a significant contributor to KStars startup time.
bool KStarsData::readUserLog()
{
    QFile file;
    QString buffer;
    QString sub, name, data;

    if (!KSUtils::openDataFile(file, "userlog.dat"))
        return false;

    QTextStream stream(&file);

    if (!stream.atEnd())
        buffer = stream.readAll();

    while (!buffer.isEmpty())
    {
        int startIndex, endIndex;

        startIndex = buffer.indexOf(QLatin1String("[KSLABEL:"));
        sub      = buffer.mid(startIndex); // FIXME: This is inefficient because we are making a copy of a huge string!
        endIndex = sub.indexOf(QLatin1String("[KSLogEnd]"));

        // Read name after KSLABEL identifier
        name = sub.mid(startIndex + 9, sub.indexOf(']') - (startIndex + 9));
        // Read data and skip new line
        data   = sub.mid(sub.indexOf(']') + 2, endIndex - (sub.indexOf(']') + 2));
        buffer = buffer.mid(endIndex + 11);

        //Find the sky object named 'name'.
        //Note that ObjectNameList::find() looks for the ascii representation
        //of star genetive names, so stars are identified that way in the user log.
        SkyObject *o = skyComposite()->findByName(name);
        if (!o)
        {
            qWarning() << name << " not found";
        }
        else
        {
            o->userLog() = data;
        }

    } // end while
    file.close();
    return true;
}

bool KStarsData::readADVTreeData()
{
    QFile file;
    QString Interface;
    QString Name, Link, subName;

    if (!KSUtils::openDataFile(file, "advinterface.dat"))
        return false;

    QTextStream stream(&file);
    QString Line;

    while (!stream.atEnd())
    {
        int Type, interfaceIndex;

        Line = stream.readLine();

        if (Line.startsWith(QLatin1String("[KSLABEL]")))
        {
            Name = Line.mid(9);
            Type = 0;
        }
        else if (Line.startsWith(QLatin1String("[END]")))
            Type = 1;
        else if (Line.startsWith(QLatin1String("[KSINTERFACE]")))
        {
            Interface = Line.mid(13);
            continue;
        }

        else
        {
            int idx = Line.indexOf(':');
            Name    = Line.left(idx);
            Link    = Line.mid(idx + 1);

            // Link is empty, using Interface instead
            if (Link.isEmpty())
            {
                Link           = Interface;
                subName        = Name;
                interfaceIndex = Link.indexOf(QLatin1String("KSINTERFACE"));
                Link.remove(interfaceIndex, 11);
                Link = Link.insert(interfaceIndex, subName.replace(' ', '+'));
            }

            Type = 2;
        }

        ADVTreeData *ADVData = new ADVTreeData;

        ADVData->Name = Name;
        ADVData->Link = Link;
        ADVData->Type = Type;

        ADVtreeList.append(ADVData);
    }

    return true;
}

//There's a lot of code duplication here, but it's not avoidable because
//this function is only called from main.cpp when the user is using
//"dump" mode to produce an image from the command line.  In this mode,
//there is no KStars object, so none of the DBus functions can be called
//directly.
bool KStarsData::executeScript(const QString &scriptname, SkyMap *map)
{
#ifndef KSTARS_LITE
    int cmdCount(0);

    QFile f(scriptname);
    if (!f.open(QIODevice::ReadOnly))
    {
        qDebug() << "Could not open file " << f.fileName();
        return false;
    }

    QTextStream istream(&f);
    while (!istream.atEnd())
    {
        QString line = istream.readLine();
        line.remove("string:");
        line.remove("int:");
        line.remove("double:");
        line.remove("bool:");

        //find a dbus line and extract the function name and its arguments
        //The function name starts after the last occurrence of "org.kde.kstars."
        //or perhaps "org.kde.kstars.SimClock.".
        if (line.startsWith(QString("dbus-send")))
        {
            QString funcprefix = "org.kde.kstars.SimClock.";
            int i              = line.lastIndexOf(funcprefix);
            if (i >= 0)
            {
                i += funcprefix.length();
            }
            else
            {
                funcprefix = "org.kde.kstars.";
                i          = line.lastIndexOf(funcprefix);
                if (i >= 0)
                {
                    i += funcprefix.length();
                }
            }
            if (i < 0)
            {
                qWarning() << "Could not parse line: " << line;
                return false;
            }

            QStringList fn = line.mid(i).split(' ');

            //DEBUG
            //qDebug() << fn << endl;

            if (fn[0] == "lookTowards" && fn.size() >= 2)
            {
                double az(-1.0);
                QString arg = fn[1].toLower();
                if (arg == "n" || arg == "north")
                    az = 0.0;
                if (arg == "ne" || arg == "northeast")
                    az = 45.0;
                if (arg == "e" || arg == "east")
                    az = 90.0;
                if (arg == "se" || arg == "southeast")
                    az = 135.0;
                if (arg == "s" || arg == "south")
                    az = 180.0;
                if (arg == "sw" || arg == "southwest")
                    az = 225.0;
                if (arg == "w" || arg == "west")
                    az = 270.0;
                if (arg == "nw" || arg == "northwest")
                    az = 335.0;
                if (az >= 0.0)
                {
                    map->setFocusAltAz(dms(90.0), map->focus()->az());
                    map->focus()->HorizontalToEquatorial(&LST, geo()->lat());
                    map->setDestination(*map->focus());
                    cmdCount++;
                }

                if (arg == "z" || arg == "zenith")
                {
                    map->setFocusAltAz(dms(90.0), map->focus()->az());
                    map->focus()->HorizontalToEquatorial(&LST, geo()->lat());
                    map->setDestination(*map->focus());
                    cmdCount++;
                }

                //try a named object.  The name is everything after fn[0],
                //concatenated with spaces.
                fn.removeAll(fn.first());
                QString objname   = fn.join(" ");
                SkyObject *target = objectNamed(objname);
                if (target)
                {
                    map->setFocus(target);
                    map->focus()->EquatorialToHorizontal(&LST, geo()->lat());
                    map->setDestination(*map->focus());
                    cmdCount++;
                }
            }
            else if (fn[0] == "setRaDec" && fn.size() == 3)
            {
                bool ok(false);
                dms r(0.0), d(0.0);

                ok = r.setFromString(fn[1], false); //assume angle in hours
                if (ok)
                    ok = d.setFromString(fn[2], true); //assume angle in degrees
                if (ok)
                {
                    map->setFocus(r, d);
                    map->focus()->EquatorialToHorizontal(&LST, geo()->lat());
                    cmdCount++;
                }
            }
            else if (fn[0] == "setAltAz" && fn.size() == 3)
            {
                bool ok(false);
                dms az(0.0), alt(0.0);

                ok = alt.setFromString(fn[1]);
                if (ok)
                    ok = az.setFromString(fn[2]);
                if (ok)
                {
                    map->setFocusAltAz(alt, az);
                    map->focus()->HorizontalToEquatorial(&LST, geo()->lat());
                    cmdCount++;
                }
            }
            else if (fn[0] == "loadColorScheme")
            {
                fn.removeAll(fn.first());
                QString csName = fn.join(" ").remove('\"');
                qCDebug(KSTARS) << "Loading Color scheme: " << csName;

                QString filename = csName.toLower().trimmed();
                bool ok(false);

                //Parse default names which don't follow the regular file-naming scheme
                if (csName == i18nc("use default color scheme", "Default Colors"))
                    filename = "classic.colors";
                if (csName == i18nc("use 'star chart' color scheme", "Star Chart"))
                    filename = "chart.colors";
                if (csName == i18nc("use 'night vision' color scheme", "Night Vision"))
                    filename = "night.colors";

                //Try the filename if it ends with ".colors"
                if (filename.endsWith(QLatin1String(".colors")))
                    ok = colorScheme()->load(filename);

                //If that didn't work, try assuming that 'name' is the color scheme name
                //convert it to a filename exactly as ColorScheme::save() does
                if (!ok)
                {
                    if (!filename.isEmpty())
                    {
                        for (int i = 0; i < filename.length(); ++i)
                            if (filename.at(i) == ' ')
                                filename.replace(i, 1, "-");

                        filename = filename.append(".colors");
                        ok       = colorScheme()->load(filename);
                    }

                    if (!ok)
                        qDebug() << QString("Unable to load color scheme named %1. Also tried %2.")
                                 .arg(csName, filename);
                }
            }
            else if (fn[0] == "zoom" && fn.size() == 2)
            {
                bool ok(false);
                double z = fn[1].toDouble(&ok);
                if (ok)
                {
                    if (z > MAXZOOM)
                        z = MAXZOOM;
                    if (z < MINZOOM)
                        z = MINZOOM;
                    Options::setZoomFactor(z);
                    cmdCount++;
                }
            }
            else if (fn[0] == "zoomIn")
            {
                if (Options::zoomFactor() < MAXZOOM)
                {
                    Options::setZoomFactor(Options::zoomFactor() * DZOOM);
                    cmdCount++;
                }
            }
            else if (fn[0] == "zoomOut")
            {
                if (Options::zoomFactor() > MINZOOM)
                {
                    Options::setZoomFactor(Options::zoomFactor() / DZOOM);
                    cmdCount++;
                }
            }
            else if (fn[0] == "defaultZoom")
            {
                Options::setZoomFactor(DEFAULTZOOM);
                cmdCount++;
            }
            else if (fn[0] == "setLocalTime" && fn.size() == 7)
            {
                bool ok(false);
                // min is a macro - use mnt
                int yr(0), mth(0), day(0), hr(0), mnt(0), sec(0);
                yr = fn[1].toInt(&ok);
                if (ok)
                    mth = fn[2].toInt(&ok);
                if (ok)
                    day = fn[3].toInt(&ok);
                if (ok)
                    hr = fn[4].toInt(&ok);
                if (ok)
                    mnt = fn[5].toInt(&ok);
                if (ok)
                    sec = fn[6].toInt(&ok);
                if (ok)
                {
                    changeDateTime(geo()->LTtoUT(KStarsDateTime(QDate(yr, mth, day), QTime(hr, mnt, sec))));
                    cmdCount++;
                }
                else
                {
                    qWarning() << ki18n("Could not set time: %1 / %2 / %3 ; %4:%5:%6")
                               .subs(day)
                               .subs(mth)
                               .subs(yr)
                               .subs(hr)
                               .subs(mnt)
                               .subs(sec)
                               .toString()
                               << endl;
                }
            }
            else if (fn[0] == "changeViewOption" && fn.size() == 3)
            {
                bool bOk(false), dOk(false);

                //parse bool value
                bool bVal(false);
                if (fn[2].toLower() == "true")
                {
                    bOk  = true;
                    bVal = true;
                }
                if (fn[2].toLower() == "false")
                {
                    bOk  = true;
                    bVal = false;
                }
                if (fn[2] == "1")
                {
                    bOk  = true;
                    bVal = true;
                }
                if (fn[2] == "0")
                {
                    bOk  = true;
                    bVal = false;
                }

                //parse double value
                double dVal = fn[2].toDouble(&dOk);

                // FIXME: REGRESSION
                //                if ( fn[1] == "FOVName"                ) { Options::setFOVName(       fn[2] ); cmdCount++; }
                //                if ( fn[1] == "FOVSizeX"         && dOk ) { Options::setFOVSizeX( (float)dVal ); cmdCount++; }
                //                if ( fn[1] == "FOVSizeY"         && dOk ) { Options::setFOVSizeY( (float)dVal ); cmdCount++; }
                //                if ( fn[1] == "FOVShape"        && nOk ) { Options::setFOVShape(       nVal ); cmdCount++; }
                //                if ( fn[1] == "FOVColor"               ) { Options::setFOVColor(      fn[2] ); cmdCount++; }
                if (fn[1] == "ShowStars" && bOk)
                {
                    Options::setShowStars(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowMessier" && bOk)
                {
                    Options::setShowMessier(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowMessierImages" && bOk)
                {
                    Options::setShowMessierImages(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowCLines" && bOk)
                {
                    Options::setShowCLines(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowCNames" && bOk)
                {
                    Options::setShowCNames(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowNGC" && bOk)
                {
                    Options::setShowNGC(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowIC" && bOk)
                {
                    Options::setShowIC(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowMilkyWay" && bOk)
                {
                    Options::setShowMilkyWay(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowEquatorialGrid" && bOk)
                {
                    Options::setShowEquatorialGrid(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowHorizontalGrid" && bOk)
                {
                    Options::setShowHorizontalGrid(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowEquator" && bOk)
                {
                    Options::setShowEquator(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowEcliptic" && bOk)
                {
                    Options::setShowEcliptic(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowHorizon" && bOk)
                {
                    Options::setShowHorizon(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowGround" && bOk)
                {
                    Options::setShowGround(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowSun" && bOk)
                {
                    Options::setShowSun(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowMoon" && bOk)
                {
                    Options::setShowMoon(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowMercury" && bOk)
                {
                    Options::setShowMercury(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowVenus" && bOk)
                {
                    Options::setShowVenus(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowMars" && bOk)
                {
                    Options::setShowMars(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowJupiter" && bOk)
                {
                    Options::setShowJupiter(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowSaturn" && bOk)
                {
                    Options::setShowSaturn(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowUranus" && bOk)
                {
                    Options::setShowUranus(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowNeptune" && bOk)
                {
                    Options::setShowNeptune(bVal);
                    cmdCount++;
                }
                //if ( fn[1] == "ShowPluto"       && bOk ) { Options::setShowPluto(    bVal ); cmdCount++; }
                if (fn[1] == "ShowAsteroids" && bOk)
                {
                    Options::setShowAsteroids(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowComets" && bOk)
                {
                    Options::setShowComets(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowSolarSystem" && bOk)
                {
                    Options::setShowSolarSystem(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowDeepSky" && bOk)
                {
                    Options::setShowDeepSky(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowSupernovae" && bOk)
                {
                    Options::setShowSupernovae(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowStarNames" && bOk)
                {
                    Options::setShowStarNames(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowStarMagnitudes" && bOk)
                {
                    Options::setShowStarMagnitudes(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowAsteroidNames" && bOk)
                {
                    Options::setShowAsteroidNames(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowCometNames" && bOk)
                {
                    Options::setShowCometNames(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowPlanetNames" && bOk)
                {
                    Options::setShowPlanetNames(bVal);
                    cmdCount++;
                }
                if (fn[1] == "ShowPlanetImages" && bOk)
                {
                    Options::setShowPlanetImages(bVal);
                    cmdCount++;
                }

                if (fn[1] == "UseAltAz" && bOk)
                {
                    Options::setUseAltAz(bVal);
                    cmdCount++;
                }
                if (fn[1] == "UseRefraction" && bOk)
                {
                    Options::setUseRefraction(bVal);
                    cmdCount++;
                }
                if (fn[1] == "UseAutoLabel" && bOk)
                {
                    Options::setUseAutoLabel(bVal);
                    cmdCount++;
                }
                if (fn[1] == "UseAutoTrail" && bOk)
                {
                    Options::setUseAutoTrail(bVal);
                    cmdCount++;
                }
                if (fn[1] == "UseAnimatedSlewing" && bOk)
                {
                    Options::setUseAnimatedSlewing(bVal);
                    cmdCount++;
                }
                if (fn[1] == "FadePlanetTrails" && bOk)
                {
                    Options::setFadePlanetTrails(bVal);
                    cmdCount++;
                }
                if (fn[1] == "SlewTimeScale" && dOk)
                {
                    Options::setSlewTimeScale(dVal);
                    cmdCount++;
                }
                if (fn[1] == "ZoomFactor" && dOk)
                {
                    Options::setZoomFactor(dVal);
                    cmdCount++;
                }
                //                if ( fn[1] == "MagLimitDrawStar"     && dOk ) { Options::setMagLimitDrawStar( dVal ); cmdCount++; }
                if (fn[1] == "StarDensity" && dOk)
                {
                    Options::setStarDensity(dVal);
                    cmdCount++;
                }
                //                if ( fn[1] == "MagLimitDrawStarZoomOut" && dOk ) { Options::setMagLimitDrawStarZoomOut( dVal ); cmdCount++; }
                if (fn[1] == "MagLimitDrawDeepSky" && dOk)
                {
                    Options::setMagLimitDrawDeepSky(dVal);
                    cmdCount++;
                }
                if (fn[1] == "MagLimitDrawDeepSkyZoomOut" && dOk)
                {
                    Options::setMagLimitDrawDeepSkyZoomOut(dVal);
                    cmdCount++;
                }
                if (fn[1] == "StarLabelDensity" && dOk)
                {
                    Options::setStarLabelDensity(dVal);
                    cmdCount++;
                }
                if (fn[1] == "MagLimitHideStar" && dOk)
                {
                    Options::setMagLimitHideStar(dVal);
                    cmdCount++;
                }
                if (fn[1] == "MagLimitAsteroid" && dOk)
                {
                    Options::setMagLimitAsteroid(dVal);
                    cmdCount++;
                }
                if (fn[1] == "AsteroidLabelDensity" && dOk)
                {
                    Options::setAsteroidLabelDensity(dVal);
                    cmdCount++;
                }
                if (fn[1] == "MaxRadCometName" && dOk)
                {
                    Options::setMaxRadCometName(dVal);
                    cmdCount++;
                }

                //these three are a "radio group"
                if (fn[1] == "UseLatinConstellationNames" && bOk)
                {
                    Options::setUseLatinConstellNames(true);
                    Options::setUseLocalConstellNames(false);
                    Options::setUseAbbrevConstellNames(false);
                    cmdCount++;
                }
                if (fn[1] == "UseLocalConstellationNames" && bOk)
                {
                    Options::setUseLatinConstellNames(false);
                    Options::setUseLocalConstellNames(true);
                    Options::setUseAbbrevConstellNames(false);
                    cmdCount++;
                }
                if (fn[1] == "UseAbbrevConstellationNames" && bOk)
                {
                    Options::setUseLatinConstellNames(false);
                    Options::setUseLocalConstellNames(false);
                    Options::setUseAbbrevConstellNames(true);
                    cmdCount++;
                }
            }
            else if (fn[0] == "setGeoLocation" && (fn.size() == 3 || fn.size() == 4))
            {
                QString city(fn[1]), province, country(fn[2]);
                province.clear();
                if (fn.size() == 4)
                {
                    province = fn[2];
                    country  = fn[3];
                }

                bool cityFound(false);
                foreach (GeoLocation *loc, geoList)
                {
                    if (loc->translatedName() == city &&
                            (province.isEmpty() || loc->translatedProvince() == province) &&
                            loc->translatedCountry() == country)
                    {
                        cityFound = true;
                        setLocation(*loc);
                        cmdCount++;
                        break;
                    }
                }

                if (!cityFound)
                    qWarning() << i18n("Could not set location named %1, %2, %3", city, province, country);
            }
        }
    } //end while

    if (cmdCount)
        return true;
#else
    Q_UNUSED(map)
    Q_UNUSED(scriptname)
#endif
    return false;
}

#ifndef KSTARS_LITE
void KStarsData::syncFOV()
{
    visibleFOVs.clear();
    // Add visible FOVs
    foreach (FOV *fov, availFOVs)
    {
        if (Options::fOVNames().contains(fov->name()))
            visibleFOVs.append(fov);
    }
    // Remove unavailable FOVs
    QSet<QString> names = QSet<QString>::fromList(Options::fOVNames());
    QSet<QString> all;
    foreach (FOV *fov, visibleFOVs)
    {
        all.insert(fov->name());
    }
    Options::setFOVNames(all.intersect(names).toList());
}

// FIXME: Why does KStarsData store the Execute instance??? -- asimha
Execute *KStarsData::executeSession()
{
    if (!m_Execute.get())
        m_Execute.reset(new Execute());

    return m_Execute.get();
}

// FIXME: Why does KStarsData store the ImageExporer instance??? KStarsData is supposed to work with no reference to KStars -- asimha
ImageExporter *KStarsData::imageExporter()
{
    if (!m_ImageExporter.get())
        m_ImageExporter.reset(new ImageExporter(KStars::Instance()));

    return m_ImageExporter.get();
}
#endif
