/***************************************************************************
                          modcalcvizequinox.cpp  -  description
                             -------------------
    begin                : Thu 22 Feb 2007
    copyright            : (C) 2007 by Jason Harris
    email                : kstars@30doradus.org
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 "modcalcvizequinox.h"

#include "dms.h"
#include "kstarsdata.h"
#include "skyobjects/kssun.h"
#include "widgets/dmsbox.h"

#include <KLineEdit>
#include <KPlotAxis>
#include <KPlotObject>
#include <KPlotPoint>

#include <cmath>

modCalcEquinox::modCalcEquinox(QWidget *parentSplit) : QFrame(parentSplit), dSpring(), dSummer(), dAutumn(), dWinter()
{
    setupUi(this);

    connect(Year, SIGNAL(valueChanged(int)), this, SLOT(slotCompute()));
    connect(InputFileBatch, SIGNAL(urlSelected(QUrl)), this, SLOT(slotCheckFiles()));
    connect(OutputFileBatch, SIGNAL(urlSelected(QUrl)), this, SLOT(slotCheckFiles()));
    connect(RunButtonBatch, SIGNAL(clicked()), this, SLOT(slotRunBatch()));
    connect(ViewButtonBatch, SIGNAL(clicked()), this, SLOT(slotViewBatch()));

    Plot->axis(KPlotWidget::LeftAxis)->setLabel(i18n("Sun's Declination"));
    Plot->setTopPadding(40);
    //Don't draw Top & Bottom axes; custom axes drawn as plot objects
    Plot->axis(KPlotWidget::BottomAxis)->setVisible(false);
    Plot->axis(KPlotWidget::TopAxis)->setVisible(false);

    //This will call slotCompute():
    Year->setValue(KStarsData::Instance()->lt().date().year());

    RunButtonBatch->setEnabled(false);
    ViewButtonBatch->setEnabled(false);

    show();
}

double modCalcEquinox::dmonth(int i)
{
    Q_ASSERT(i >= 0 && i < 12 && "Month must be in 0 .. 11 range");
    return DMonth[i];
}

void modCalcEquinox::slotCheckFiles()
{
    RunButtonBatch->setEnabled(!InputFileBatch->lineEdit()->text().isEmpty() &&
                               !OutputFileBatch->lineEdit()->text().isEmpty());
}

void modCalcEquinox::slotRunBatch()
{
    QString inputFileName = InputFileBatch->url().toLocalFile();

    if (QFile::exists(inputFileName))
    {
        QFile f(inputFileName);
        if (!f.open(QIODevice::ReadOnly))
        {
            QString message = i18n("Could not open file %1.", f.fileName());
            KMessageBox::sorry(nullptr, message, i18n("Could Not Open File"));
            inputFileName.clear();
            return;
        }

        QTextStream istream(&f);
        processLines(istream);

        ViewButtonBatch->setEnabled(true);

        f.close();
    }
    else
    {
        QString message = i18n("Invalid file: %1", inputFileName);
        KMessageBox::sorry(nullptr, message, i18n("Invalid file"));
        inputFileName.clear();
        return;
    }
}

void modCalcEquinox::processLines(QTextStream &istream)
{
    QFile fOut(OutputFileBatch->url().toLocalFile());
    fOut.open(QIODevice::WriteOnly);
    QTextStream ostream(&fOut);
    int originalYear = Year->value();

    //Write header to output file
    ostream << i18n("# Timing of Equinoxes and Solstices\n") << i18n("# computed by KStars\n#\n")
            << i18n("# Vernal Equinox\t\tSummer Solstice\t\t\tAutumnal Equinox\t\tWinter Solstice\n#\n");

    while (!istream.atEnd())
    {
        QString line = istream.readLine();
        bool ok      = false;
        int year     = line.toInt(&ok);

        //for now I will simply change the value of the Year widget to trigger
        //computation of the Equinoxes and Solstices.
        if (ok)
        {
            //triggers slotCompute(), which sets values of dSpring et al.:
            Year->setValue(year);

            //Write to output file
            ostream << QLocale().toString(dSpring.date(), QLocale::LongFormat) << "\t"
                    << QLocale().toString(dSummer.date(), QLocale::LongFormat) << "\t"
                    << QLocale().toString(dAutumn.date(), QLocale::LongFormat) << "\t"
                    << QLocale().toString(dWinter.date(), QLocale::LongFormat) << endl;
        }
    }

    if (Year->value() != originalYear)
        Year->setValue(originalYear);
}

void modCalcEquinox::slotViewBatch()
{
    QFile fOut(OutputFileBatch->url().toLocalFile());
    fOut.open(QIODevice::ReadOnly);
    QTextStream istream(&fOut);
    QStringList text;

    while (!istream.atEnd())
        text.append(istream.readLine());

    fOut.close();

    KMessageBox::informationList(nullptr, i18n("Results of Sidereal time calculation"), text,
                                 OutputFileBatch->url().toLocalFile());
}

void modCalcEquinox::slotCompute()
{
    KStarsData *data = KStarsData::Instance();
    KSSun Sun;
    int year0 = Year->value();

    KStarsDateTime dt(QDate(year0, 1, 1), QTime(0, 0, 0));
    long double jd0 = dt.djd(); //save JD on Jan 1st
    for (int imonth = 0; imonth < 12; imonth++)
    {
        KStarsDateTime kdt(QDate(year0, imonth + 1, 1), QTime(0, 0, 0));
        DMonth[imonth] = kdt.djd() - jd0;
    }

    Plot->removeAllPlotObjects();

    //Add the celestial equator, just a single line bisecting the plot horizontally
    KPlotObject *ce = new KPlotObject(data->colorScheme()->colorNamed("EqColor"), KPlotObject::Lines, 2.0);
    ce->addPoint(0.0, 0.0);
    ce->addPoint(366.0, 0.0);
    Plot->addPlotObject(ce);

    // Tropic of cancer
    KPlotObject *tcan = new KPlotObject(data->colorScheme()->colorNamed("EqColor"), KPlotObject::Lines, 2.0);
    tcan->addPoint(0.0, 23.5);
    tcan->addPoint(366.0, 23.5);
    Plot->addPlotObject(tcan);

    // Tropic of capricorn
    KPlotObject *tcap = new KPlotObject(data->colorScheme()->colorNamed("EqColor"), KPlotObject::Lines, 2.0);
    tcap->addPoint(0.0, -23.5);
    tcap->addPoint(366.0, -23.5);
    Plot->addPlotObject(tcap);

    //Add Ecliptic.  This is more complicated than simply incrementing the
    //ecliptic longitude, because we want the x-axis to be time, not RA.
    //For each day in the year, compute the Sun's position.
    KPlotObject *ecl = new KPlotObject(data->colorScheme()->colorNamed("EclColor"), KPlotObject::Lines, 2);
    ecl->setLinePen(QPen(ecl->pen().color(), 4));

    Plot->setLimits(1.0, double(dt.date().daysInYear()), -30.0, 30.0);

    //Add top and bottom axis lines, and custom tickmarks at each month
    addDateAxes();

    for (int i = 1; i <= dt.date().daysInYear(); i++)
    {
        KSNumbers num(dt.djd());
        Sun.findPosition(&num);
        ecl->addPoint(double(i), Sun.dec().Degrees());

        dt = dt.addDays(1);
    }
    Plot->addPlotObject(ecl);

    dSpring = findEquinox(Year->value(), true, ecl);
    dSummer = findSolstice(Year->value(), true);
    dAutumn = findEquinox(Year->value(), false, ecl);
    dWinter = findSolstice(Year->value(), false);

    //Display the Date/Time of each event in the text fields
    VEquinox->setText(QLocale().toString(dSpring, QLocale::LongFormat));
    SSolstice->setText(QLocale().toString(dSummer, QLocale::LongFormat));
    AEquinox->setText(QLocale().toString(dAutumn, QLocale::LongFormat));
    WSolstice->setText(QLocale().toString(dWinter, QLocale::LongFormat));

    //Add vertical dotted lines at times of the equinoxes and solstices
    KPlotObject *poSpring = new KPlotObject(Qt::white, KPlotObject::Lines, 1);
    poSpring->setLinePen(QPen(Qt::white, 1.0, Qt::DotLine));
    poSpring->addPoint(dSpring.djd() - jd0, Plot->dataRect().top());
    poSpring->addPoint(dSpring.djd() - jd0, Plot->dataRect().bottom());
    Plot->addPlotObject(poSpring);
    KPlotObject *poSummer = new KPlotObject(Qt::white, KPlotObject::Lines, 1);
    poSummer->setLinePen(QPen(Qt::white, 1.0, Qt::DotLine));
    poSummer->addPoint(dSummer.djd() - jd0, Plot->dataRect().top());
    poSummer->addPoint(dSummer.djd() - jd0, Plot->dataRect().bottom());
    Plot->addPlotObject(poSummer);
    KPlotObject *poAutumn = new KPlotObject(Qt::white, KPlotObject::Lines, 1);
    poAutumn->setLinePen(QPen(Qt::white, 1.0, Qt::DotLine));
    poAutumn->addPoint(dAutumn.djd() - jd0, Plot->dataRect().top());
    poAutumn->addPoint(dAutumn.djd() - jd0, Plot->dataRect().bottom());
    Plot->addPlotObject(poAutumn);
    KPlotObject *poWinter = new KPlotObject(Qt::white, KPlotObject::Lines, 1);
    poWinter->setLinePen(QPen(Qt::white, 1.0, Qt::DotLine));
    poWinter->addPoint(dWinter.djd() - jd0, Plot->dataRect().top());
    poWinter->addPoint(dWinter.djd() - jd0, Plot->dataRect().bottom());
    Plot->addPlotObject(poWinter);
}

//Add custom top/bottom axes with tickmarks for each month
void modCalcEquinox::addDateAxes()
{
    KPlotObject *poTopAxis = new KPlotObject(Qt::white, KPlotObject::Lines, 1);
    poTopAxis->addPoint(0.0, Plot->dataRect().bottom()); //y-axis is reversed!
    poTopAxis->addPoint(366.0, Plot->dataRect().bottom());
    Plot->addPlotObject(poTopAxis);

    KPlotObject *poBottomAxis = new KPlotObject(Qt::white, KPlotObject::Lines, 1);
    poBottomAxis->addPoint(0.0, Plot->dataRect().top() + 0.02);
    poBottomAxis->addPoint(366.0, Plot->dataRect().top() + 0.02);
    Plot->addPlotObject(poBottomAxis);

    //Tick mark for each month
    for (int imonth = 0; imonth < 12; imonth++)
    {
        KPlotObject *poMonth = new KPlotObject(Qt::white, KPlotObject::Lines, 1);
        poMonth->addPoint(dmonth(imonth), Plot->dataRect().top());
        poMonth->addPoint(dmonth(imonth), Plot->dataRect().top() + 1.4);
        Plot->addPlotObject(poMonth);
        poMonth = new KPlotObject(Qt::white, KPlotObject::Lines, 1);
        poMonth->addPoint(dmonth(imonth), Plot->dataRect().bottom());
        poMonth->addPoint(dmonth(imonth), Plot->dataRect().bottom() - 1.4);
        Plot->addPlotObject(poMonth);
    }
}

KStarsDateTime modCalcEquinox::findEquinox(int year, bool Spring, KPlotObject *ecl)
{
    // Interpolate to find the moment when the Sun crosses the equator
    // Set initial guess in February or August to be sure that this
    // point is before equinox.
    const int month = Spring ? 2 : 8;
    int i           = QDate(year, month, 1).dayOfYear();
    double dec1, dec2;
    dec2 = ecl->points().at(i)->y();
    do
    {
        ++i;
        dec1 = dec2;
        dec2 = ecl->points().at(i)->y();
    } while (dec1 * dec2 > 0.0); //when dec1*dec2<0.0, we bracket the zero

    double x1 = ecl->points().at(i-1)->x();
    double x2 = ecl->points().at(i)->x();
    double d  = fabs(dec2 - dec1);
    double f  = 1.0 - fabs(dec2) / d; //fractional distance of the zero, from point1 to point2

    KStarsDateTime dt0(QDate(year, 1, 1), QTime(0, 0, 0));
    KStarsDateTime dt = dt0.addSecs(86400.0 * (x1 - 1 + f * (x2 - x1)));
    return dt;
}

KStarsDateTime modCalcEquinox::findSolstice(int year, bool Summer)
{
    //Find the moment when the Sun reaches maximum declination
    //First find three points which bracket the maximum (i.e., x2 > x1,x3)
    //Start at June 16th, which will always be approaching the solstice

    long double jd1, jd2, jd3, jd4;
    double y2(0.0), y3(0.0), y4(0.0);
    int month = 6;
    if (!Summer)
        month = 12;

    jd3 = KStarsDateTime(QDate(year, month, 16), QTime(0, 0, 0)).djd();
    KSNumbers num(jd3);
    KSSun Sun;
    Sun.findPosition(&num);
    y3 = Sun.dec().Degrees();

    int sgn = 1;
    if (!Summer)
        sgn = -1; //find minimum if the winter solstice is sought

    do
    {
        jd3 += 1.0;
        num.updateValues(jd3);
        Sun.findPosition(&num);
        y2 = y3;
        Sun.findPosition(&num);
        y3 = Sun.dec().Degrees();
    } while (y3 * sgn > y2 * sgn);

    //Ok, now y2 is larger(smaller) than both y3 and y1.
    // Never read back
//    jd2 = jd3 - 1.0;
    jd1 = jd3 - 2.0;

    //Choose a new starting jd2 that follows the golden ratio:
    // a/b = 1.618; a+b = 2...a = 0.76394
    jd2 = jd1 + 0.76394;
    num.updateValues(jd2);
    Sun.findPosition(&num);
    y2 = Sun.dec().Degrees();

    while (jd3 - jd1 > 0.0005) //sub-minute precision
    {
        jd4 = jd1 + jd3 - jd2;

        num.updateValues(jd4);
        Sun.findPosition(&num);
        y4 = Sun.dec().Degrees();

        if (y4 * sgn > y2 * sgn) //make jd4 the new center
        {
            if (jd4 > jd2)
            {
                jd1 = jd2;
                jd2 = jd4;
                y2  = y4;
            }
            else
            {
                jd3 = jd2;
                // Never read back
//                y3  = y2;
                jd2 = jd4;
                y2  = y4;
            }
        }
        else //make jd4 a new endpoint
        {
            if (jd4 > jd2)
            {
                jd3 = jd4;
                // Never read back
//                y3  = y4;
            }
            else
            {
                jd1 = jd4;
            }
        }
    }

    return KStarsDateTime(jd2);
}
