// startextdump.cpp
//
// Copyright (C) 2004, Chris Laurel <claurel@shatters.net>
//
// 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.
//
// Dump the contents of a Celestia star database file in a text
// format that's easy read and edit.

#include <iostream>
#include <fstream>
#include <iomanip>
#include <celastro/astro.h>
#include <celcompat/numbers.h>
#include <celutil/bytes.h>
#include <celengine/stellarclass.h>

using namespace std;

namespace astro = celestia::astro;

static string inputFilename;
static string outputFilename;
static string hdFilename;
static bool useOldFormat = false;
static bool useSphericalCoords = false;

void Usage()
{
    cerr << "Usage: startextdump [options] <star database file> [output file]\n";
    cerr << "  Options:\n";
    cerr << "    --old (or -o)       : input star database is pre-2.0 format\n";
    cerr << "    --hd                : dump HD catalog cross reference\n";
    cerr << "    --spherical (or -s) : output spherical coordinates (RA/dec/distance\n";
}


static uint32_t readUint(istream& in)
{
    uint32_t n;
    in.read(reinterpret_cast<char*>(&n), sizeof n);
    LE_TO_CPU_INT32(n, n);
    return n;
}

static float readFloat(istream& in)
{
    float f;
    in.read(reinterpret_cast<char*>(&f), sizeof f);
    LE_TO_CPU_FLOAT(f, f);
    return f;
}

static int16_t readShort(istream& in)
{
    int16_t n;
    in.read(reinterpret_cast<char*>(&n), sizeof n);
    LE_TO_CPU_INT16(n, n);
    return n;
}

static uint16_t readUshort(istream& in)
{
    uint16_t n;
    in.read(reinterpret_cast<char*>(&n), sizeof n);
    LE_TO_CPU_INT16(n, n);
    return n;
}

static uint8_t readUbyte(istream& in)
{
    uint8_t n;
    in.read((char*) &n, 1);
    return n;
}


void printStellarClass(uint16_t sc, ostream& out)
{
    StellarClass::StarType st = (StellarClass::StarType) (sc >> 12);

    if (st == StellarClass::WhiteDwarf)
    {
        out << "WD";
    }
    else if (st == StellarClass::NeutronStar)
    {
        out << "Q";
    }
    else if (st == StellarClass::BlackHole)
    {
        out << "X";
    }
    else if (st == StellarClass::NormalStar)
    {
        unsigned int spectralClass = (sc >> 8 & 0xf);
        unsigned int spectralSubclass = (sc >> 4 & 0xf);
        unsigned int luminosityClass = (sc & 0xf);

        if (spectralClass == 12)
        {
            out << '?';
        }
        else
        {
            out << "OBAFGKMRSNWW?LTC"[spectralClass];
            out << "0123456789"[spectralSubclass];
            switch (luminosityClass)
            {
            case StellarClass::Lum_Ia0:
                out << "Ia-0";
                break;
            case StellarClass::Lum_Ia:
                out << "Ia";
                break;
            case StellarClass::Lum_Ib:
                out << "Ib";
                break;
            case StellarClass::Lum_II:
                out << "II";
                break;
            case StellarClass::Lum_III:
                out << "III";
                break;
            case StellarClass::Lum_IV:
                out << "IV";
                break;
            case StellarClass::Lum_V:
                out << "V";
                break;
            case StellarClass::Lum_VI:
                out << "VI";
                break;
            }
        }
    }
    else
    {
        out << '?';
    }
}


bool DumpOldStarDatabase(istream& in, ostream& out, ofstream& hdOut,
                         bool spherical)
{
    uint32_t nStarsInFile = readUint(in);
    if (!in.good())
    {
        cerr << "Error reading count of stars from database.\n";
        return false;
    }

    out << nStarsInFile << '\n';

    for (uint32_t i = 0; i < nStarsInFile; i++)
    {
        if (!in.good())
        {
            cerr << "Error reading from star database at record " << i << endl;
            return false;
        }

        uint32_t catalogNum    = readUint(in);
        uint32_t HDCatalogNum  = readUint(in);
        float  RA            = readFloat(in);
        float  dec           = readFloat(in);
        float  parallax      = readFloat(in);
        int16_t  appMag        = readShort(in);
        uint16_t stellarClass  = readUshort(in);
        /*uint8_t  parallaxError = */readUbyte(in); // not used yet

        // Compute distance based on parallax
        double distance = astro::LY_PER_PARSEC<double> / (parallax > 0.0 ? parallax / 1000.0 : 1e-6);
        out << catalogNum << ' ';
        out << setprecision(8);

        if (spherical)
        {
            out << RA * 360.0 / 24.0 << ' ' << dec << ' ' << distance << ' ';
            out << setprecision(6);
            out << (float) appMag / 256.0f << ' ';
        }
        else
        {
            Eigen::Vector3d pos = astro::equatorialToCelestialCart((double) RA, (double) dec, distance);
            float absMag = (float) (appMag / 256.0 + 5 -
                                    5 * log10(distance / astro::LY_PER_PARSEC<double>));
            out << (float) pos.x() << ' ' <<
                   (float) pos.y() << ' ' <<
                   (float) pos.z() << ' ';
            out << setprecision(5);
            out << absMag << ' ';
        }
        printStellarClass(stellarClass, out);
        out << '\n';

        // Dump HD catalog cross reference
        if (hdOut.is_open() && HDCatalogNum != ~0u)
            hdOut << HDCatalogNum << ' ' << catalogNum << '\n';
    }

    return true;
}


bool DumpStarDatabase(istream& in, ostream& out, bool spherical)
{
    char header[8];
    in.read(header, sizeof header);
    if (strncmp(header, "CELSTARS", sizeof header))
    {
        cerr << "Missing header in star database.\n";
        return false;
    }

    uint16_t version = readUshort(in);
    if (version != 0x0100)
    {
        cerr << "Unsupported file version " << (version >> 8) << '.' <<
            (version & 0xff) << '\n';
        return false;
    }

    uint32_t nStarsInFile = readUint(in);
    if (!in.good())
    {
        cerr << "Error reading count of stars from database.\n";
        return false;
    }

    out << nStarsInFile << '\n';

    for (uint32_t i = 0; i < nStarsInFile; i++)
    {
        if (!in.good())
        {
            cerr << "Error reading from star database at record " << i << endl;
            return false;
        }

        uint32_t catalogNum   = readUint(in);
        float x               = readFloat(in);
        float y               = readFloat(in);
        float z               = readFloat(in);
        int16_t  absMag       = readShort(in);
        uint16_t stellarClass = readUshort(in);

        out << catalogNum << ' ';
        out << setprecision(7);

        if (spherical)
        {
            Eigen::Vector3d eclipticpos = {(double) x, (double) y, (double) z};
            Eigen::Vector3d pos = astro::eclipticToEquatorial(eclipticpos);
            double distance = sqrt(x * x + y * y + z * z);
            // acos outputs angles in interval [0, pi], use negative sign for interval [-pi, 0]
            double phi = -acos(pos.y() / distance) * 180 / celestia::numbers::pi;
            double theta = atan2(pos.z(), -pos.x()) * 180 / celestia::numbers::pi;
            // atan2 outputs angles in interval [-pi, pi], so we add 360 to fix this
            double ra = theta - 180 + 360;
            double dec = phi + 90;
            float appMag = float (absMag / 256.0 - 5 + 5 * log10(distance / astro::LY_PER_PARSEC<double>));

            out << fixed << setprecision(9) << (float) ra << ' ' << (float) dec << ' ';
            out << setprecision(6) << (float) distance << ' ';
            out << setprecision(2);
            out << appMag << ' ';
        }
        else
        {
            out << x << ' ' << y << ' ' << z << ' ';
            out << setprecision(4);
            out << ((float) absMag / 256.0f) << ' ';
        }

        printStellarClass(stellarClass, out);
        out << '\n';
    }

    return true;
}


bool parseCommandLine(int argc, char* argv[])
{
    int i = 1;
    int fileCount = 0;

    while (i < argc)
    {
        if (argv[i][0] == '-')
        {
            if (!strcmp(argv[i], "--hd"))
            {
                if (i == argc - 1)
                {
                    return false;
                }
                else
                {
                    i++;
                    hdFilename = string(argv[i]);
                }
            }
            else if (!strcmp(argv[i], "-o") || !strcmp(argv[i], "--old"))
            {
                useOldFormat = true;
            }
            else if (!strcmp(argv[i], "-s") || !strcmp(argv[i], "--spherical"))
            {
                useSphericalCoords = true;
            }
            else
            {
                cerr << "Unknown command line switch: " << argv[i] << '\n';
                return false;
            }
            i++;
        }
        else
        {
            if (fileCount == 0)
            {
                // input filename first
                inputFilename = string(argv[i]);
                fileCount++;
            }
            else if (fileCount == 1)
            {
                // output filename second
                outputFilename = string(argv[i]);
                fileCount++;
            }
            else
            {
                // more than two filenames on the command line is an error
                return false;
            }
            i++;
        }
    }

    return true;
}


int main(int argc, char* argv[])
{
    if (!parseCommandLine(argc, argv) || inputFilename.empty())
    {
        Usage();
        return 1;
    }

    ifstream stardbFile(inputFilename, ios::in | ios::binary);
    if (!stardbFile.good())
    {
        cerr << "Error opening star database file " << inputFilename << '\n';
        return 1;
    }

    ofstream hdOut;
    if (useOldFormat && !hdFilename.empty())
    {
        hdOut.open(hdFilename, ios::out);
        if (!hdOut.good())
        {
            cerr << "Error opening HD catalog output file " << hdFilename << '\n';
            return 1;
        }
    }

    bool success;
    ostream* out = &cout;
    ofstream fout;
    if (!outputFilename.empty())
    {
        fout.open(outputFilename, ios::out);
        if (!fout.good())
        {
            cerr << "Error opening output file " << outputFilename << '\n';
            return 1;
        }
	out = &fout;
    }

    if (useOldFormat)
    {
        success = DumpOldStarDatabase(stardbFile, *out, hdOut,
                                      useSphericalCoords);
    }
    else
    {
        success = DumpStarDatabase(stardbFile, *out, useSphericalCoords);
    }

    return success ? 0 : 1;
}
