// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

/*
 * Galaxy creation code obtained from http://beltoforion.de/galaxy/galaxy_en.html
 * Thanks to Ingo Berg, great work.
 * Licensed under a  Creative Commons Attribution 3.0 License
 * http://creativecommons.org/licenses/by/3.0/
 */

#include "galaxydata.h"
#include "cumulativedistributor.h"
#include "star.h"
#include <QtGraphs/qscatterdataproxy.h>
#include <QtGraphs/qvalue3daxis.h>
#include <QtGraphs/q3dscene.h>
#include <QtGraphs/q3dcamera.h>
#include <QtGraphs/qscatter3dseries.h>
#include <QtGraphs/q3dtheme.h>
#include <QtCore/qmath.h>
#include <QtCore/qrandom.h>
#include <QPainter>

#include <QDebug>

static const int numOfStars = 70000;
static const int numOfDust = numOfStars / 2;
static const int numOfH2 = 200;

GalaxyData::GalaxyData(Q3DScatter *scatter,
                       qreal rad,
                       qreal radCore,
                       qreal deltaAng,
                       qreal ex1,
                       qreal ex2)
    : m_graph(scatter),
    m_pStars(0),
    m_pDust(0),
    m_pH2(0),
    m_elEx1(ex1),
    m_elEx2(ex2),
    m_angleOffset(deltaAng),
    m_radCore(radCore),
    m_radGalaxy(rad),
    m_radFarField(m_radGalaxy * 2),
    m_minx(9999.9),
    m_maxx(-9999.0),
    m_miny(9999.9),
    m_maxy(-9999.0),
    m_filtered(false)
{
    m_graph->activeTheme()->setType(Q3DTheme::ThemeEbony);
    m_graph->setShadowQuality(QAbstract3DGraph::ShadowQualityLow);

    m_graph->axisX()->setRange(-25000.0f, 25000.0f);
    m_graph->axisZ()->setRange(-25000.0f, 25000.0f);
    m_graph->setOptimizationHints(QAbstract3DGraph::OptimizationDefault);
    m_graph->activeTheme()->setBackgroundColor(QColor(31, 31, 31));
    m_graph->activeTheme()->setWindowColor(QColor(31, 31, 31));
    m_graph->activeTheme()->setLabelBackgroundColor(QColor(31, 31, 31));
    m_graph->activeTheme()->setLabelTextColor(QColor(31, 31, 31));

    QObject::connect(m_graph, &QAbstract3DGraph::currentFpsChanged,
                     this, &GalaxyData::handleFpsChange);
    m_graph->setMeasureFps(true);

    createSeries();
    createGalaxy();
}

GalaxyData::~GalaxyData()
{
    delete m_graph;
    if (m_pStars)
        delete [] m_pStars;
    if (m_pDust)
        delete [] m_pDust;
    if (m_pH2)
        delete [] m_pH2;
}

void GalaxyData::createGalaxy()
{
    if (m_pStars)
        delete [] m_pStars;
    m_pStars = new Star[numOfStars];

    if (m_pDust)
        delete [] m_pDust;
    m_pDust = new Star[numOfDust];

    if (m_pH2)
        delete [] m_pH2;
    m_pH2 = new Star[numOfH2 * 2];


    m_minx = 9999.9;
    m_maxx = -9999.0;
    m_miny = 9999.9;
    m_maxy = -9999.0;

    // First star is the black hole at the center
    m_pStars[0].m_a = 0;
    m_pStars[0].m_b = 0;
    m_pStars[0].m_angle = 0;
    m_pStars[0].m_theta = 0;
    m_pStars[0].m_center = QVector2D(0.0f, 0.0f);
    m_pStars[0].calcXY();

    // second star is at the edge of the core area
    m_pStars[1].m_a = m_radCore;
    m_pStars[1].m_b = m_radCore * getExcentricity(m_radCore);
    m_pStars[1].m_angle = getAngularOffset(m_radCore);
    m_pStars[1].m_theta = 0;
    m_pStars[1].m_center = QVector2D(0.0f, 0.0f);
    m_pStars[1].calcXY();
    checkMinMax(m_pStars[1]);

    // third star is at the edge of the disk
    m_pStars[2].m_a = m_radGalaxy;
    m_pStars[2].m_b = m_radGalaxy * getExcentricity(m_radGalaxy);
    m_pStars[2].m_angle = getAngularOffset(m_radGalaxy);
    m_pStars[2].m_theta = 0;
    m_pStars[2].m_center = QVector2D(0.0f, 0.0f);
    m_pStars[2].calcXY();
    checkMinMax(m_pStars[2]);

    CumulativeDistributor cd;
    cd.setupRealistic(1.0,             // Maximalintensität
                      0.02,            // k (bulge)
                      m_radGalaxy/3.0, // disc skalenlänge
                      m_radCore,       // bulge radius
                      0,               // start der intensitätskurve
                      m_radFarField,   // ende der intensitätskurve
                      1000.0);           // Anzahl der stützstellen

    for (int i = 3; i < numOfStars; ++i) {
        qreal rad = cd.valFromProp(QRandomGenerator::global()->generateDouble());

        m_pStars[i].m_a = rad;
        m_pStars[i].m_b = rad * getExcentricity(rad);
        m_pStars[i].m_angle = getAngularOffset(rad);
        m_pStars[i].m_theta = 360.0 * QRandomGenerator::global()->generateDouble();
        m_pStars[i].m_center = QVector2D(0.0f, 0.0f);
        m_pStars[i].calcXY();

        checkMinMax(m_pStars[i]);
    }

    // Initialize Dust
    qreal x, y, rad;
    for (int i = 0; i < numOfDust; ++i)
    {
        x = 2.0 * m_radGalaxy * QRandomGenerator::global()->generateDouble() - m_radGalaxy;
        y = 2.0 * m_radGalaxy * QRandomGenerator::global()->generateDouble() - m_radGalaxy;
        rad = sqrt(x*x + y*y);

        m_pDust[i].m_a = rad;
        m_pDust[i].m_b = rad * getExcentricity(rad);
        m_pDust[i].m_angle = getAngularOffset(rad);
        m_pDust[i].m_theta = 360.0 * QRandomGenerator::global()->generateDouble();
        m_pDust[i].m_center = QVector2D(0.0f, 0.0f);
        m_pDust[i].calcXY();

        checkMinMax(m_pDust[i]);
    }

    // Initialize H2
    for (int i = 0; i < numOfH2; ++i)
    {
        x = 2*(m_radGalaxy) * QRandomGenerator::global()->generateDouble() - (m_radGalaxy);
        y = 2*(m_radGalaxy) * QRandomGenerator::global()->generateDouble() - (m_radGalaxy);
        rad = sqrt(x*x + y*y);

        int k1 = 2*i;
        m_pH2[k1].m_a = rad;
        m_pH2[k1].m_b = rad * getExcentricity(rad);
        m_pH2[k1].m_angle = getAngularOffset(rad);
        m_pH2[k1].m_theta = 360.0 * QRandomGenerator::global()->generateDouble();
        m_pH2[k1].m_center = QVector2D(0.0f, 0.0f);
        m_pH2[k1].calcXY();

        int k2 = 2*i + 1;
        m_pH2[k2].m_a = rad + 1000.0;
        m_pH2[k2].m_b = rad * getExcentricity(rad);
        m_pH2[k2].m_angle = m_pH2[k1].m_angle;
        m_pH2[k2].m_theta = m_pH2[k1].m_theta;
        m_pH2[k2].m_center = m_pH2[k1].m_center;
        m_pH2[k2].calcXY();
    }

    qreal max = qMax(m_maxx, m_maxy);
    qreal min = -qMin(m_minx, m_miny);
    max = qMax(min, max);
    m_range = int((max + 500.0) / 1000.0) * 1000;
    m_graph->axisX()->setRange(-float(m_range), float(m_range));
    m_graph->axisZ()->setRange(-float(m_range), float(m_range));

    if (!m_filtered)
        createNormalDataView();
    else
        createFilteredView();
}

void GalaxyData::createSeries()
{
    QScatterDataProxy *proxyNormal = new QScatterDataProxy;
    m_normalSeries = new QScatter3DSeries(proxyNormal);
    m_normalSeries->setMesh(QAbstract3DSeries::MeshPoint);
    m_graph->addSeries(m_normalSeries);

    QScatterDataProxy *proxyDust = new QScatterDataProxy;
    m_dustSeries = new QScatter3DSeries(proxyDust);
    m_dustSeries->setMesh(QAbstract3DSeries::MeshPoint);
    m_graph->addSeries(m_dustSeries);

    QScatterDataProxy *proxyH2 = new QScatterDataProxy;
    m_H2Series = new QScatter3DSeries(proxyH2);
    m_H2Series->setMesh(QAbstract3DSeries::MeshPoint);
    m_graph->addSeries(m_H2Series);

    QScatterDataProxy *proxyFiltered = new QScatterDataProxy;
    m_filteredSeries = new QScatter3DSeries(proxyFiltered);
    m_filteredSeries->setMesh(QAbstract3DSeries::MeshCube);
    m_graph->addSeries(m_filteredSeries);

}

void GalaxyData::createNormalDataView()
{
    QScatterDataArray *dataArray = new QScatterDataArray;
    dataArray->resize(numOfStars);
    QScatterDataItem *ptrToDataArray = &dataArray->first();

    for (uint i = 0; i < numOfStars; i++) {
        ptrToDataArray->setPosition(QVector3D(m_pStars[i].m_pos.x(),
                                              0.0f,
                                              m_pStars[i].m_pos.y()));
        ptrToDataArray++;
    }

    m_normalSeries->dataProxy()->resetArray(dataArray);
    m_normalSeries->setMesh(QAbstract3DSeries::MeshPoint);
    m_normalSeries->setBaseColor(Qt::white);

    dataArray = new QScatterDataArray;
    dataArray->resize(numOfDust);
    ptrToDataArray = &dataArray->first();

    for (uint i = 0; i < numOfDust; i++) {
        ptrToDataArray->setPosition(QVector3D(m_pDust[i].m_pos.x(),
                                              0.0f,
                                              m_pDust[i].m_pos.y()));
        ptrToDataArray++;
    }

    m_dustSeries->dataProxy()->resetArray(dataArray);
    m_dustSeries->setMesh(QAbstract3DSeries::MeshPoint);
    m_dustSeries->setBaseColor(QColor(131, 111, 255));

    dataArray = new QScatterDataArray;
    dataArray->resize(numOfDust);
    ptrToDataArray = &dataArray->first();

    uint H2Count = numOfH2 * 2;
    for (uint i = 0; i < H2Count; i++) {
        ptrToDataArray->setPosition(QVector3D(m_pH2[i].m_pos.x(),
                                              0.0f,
                                              m_pH2[i].m_pos.y()));
        ptrToDataArray++;
    }

    m_H2Series->dataProxy()->resetArray(dataArray);
    m_H2Series->setMesh(QAbstract3DSeries::MeshPoint);
    m_H2Series->setBaseColor(Qt::red);
}

void GalaxyData::createFilteredView()
{
    int steps = (m_range / 1000) * 2;
    int tableSize = steps * steps;
    int *table = new int[tableSize];
    for (int i = 0; i < tableSize; i++)
        table[i] = 0;
    qreal add = qreal(m_range);
    int max = 0;

    for (uint i = 0; i < numOfStars; i++) {
        int x = int(m_pStars[i].m_pos.x() + add) / 1000;
        int y = int(m_pStars[i].m_pos.y() + add) / 1000;
        table[y * steps + x] = table[y * steps + x] + 1;

        if (max < table[y * steps + x])
            max = table[y * steps + x];
    }

    // Count how many cells have data
    int nActiveCell = 0;
    for (int i = 0; i < tableSize; i++) {
        if (table[i])
            nActiveCell++;
    }


    QScatterDataArray *dataArray = new QScatterDataArray;
    dataArray->resize(nActiveCell);
    QScatterDataItem *ptrToDataArray = &dataArray->first();

    for (int y = 0; y < steps; y++) {
        for (int x = 0; x < steps; x++) {
            if (table[y * steps + x]) {
                ptrToDataArray->setPosition(QVector3D(float(x) * 1000.0f - add + 500.0f,
                                                      float(table[y * steps + x]),
                                                      float(y) * 1000.0f - add + 500.0f));
                ptrToDataArray++;
            }
        }
    }

    m_filteredSeries->dataProxy()->resetArray(dataArray);
    m_filteredSeries->setMesh(QAbstract3DSeries::MeshCube);
    m_filteredSeries->setItemSize(0.1f);

    m_graph->axisY()->setRange(0.0f, float(max + 1));

    qDebug() << "max = " << max;
}

void GalaxyData::checkMinMax(const Star &star)
{
    if (star.m_pos.x() < m_minx)
        m_minx = star.m_pos.x();
    if (star.m_pos.x() > m_maxx)
        m_maxx = star.m_pos.x();
    if (star.m_pos.y() < m_miny)
        m_miny = star.m_pos.y();
    if (star.m_pos.y() > m_maxy)
        m_maxy = star.m_pos.y();
}

qreal GalaxyData::getExcentricity(qreal r) const
{
    if (r < m_radCore) {
        // Core region of the galaxy. Innermost part is round
        // excentricity increasing linear to the border of the core.
        return 1 + (r / m_radCore) * (m_elEx1-1);
    } else if (r > m_radCore && r <= m_radGalaxy) {
        return m_elEx1 + (r - m_radCore) / (m_radGalaxy - m_radCore) * (m_elEx2 - m_elEx1);
    } else if (r > m_radGalaxy && r < m_radFarField) {
        // excentricity is slowly reduced to 1.
        return m_elEx2 + (r - m_radGalaxy) / (m_radFarField - m_radGalaxy) * (1 - m_elEx2);
    } else {
        return 1.0;
    }
}

qreal GalaxyData::getAngularOffset(qreal rad) const
{
  return rad * m_angleOffset;
}

void GalaxyData::radiusGalaxyChanged(int value)
{
    m_radGalaxy = qreal(value);
    createGalaxy();
}

void GalaxyData::radiusCoreChanged(int value)
{
    m_radCore = qreal(value);
    createGalaxy();
}

void GalaxyData::angleOffsetChanged(int value)
{
    m_angleOffset = qreal(value) / 100000.0;
    createGalaxy();
}

void GalaxyData::eccentricityInnerChanged(int value)
{
    m_elEx1 = qreal(value) / 100.0;
    createGalaxy();
}

void GalaxyData::eccentricityOuterChanged(int value)
{
    m_elEx2 = qreal(value) / 100.0;
    createGalaxy();
}

void GalaxyData::setFilteredEnabled(bool enabled)
{
    m_filtered = enabled;
    if (enabled) {
        QLinearGradient gr(0, 0, 1, 100);
        gr.setColorAt(0.0, Qt::white);
        gr.setColorAt(0.04, Qt::green);
        gr.setColorAt(0.1, Qt::darkGreen);
        gr.setColorAt(1.0, Qt::red);

        m_filteredSeries->setBaseGradient(gr);
        m_filteredSeries->setColorStyle(Q3DTheme::ColorStyleRangeGradient);

        m_normalSeries->setVisible(false);

        createFilteredView();

        m_filteredSeries->setVisible(true);
    } else {
        m_normalSeries->setColorStyle(Q3DTheme::ColorStyleUniform);
        m_graph->axisY()->setRange(-1.0f, 1.0f);
        m_normalSeries->setItemSize(0.0f);

        m_filteredSeries->setVisible(false);

        createNormalDataView();

        m_normalSeries->setVisible(true);
    }
}


void GalaxyData::setStaticEnabled(bool enabled)
{
    if (enabled)
        m_graph->setOptimizationHints(QAbstract3DGraph::OptimizationDefault);
    else
        m_graph->setOptimizationHints(QAbstract3DGraph::OptimizationLegacy);
}

void GalaxyData::setStarsVisible(bool enabled)
{
    m_normalSeries->setVisible(enabled);
}

void GalaxyData::setDustVisible(bool enabled)
{
    m_dustSeries->setVisible(enabled);
}

void GalaxyData::setH2Visible(bool enabled)
{
    m_H2Series->setVisible(enabled);
}

void GalaxyData::resetValues()
{
    m_radiusGalaxySlider->setValue(15000);
    m_radiusCoreSlider->setValue(4000);
    m_angleOffsetSlider->setValue(40);
    m_eccentricityInnerSlider->setValue(90);
    m_eccentricityOuterSlider->setValue(90);
}

void GalaxyData::handleFpsChange(qreal fps)
{
    static const QString fpsPrefix(QStringLiteral("FPS: "));
    m_fpsLabel->setText(fpsPrefix + QString::number(qRound(fps)));
}
