// **************************************************************************
/** @class FactFits

@brief FITS writter for the FACT project. 

The FactFits class is able to open, manage and update FITS files. 

The file columns should be given to the class before the file is openned. Once
a file has been created, the structure of its columns cannot be changed. Only
row can be added.

This class relies on the CCfits and CFitsIO packages.

*/
// **************************************************************************
#include "Fits.h"

#include "Time.h"
#include "Converter.h"
#include "MessageImp.h"


#include <sys/stat.h> //for file stats
#include <cstdio> // for file rename

using namespace std;
using namespace CCfits;

// --------------------------------------------------------------------------
//
//! This gives a standard variable to the file writter. 
//! This variable should not be related to the DIM service being logged. 
//! @param desc the description of the variable to add
//! @param dataFormat the FITS data format corresponding to the variable to add.
//! @param dataPointer the memory location where the variable is stored
//! @param numDataBytes the number of bytes taken by the variable
//
void Fits::AddStandardColumn(const Description& desc, const string &dataFormat, void* dataPointer, long unsigned int numDataBytes)
{
    //check if entry already exist
    for (vector<Description>::const_iterator it=fStandardColDesc.begin(); it != fStandardColDesc.end(); it++)
        if (it->name == desc.name)
            return;

    fStandardColDesc.push_back(desc);
    fStandardFormats.push_back(dataFormat);
    fStandardPointers.push_back(dataPointer);
    fStandardNumBytes.push_back(numDataBytes);
}

// --------------------------------------------------------------------------
//
//! This gives the file writter access to the DIM data
//! @param desc a vector containing the description of all the columns to log
//! @param dataFormat a vector containing the FITS data format of all the columsn to log
//! @param dataPointer the memory location where the DIM data starts
//! @param numDataBytes the number of bytes taken by the DIM data. 
//	
void Fits::InitDataColumns(const vector<Description> &desc, const vector<string>& dataFormat, void* dataPointer)
{
    fDataFormats = dataFormat;
    fDataPointer = dataPointer;

    //we will copy this information here. It duplicates the data, which is not great, but it is the easiest way of doing it right now
    if (desc.size() == dataFormat.size())
    {
        fDataColDesc = desc;
        return;
    }

    fDataColDesc.clear();
    for (unsigned int i=0;i<dataFormat.size();i++)
    {
        ostringstream stt;
        stt << "Data" << i;
        fDataColDesc.push_back(Description(stt.str(), "comment", "unit"));
    }
}

// --------------------------------------------------------------------------
//
//! This opens the FITS file (after the columns have been passed)
//! @param fileName the filename with complete or relative path of the file to open
//! @param tableName the name of the table that will receive the logged data.
//! @param file a pointer to an existing FITS file. If NULL, file will be opened and managed internally
//! @param fitsCounter a pointer to the integer keeping track of the opened FITS files
//! @param out a pointer to the MessageImp that should be used to log errors
//! @param runNumber the runNumber for which this file is opened. 0 means nightly file.
//
bool Fits::Open(const string& fileName, const string& tableName, uint32_t* fitsCounter, MessageImp* out, int runNumber, FITS* file)
{
    fRunNumber = runNumber;
    fMess = out;
    fFileName = fileName;

    if (fFile)
    {
        fMess->Error("File already open...");
        return false;
    }

    fFile = new FitsFile(*fMess);

    if (file == NULL)
    {
        if (!fFile->OpenFile(fileName, true))
            return false;

        fNumOpenFitsFiles = fitsCounter;
        (*fNumOpenFitsFiles)++;
    }
    else
    {
        if (!fFile->SetFile(file))
            return false;
    }

    //concatenate the standard and data columns
    //do it the inneficient way first: its easier and faster to code.
    for (unsigned int i=0;i<fStandardColDesc.size();i++)
    {
        fFile->AddColumn(fStandardColDesc[i].name, fStandardFormats[i],
                         fStandardColDesc[i].unit);
    }

    for (unsigned int i=0; i<fDataColDesc.size(); i++)
    {
        string name = fDataColDesc[i].name;
        if (name.empty())
        {
            ostringstream stt;
            stt << "Data" << i;
            name = stt.str();
        }

        fFile->AddColumn(name, fDataFormats[i], fDataColDesc[i].unit);
    }

    try
    {
        if (!fFile->OpenNewTable(tableName, 100))
        {
            Close();
            return false;
        }

        fCopyBuffer.resize(fFile->GetDataSize());

        return fFile->GetNumRows()==0 ? true : WriteHeaderKeys();
    }
    catch (const CCfits::FitsException &e)
    {
        fMess->Error("Opening or creating table '"+tableName+"' in '"+fileName+"': "+e.message());

        fFile->fTable = NULL;
        Close();
        return false;
    }
}

// --------------------------------------------------------------------------
//
//! This writes the standard header 
//
bool Fits::WriteHeaderKeys()
{
    if (!fFile->fTable)
        return false;

    if (!fFile->WriteDefaultKeys("datalogger"))
        return false;

    if (!fFile->WriteKeyNT("TSTART", "", "Time of the first receied data"))
        return false;

    if (!fFile->WriteKeyNT("TSTOP",  "", "Time of the last receied data"))
        return false;

    return true;
}
void Fits::MoveFileToCorruptedFile()
{
    ostringstream corruptName;
    struct stat st;
    int append = 0;
    corruptName << fFileName << "corrupt" << append;
    while (stat(corruptName.str().c_str(), &st))
    {
        append++;
        corruptName.str("");
        corruptName << fFileName << "corrupt" << append;
    }
    if (rename(fFileName.c_str(), corruptName.str().c_str()) != 0)
        fMess->Error("Could not rename file " + fFileName);
    else
        fMess->Message("Renamed file " + fFileName + " to " + corruptName.str());

}
// --------------------------------------------------------------------------
//
//! This writes one line of data to the file.
//! @param conv the converter corresponding to the service being logged
//
bool Fits::Write(const Converter &conv)
{
    //first copy the standard variables to the copy buffer
    int shift = 0;
    for (unsigned int i=0;i<fStandardNumBytes.size();i++)
    {
        const char *charSrc = reinterpret_cast<char*>(fStandardPointers[i]);
        reverse_copy(charSrc, charSrc+fStandardNumBytes[i], fCopyBuffer.data()+shift);
        shift += fStandardNumBytes[i];
    }

    try
    {
        //now take care of the DIM data. The Converter is here for that purpose
        conv.ToFits(fCopyBuffer.data()+shift, fDataPointer, fCopyBuffer.size()-shift);
    }
    catch (const runtime_error &e)
    {
        ostringstream str;
        str << fFile->GetName() << ": " << e.what();
        fMess->Error(str);
        return false;
    }

    // This is not necessary, is it?
    // fFile->fTable->makeThisCurrent();

    if (!fFile->AddRow())
    {
        Close();
        MoveFileToCorruptedFile();
        return false;
    }

    if (!fFile->WriteData(fCopyBuffer))
    {
        Close();
        return false;
    }

    //the first standard variable is the current MjD
    if (fEndMjD==0)
    {
        // FIXME: Check error?
        const double doubleValue = *reinterpret_cast<double*>(fStandardPointers[0]);
        fFile->WriteKeyNT("TSTART", Time(doubleValue).Iso(),
                          "Time of the first received data");
    }
    fEndMjD = *reinterpret_cast<double*>(fStandardPointers[0]);

    return true;
}

// --------------------------------------------------------------------------
//
//! This closes the currently openned FITS file. 
//! it also updates the header to reflect the time of the last logged row
//	
void Fits::Close() 
{
    if (!fFile)
        return;

    if (fFile->IsOpen() && fFile->IsOwner())
    {
        // FIMXE: Check for error? (It is allowed that fFile is NULL)
        fFile->WriteKeyNT("TSTOP", Time(fEndMjD).Iso(),
                          "Time of the last receied data");
    }

    if (fFile->IsOwner())
    {
        if (fNumOpenFitsFiles != NULL)
            (*fNumOpenFitsFiles)--;
    }

    const string name = fFile->GetName();

    delete fFile;
    fFile = NULL;

    fMess->Info("Closed: "+name);

    fMess = NULL;
}

// --------------------------------------------------------------------------
//! Returns the size on the disk of the Fits file being written.
int Fits::GetWrittenSize() const
{
    if (!IsOpen())
        return 0;

    struct stat st;
    if (stat(fFile->GetName().c_str(), &st))
        return 0;

    return st.st_size;
}

/*
 To be done:
 - Check the check for column names in opennewtable
 - If Open return false we end in an infinite loop (at least if
   the dynamic cats to Bintable fails.

*/
