//****************************************************************
/** @class FitsDumper

  @brief Dumps contents of fits tables to stdout or a file

 */
 //****************************************************************
#include "Configuration.h"

#include <map>
#include <fstream>

#include <CCfits/CCfits>

using namespace std;

class FitsDumper
{
public:
    FitsDumper();
    ~FitsDumper();

private:
    
    CCfits::FITS  *fFile;                 /// FITS pointer
    CCfits::Table *fTable;                /// Table pointer
    map<string, CCfits::Column*> fColMap; /// map between the column names and their CCfits objects

    // Convert CCfits::ValueType into a human readable string
    string ValueTypeToStr(CCfits::ValueType type) const;

    // Convert CCfits::ValueType into a number of associated bytes
    int    ValueTypeToSize(CCfits::ValueType type) const;

    /// Calculate the buffer size required to read a row of the fits table, as well as the offsets to each column
    vector<int> CalculateOffsets() const;

    template<class T>
        T PtrToValue(const unsigned char* &ptr) const;
//    template<class T>
//        double PtrToDouble(const unsigned char *ptr) const;
//    double PtrToDouble(const unsigned char *ptr, CCfits::ValueType type) const;

    /// Write a single row of the selected data
    int  WriteRow(ostream &, const vector<CCfits::Column*> &, const vector<int> &, unsigned char *) const;

    bool OpenFile(const string &);        /// Open a file
    bool OpenTable(const string &);       /// Open a table

    /// Lists all columns of an open file
    void List();                          
    void ListHeader();
    void ListKeywords(ostream &);

    /// Perform the dumping, based on the current dump list
    bool Dump(const string &, const vector<string> &list, int);

//    bool Plot(const vector<string> &list);

public:
    ///Configures the fitsLoader from the config file and/or command arguments.
    int ExecConfig(Configuration& conf);
};

// --------------------------------------------------------------------------
//
//! Constructor
//! @param out
//!        the ostream where to redirect the outputs
//
FitsDumper::FitsDumper() : fFile(0), fTable(0)
{
}

// --------------------------------------------------------------------------
//
//! Destructor
//
FitsDumper::~FitsDumper()
{
    if (fFile)
        delete fFile;
}


string FitsDumper::ValueTypeToStr(CCfits::ValueType type) const
{
    switch (type)
    {
    case CCfits::Tbyte:     return "uint8_t";
    case CCfits::Tushort:   return "uint16_t";
    case CCfits::Tshort:    return "int16_t";
    case CCfits::Tuint:     return "uint32_t";
    case CCfits::Tint:      return "int32_t";
    case CCfits::Tulong:    return "uint32_t";
    case CCfits::Tlong:     return "int32_t";
    case CCfits::Tlonglong: return "int64_t";
    case CCfits::Tfloat:    return "float";
    case CCfits::Tdouble:   return "double";

    default:
        return "unknwown";
    }
}

int FitsDumper::ValueTypeToSize(CCfits::ValueType type) const
{
    switch (type)
    {
    case CCfits::Tbyte:     return sizeof(uint8_t);
    case CCfits::Tushort:
    case CCfits::Tshort:    return sizeof(uint16_t);
    case CCfits::Tuint:
    case CCfits::Tint:
    case CCfits::Tulong:
    case CCfits::Tlong:     return sizeof(uint32_t);
    case CCfits::Tlonglong: return sizeof(uint64_t);
    case CCfits::Tfloat:    return sizeof(float);
    case CCfits::Tdouble:   return sizeof(double);

    default:
        return 0;
    }
}

template<class T>
T FitsDumper::PtrToValue(const unsigned char* &ptr) const
{
    T t;
    reverse_copy(ptr, ptr+sizeof(T), reinterpret_cast<unsigned char*>(&t));
    ptr += sizeof(T);

    return t;
}
/*
template<class T>
double FitsDumper::PtrToDouble(const unsigned char *ptr) const
{
    T t;
    reverse_copy(ptr, ptr+sizeof(T), reinterpret_cast<unsigned char*>(&t));
    return t;
}

double FitsDumper::PtrToDouble(const unsigned char *ptr, CCfits::ValueType type) const
{
    switch (type)
    {
    case CCfits::Tbyte:     return PtrToDouble<uint8_t> (ptr);
    case CCfits::Tushort:   return PtrToDouble<uint16_t>(ptr);
    case CCfits::Tuint:
    case CCfits::Tulong:    return PtrToDouble<uint32_t>(ptr);
    case CCfits::Tshort:    return PtrToDouble<int16_t> (ptr);
    case CCfits::Tint:
    case CCfits::Tlong:     return PtrToDouble<int32_t> (ptr);
    case CCfits::Tlonglong: return PtrToDouble<int64_t> (ptr);
    case CCfits::Tfloat:    return PtrToDouble<float>   (ptr);
    case CCfits::Tdouble:   return PtrToDouble<double>  (ptr);

    default:
        throw runtime_error("Data type not implemented yet.");
    }
}
*/

// --------------------------------------------------------------------------
//
//! Writes a single row of the selected FITS data to the output file.
//!
//! Data type \b not yet implemented:
//!   CCfits::Tnull, CCfits::Tbit, CCfits::Tlogical, CCfits::Tstring,
//!   CCfits::Tcomplex, CCfits::Tdblcomplex, CCfits::VTbit,
//!   CCfits::VTbyte, CCfits::VTlogical, CCfits::VTushort,
//!   CCfits::VTshort, CCfits::VTuint, CCfits::VTint, CCfits::VTulong,
//!   CCfits::VTlong, CCfits::VTlonglong, CCfits::VTfloat,
//!   CCfits::VTdouble, CCfits::VTcomplex, CCfits::VTdblcomplex
//!
//! @param offsets
//!         a vector containing the offsets to the columns (in bytes)
//! @param targetFile
//!         the ofstream where to write to
//! @param fitsBuffer
//!         the memory were the row has been loaded by cfitsio
//
int FitsDumper::WriteRow(ostream &out, const vector<CCfits::Column*> &list, const vector<int> &offsets, unsigned char* fitsBuffer) const
{
    int cnt = 0;

    for (vector<CCfits::Column*>::const_iterator it=list.begin(); it!=list.end(); it++)
    {
        const CCfits::Column *col = *it;

        // CCfits starts counting at 1 not 0
        const int offset = offsets[col->index()-1];

        // Get the pointer to the array which we are supposed to print
        const unsigned char *ptr = fitsBuffer + offset;

        // Loop over all array entries
        for (int width=0; width<col->width(); width++)
        {
            switch (col->type())
            {
            case CCfits::Tbyte:     out << PtrToValue<uint8_t> (ptr); break;
            case CCfits::Tushort:   out << PtrToValue<uint16_t>(ptr); break;
            case CCfits::Tuint:
            case CCfits::Tulong:    out << PtrToValue<uint32_t>(ptr); break;
            case CCfits::Tshort:    out << PtrToValue<int16_t> (ptr); break;
            case CCfits::Tint:
            case CCfits::Tlong:     out << PtrToValue<int32_t> (ptr); break;
            case CCfits::Tlonglong: out << PtrToValue<int64_t> (ptr); break;
            case CCfits::Tfloat:    out << PtrToValue<float>   (ptr); break;
            case CCfits::Tdouble:   out << PtrToValue<double>  (ptr); break;

            default:
                cerr << "Data type not implemented yet." << endl;
                return 0;
            }

            out << " ";
            cnt++;
        }
    }

    if (cnt>0)
        out << endl;

    if (out.fail())
    {
        cerr << "ERROR - writing output: " << strerror(errno) << endl;
        return -1;
    }

    return cnt;
}

// --------------------------------------------------------------------------
//
//! Calculates the required buffer size for reading one row of the current
//! table. Also calculates the offsets to all the columns
//
vector<int> FitsDumper::CalculateOffsets() const
{
    map<int,int> sizes;

    for (map<string, CCfits::Column*>::const_iterator it=fColMap.begin();
         it!=fColMap.end(); it++)
    {
        const int &width = it->second->width();
        const int &idx   = it->second->index();

        const int size = ValueTypeToSize(it->second->type());
        if (size==0)
        {
            cerr << "Data type " << (int)it->second->type() << " not implemented yet." << endl;
            return vector<int>();
        }

        sizes[idx] = size*width;
    }

    //calculate the offsets in the vector.
    vector<int> result(1, 0);

    int size = 0;
    int idx  = 0;

    for (map<int,int>::const_iterator it=sizes.begin(); it!=sizes.end(); it++)
    {
        size += it->second;
        result.push_back(size);

        if (it->first == ++idx)
            continue;

        cerr << "Expected index " << idx << ", but found " << it->first << endl;
        return vector<int>();
    }

    return result;
}

// --------------------------------------------------------------------------
//
//! Loads the fits file based on the current parameters
//
bool FitsDumper::OpenFile(const string &filename)
{
    if (fFile)
        delete fFile;

    ostringstream str;
    try
    {
        fFile = new CCfits::FITS(filename);
    }
    catch (CCfits::FitsException e)
    {
        cerr << "Could not open FITS file " << filename << " reason: " << e.message() << endl;
        return false;
    }

    return true;
}

bool FitsDumper::OpenTable(const string &tablename)
{
    if (!fFile)
    {
        cerr << "No file open." << endl;
        return false;
    }

    const multimap< string, CCfits::ExtHDU * > extMap = fFile->extension();
    if (extMap.find(tablename) == extMap.end())
    {
        cerr << "Table '" << tablename << "' not found." << endl;
        return false;
    }

    fTable  = dynamic_cast<CCfits::Table*>(extMap.find(tablename)->second);
    if (!fTable)
    {
        cerr << "Object '" << tablename << "' returned not a CCfits::Table." << endl;
        return false;
    }

    fColMap = fTable->column();

    fTable->makeThisCurrent();

    fTable->getComments();
    fTable->getHistory();
    fTable->readAllKeys();

    return true;
}

class MyColumn : public CCfits::Column
{
public:
    const string &comment() const { return CCfits::Column::comment(); }
};

void FitsDumper::List()
{
    if (!fFile)
    {
        cerr << "No file open." << endl;
        return;
    }

    cout << "\nFile: " << fFile->name() << "\n";

    const multimap< string, CCfits::ExtHDU * > extMap = fFile->extension();
    for (std::multimap<string, CCfits::ExtHDU*>::const_iterator it=extMap.begin(); it != extMap.end(); it++)
    {

        CCfits::Table *table = dynamic_cast<CCfits::Table*>(extMap.find(it->first)->second);

        cout << " " << it->first << " [" << table->rows() << "]\n";

        const map<string, CCfits::Column*> &cols = table->column();

        for (map<string, CCfits::Column*>::const_iterator ic=cols.begin();
             ic != cols.end(); ic++)
        {
            const MyColumn *col = static_cast<MyColumn*>(ic->second);

            cout << "   " << col->name() << "[" << col->width() << "] * " << col->scale() << " (" << col->unit() << ":" <<  ValueTypeToStr(col->type())<<  ") " << col->comment() << "\n";
            /*
             inline size_t Column::repeat () const
             inline bool   Column::varLength () const
             inline double Column::zero () const
             inline const  String& Column::display () const
             inline const  String& Column::dimen () const
             inline const  String& Column::TBCOL ()
             inline const  String& Column::TTYPE ()
             inline const  String& Column::TFORM ()
             inline const  String& Column::TDISP ()
             inline const  String& Column::TZERO ()
             inline const  String& Column::TDIM  ()
             inline const  String& Column::TNULL ()
             inline const  String& Column::TLMIN ()
             inline const  String& Column::TLMAX ()
             inline const  String& Column::TDMAX ()
             inline const  String& Column::TDMIN ()
            */
        }
        cout << '\n';
    }
    cout << flush;
}

class MyKeyword : public CCfits::Keyword
{
public:
    CCfits::ValueType keytype() const { return CCfits::Keyword::keytype(); }
};

void FitsDumper::ListKeywords(ostream &out)
{
    map<string,CCfits::Keyword*> keys = fTable->keyWord();
    for (map<string,CCfits::Keyword*>::const_iterator it=keys.begin();
         it!=keys.end(); it++)
    {
        string str;
        double d;

        const MyKeyword *kw = static_cast<MyKeyword*>(it->second);
        kw->keytype();
        out << "## " << setw(8) << kw->name() << "='";
        if (kw->keytype()==16)
            out << kw->value(str);
        if (kw->keytype()==82)
            out << kw->value(d);
        out << "' \t(" << kw->comment() << ")" << endl;
    }
}

void FitsDumper::ListHeader()
{
    if (!fTable)
    {
        cerr << "No table open." << endl;
        return;
    }

    cout << "\nTable: " << fTable->name() << " (rows=" << fTable->rows() << ")\n";
    if (!fTable->comment().empty())
        cout << "Comment: \t" << fTable->comment() << '\n';
    if (!fTable->history().empty())
        cout << "History: \t" << fTable->history() << '\n';

    ListKeywords(cout);
    cout << endl;
}


// --------------------------------------------------------------------------
//
//! Perform the actual dump, based on the current parameters
//
bool FitsDumper::Dump(const string &filename, const vector<string> &list, int precision)
{
    for (vector<string>::const_iterator it=list.begin(); it!=list.end(); it++)
        if (fColMap.find(*it) == fColMap.end())
        {
            cerr << "WARNING - Column '" << *it << "' not found in table." << endl;
            return false;
        }

    // FIXME: Maybe do this when opening a table?
    const vector<int> offsets = CalculateOffsets();
    if (offsets.size()==0)
        return false;

    ofstream out(filename=="-"?"/dev/stdout":filename);
    if (!out)
    {
        cerr << "Cannot open file " << filename << ": " << strerror(errno) << endl;
        return false;
    }

    out.precision(precision);

    out << "## --------------------------------------------------------------------------\n";
    if (filename!="-")
        out << "## File:    \t" << filename << '\n';
    out << "## Table:   \t" << fTable->name() << '\n';
    if (!fTable->comment().empty())
        out << "## Comment: \t" << fTable->comment() << '\n';
    if (!fTable->history().empty())
        out << "## History: \t" << fTable->history() << '\n';
    out << "## NumRows: \t" << fTable->rows() << '\n';
    out << "## --------------------------------------------------------------------------\n";
    ListKeywords(out);
    out << "## --------------------------------------------------------------------------\n";
    out << "#\n";
    for (vector<string>::const_iterator it=list.begin(); it!=list.end(); it++)
    {
        const MyColumn *col = static_cast<MyColumn*>(fTable->column()[*it]);
        out << "# " << col->name() << "[" << col->width() << "]: " << col->unit();
        if (!col->comment().empty())
            out << " (" <<col->comment() << ")";
        out << '\n';
    }
    out << "#" << endl;


    // Loop over all columns in our list of requested columns
    vector<CCfits::Column*> columns;
    for (vector<string>::const_iterator it=list.begin(); it!=list.end(); it++)
        columns.push_back(fColMap.find(*it)->second);


    const int size = offsets[offsets.size()-1];
    unsigned char* fitsBuffer = new unsigned char[size];

    int status = 0;
    for (int i=1; i<=fTable->rows(); i++)
    {
        fits_read_tblbytes(fFile->fitsPointer(), i, 1, size, fitsBuffer, &status);
        if (status)
        {
            cerr << "An error occurred while reading fits row #" << i << " error code: " << status << endl;
            break;
        }
        if (WriteRow(out, columns, offsets, fitsBuffer)<0)
        {
            status=1;
            break;
        }
    }
    delete[] fitsBuffer;

    return status==0;
}

// --------------------------------------------------------------------------
//
//! Perform the actual dump, based on the current parameters
//
/*
bool FitsDumper::Plot(const vector<string> &list)
{
   for (vector<string>::const_iterator it=list.begin(); it!=list.end(); it++)
        if (fColMap.find(*it) == fColMap.end())
        {
            cerr << "WARNING - Column '" << *it << "' not found in table." << endl;
            return false;
        }

    // FIXME: Maybe do this when opening a table?
    const vector<int> offsets = CalculateOffsets();
    if (offsets.size()==0)
        return false;

    // Loop over all columns in our list of requested columns
    const CCfits::Column *col[3] =
    {
        list.size()>0 ? fColMap.find(list[0])->second : 0,
        list.size()>1 ? fColMap.find(list[1])->second : 0,
        list.size()>2 ? fColMap.find(list[2])->second : 0
    };

    const int size = offsets[offsets.size()-1];
    unsigned char* fitsBuffer = new unsigned char[size];

    const int idx = 0;

    // CCfits starts counting at 1 not 0
    const size_t pos[3] =
    {
        col[0] ? offsets[col[0]->index()-1] + idx*col[0]->width()*ValueTypeToSize(col[0]->type()) : 0,
        col[1] ? offsets[col[1]->index()-1] + idx*col[1]->width()*ValueTypeToSize(col[1]->type()) : 0,
        col[2] ? offsets[col[2]->index()-1] + idx*col[2]->width()*ValueTypeToSize(col[2]->type()) : 0
    };

    int status = 0;
    for (int i=1; i<=fTable->rows(); i++)
    {
        fits_read_tblbytes(fFile->fitsPointer(), i, 1, size, fitsBuffer, &status);
        if (status)
        {
            cerr << "An error occurred while reading fits row #" << i << " error code: " << status << endl;
            break;
        }

        try
        {
            const double x[3] =
            {
                col[0] ? PtrToDouble(fitsBuffer+pos[0], col[0]->type()) : 0,
                col[1] ? PtrToDouble(fitsBuffer+pos[1], col[1]->type()) : 0,
                col[2] ? PtrToDouble(fitsBuffer+pos[2], col[2]->type()) : 0
            };
        }
        catch (const runtime_error &e)
        {
            cerr << e.what() << endl;
            status=1;
            break;
        }
    }
    delete[] fitsBuffer;

    return status==0;
}
*/

// --------------------------------------------------------------------------
//
//! Retrieves the configuration parameters
//! @param conf
//!             the configuration object
//
int FitsDumper::ExecConfig(Configuration& conf)
{
    if (conf.Has("fitsfile"))
    {
        if (!OpenFile(conf.Get<string>("fitsfile")))
            return -1;
    }

    if (conf.Get<bool>("list"))
        List();

    if (conf.Has("tablename"))
    {
        if (!OpenTable(conf.Get<string>("tablename")))
            return -1;
    }

    if (conf.Get<bool>("header"))
        ListHeader();

    if (conf.Get<bool>("header") || conf.Get<bool>("list"))
        return 1;

    if (conf.Has("outfile"))
    {
        if (!Dump(conf.Get<string>("outfile"),
                  conf.Get<vector<string>>("col"),
                  conf.Get<int>("precision")))
            return -1;
    }

    return 0;
}

void PrintUsage()
{
    cout <<
        "fitsdump is a tool to dump data from a FITS table as ascii.\n"
        "\n"
        "Usage: fitsdump [OPTIONS] fitsfile col col ... \n"
        "  or:  fitsdump [OPTIONS]\n";
    cout << endl;
}

void PrintHelp()
{
    // 
}

void SetupConfiguration(Configuration& conf)
{
    po::options_description configs("Fitsdump options");
    configs.add_options()
        ("fitsfile,f",  var<string>()
#if BOOST_VERSION >= 104200
         ->required()
#endif
                                                  , "Name of FITS file")
        ("tablename,t", var<string>("DATA")
#if BOOST_VERSION >= 104200
         ->required()
#endif
                                                  , "Name of input table")
        ("col,c",       vars<string>(),             "List of columns to dump")
        ("outfile,o",   var<string>("/dev/stdout"), "Name of output file (-:/dev/stdout)")
        ("precision,p", var<int>(20),               "Precision of ofstream")
        ("list,l",      po_switch(),                "List all tables and columns in file")
        ("header,h",    po_switch(),                "Dump header of given table")
        ;

    po::positional_options_description p;
    p.add("fitsfile",   1); // The first positional options
    p.add("col",       -1); // All others

    conf.AddOptions(configs);
    conf.SetArgumentPositions(p);
}

int main(int argc, const char** argv)
{
    Configuration conf(argv[0]);
    conf.SetPrintUsage(PrintUsage);
    SetupConfiguration(conf);

    po::variables_map vm;
    try
    {
        vm = conf.Parse(argc, argv);
    }
#if BOOST_VERSION > 104000
    catch (po::multiple_occurrences &e)
    {
        cerr << "Program options invalid due to: " << e.what() << " of option '" << e.get_option_name() << "'." << endl;
        return -1;
    }
#endif
    catch (exception& e)
    {
        cerr << "Program options invalid due to: " << e.what() << endl;
        return -1;
    }

    if (conf.HasVersion() || conf.HasPrint())
        return -1;

    if (conf.HasHelp())
    {
        PrintHelp();
        return -1;
    }

    FitsDumper loader;
    return loader.ExecConfig(conf);
}
