/*******************************************************************************
 * JMMC project ( http://www.jmmc.fr ) - Copyright (C) CNRS.
 ******************************************************************************/
/**
 * @file
 * Functions to handle AMBER_SPECTRUM table.
 *
 */

/* 
 * System Headers
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include "fitsio.h"

/* 
 * Local Headers
 */
#include "amdlib.h"
#include "amdlibProtected.h"

/*
 * Local function declaration
 */
static void amdlibFreeSpectrum(amdlibSPECTRUM *spc);

/**
 * Allocate memory for storing spectra. 
 *
 * @param spc pointer to amdlibSPECTRUM structure.
 * @param nbTels number of telescopes 
 * @param nbWlen number of spectral channels 
 *
 * @return
 * amdlibSUCCESS on successful completion. Otherwise amdlibFAILURE is returned.
 */
amdlibCOMPL_STAT amdlibAllocateSpectrum(amdlibSPECTRUM *spc,
                                        const int      nbTels,
                                        const int      nbWlen)
{
    int i;

    amdlibLogTrace("amdlibAllocateSpectrum()");

    /* First free previous allocated memory */
    if (spc->thisPtr == spc)
    {
        amdlibFreeSpectrum(spc);
    }
    
    /* Init & Update thisPtr */
    spc->thisPtr =  memset(spc, '\0', sizeof(*spc));
    
    spc->nbTels = nbTels;
    spc->nbWlen = nbWlen;
    
    for (i=0; i < nbTels; i++)
    {
        spc->spec[i] = calloc(nbWlen, sizeof(double));
        if (spc->spec[i] == NULL)
        {
            amdlibFreeSpectrum(spc);
            return amdlibFAILURE;
        }
        spc->specErr[i] = calloc(nbWlen, sizeof(double));
        if (spc->specErr[i] == NULL)
        {
            amdlibFreeSpectrum(spc);
            return amdlibFAILURE;
        }
    }
    
    return amdlibSUCCESS;
}

/**
 * Release memory allocated to store spectra, and reset the structure
 * members to zero.
 *
 * @param spc pointer to amdlibSPECTRUM structure.
 */
void amdlibReleaseSpectrum(amdlibSPECTRUM *spc)
{
    amdlibLogTrace("amdlibReleaseSpectrum()");

    amdlibFreeSpectrum(spc);
    memset(spc, '\0', sizeof(amdlibSPECTRUM));
}

/**
 * Get spectra from P2VM. 
 * 
 * This function gets the spectra form the P2VM which corresponds to the
 * instrument transmission. If wave is given, it just gets the spectrum for
 * the wavelength of this table, otherwise all spectrum is copied.
 *
 * @param p2vm pointer to P2VM structure
 * @param wave pointer to the structure containing the wavelengths for which
 * spectrum has to be get. If NULL, all spectrum values are read.
 * @param spectrum pointer to spectrum structure
 * @param errMsg error description message returned if function fails.
 * 
 * @return 
 * amdlibSUCCESS on successful completion. Otherwise amdlibFAILURE is returned.  */
amdlibCOMPL_STAT amdlibGetSpectrumFromP2VM(amdlibP2VM_MATRIX *p2vm, 
                                           amdlibWAVELENGTH  *wave,
                                           amdlibSPECTRUM    *spectrum, 
                                           amdlibERROR_MSG   errMsg)
{
    int nbTels;
    int nbWlen;

    amdlibLogTrace("amdlibGetSpectrumFromP2VM()");

    /* Re-allocate spectrum structure */
    nbTels = (p2vm->type == amdlibP2VM_2T) ? 2 : 3;
    if (wave == NULL)
    {
        nbWlen = p2vm->nbChannels;
    }
    else
    {
        nbWlen = wave->nbWlen;
    }
    amdlibReleaseSpectrum(spectrum);
    if (amdlibAllocateSpectrum(spectrum, nbTels, nbWlen) != amdlibSUCCESS)
    {
        amdlibSetErrMsg("Could not allocate memory for spectra");
        return amdlibFAILURE;
    }

    /* Copy spectra */
    int l;
    int tel;
    for (l = 0; l < spectrum->nbWlen; l++)   
    {

        int lP2vm = 0;
        if (wave == NULL)
        {
            lP2vm = l;
        }
        else
        {
            /* Look for wavelength in P2VM */
            while (wave->wlen[l] != p2vm->wlen[lP2vm])
            {
                if (lP2vm == p2vm->nbChannels)
                {
                    amdlibSetErrMsg("P2VM does not cover wavelength %.3f",
                                    wave->wlen[l]);
                    return amdlibFAILURE;
                }
                lP2vm++;
            } 
        }

        for (tel = 0; tel < nbTels; tel++)
        {
            /* P2VM photoemtry is the mean flux of all the frames, then go
             * back to the total flux */
            spectrum->spec[tel][l] = p2vm->photometryPt[0][tel][lP2vm] *
                p2vm->nbFrames;
            /* Neglect detector read-out noise, and prevent against neagtive
             * flux */
            spectrum->specErr[tel][l] = sqrt(fabs(spectrum->spec[tel][l]));
        }
    }

    return amdlibSUCCESS;
}

/**
 * amdlibSUCCESS on successful completion. Otherwise amdlibFAILURE is returned.  */

amdlibCOMPL_STAT amdlibGetAndNormalizeSpectrumFromScienceData
                                                 (amdlibSCIENCE_DATA *data, 
                                                  amdlibP2VM_MATRIX  *p2vm,
                                                  amdlibWAVEDATA     *waveData,
                                                  amdlibWAVELENGTH   *wave,
                                                  amdlibSPECTRUM     *spectrum, 
                                                  amdlibERROR_MSG    errMsg)
{
    int nbTels;
    int nbWlen;
    int l;
    int tel;
    amdlibDOUBLE fluxMax;

    amdlibLogTrace("amdlibGetAndNormalizeSpectrumFromScienceData()");

    /* Re-allocate spectrum structure */
    nbTels = (data->nbCols == 3) ? 2 : 3;
    if (wave == NULL)
    {
        nbWlen = data->nbChannels;
    }
    else
    {
        nbWlen = wave->nbWlen;
    }
    if (spectrum->thisPtr == spectrum)
    {
        amdlibFreeSpectrum(spectrum);
    }

    if (amdlibAllocateSpectrum(spectrum, nbTels, nbWlen) != amdlibSUCCESS)
    {
        amdlibSetErrMsg("Could not allocate memory for spectra");
        return amdlibFAILURE;
    }

    if (p2vm != NULL)
    {
       if(amdlibGetSpectrumFromP2VM(p2vm,wave,spectrum,errMsg)!= amdlibSUCCESS)
       {
           return amdlibFAILURE;
       }
       /* convert this spectrum as a transmission: */
       for (tel = 0; tel < nbTels; tel++)
        {
            fluxMax=spectrum->spec[tel][0];
            for (l = 1; l < spectrum->nbWlen; l++)   
            {
                fluxMax=amdlibMax(fluxMax,spectrum->spec[tel][l]);
            }
            for (l = 0; l < spectrum->nbWlen; l++)   
            {
                spectrum->spec[tel][l]/=fluxMax;
            }
        }
    }
    else
    {
       for (tel = 0; tel < nbTels; tel++)
        {
            for (l = 0; l < spectrum->nbWlen; l++)   
            {
                spectrum->spec[tel][l]=1.0;
            }
        }
    }
    /* Copy spectra */
    for (l = 0; l < spectrum->nbWlen; l++)   
    {
        double spec[amdlibNB_TEL];
        int frame;
        int lData = 0;
        if (wave == NULL)
        {
            lData = l;
        }
        else
        {
            while (wave->wlen[l] != waveData->wlen[data->channelNo[lData]])
            {
                if (lData == data->nbChannels)
                {
                    amdlibSetErrMsg("Science data does not contain wavelength "
                                    "%.3f", wave->wlen[l]);
                    return amdlibFAILURE;
                }
                lData++;
            } 
        }

        /* Compute beam spectra over all frames */
        for (tel = 0; tel < nbTels; tel++)
        {
            spec[tel]    = 0.0;
        }
        for (frame = 0; frame < data->nbFrames; frame++)
        {
            spec[0] += data->frame[frame].photo1[lData];
            spec[1] += data->frame[frame].photo2[lData];
            if (nbTels == 3)
            {
                spec[2] += data->frame[frame].photo3[lData];
            }
        }
        /* and copy them after normalisation with transmission */
        for (tel = 0; tel < nbTels; tel++)
        {
            spectrum->spec[tel][l] = spec[tel]/spectrum->spec[tel][l];
        }
        /* Compute beam spectra sigma2 over all frames */
        for (tel = 0; tel < nbTels; tel++)
        {
            spec[tel]    = 0.0;
        }
        for (frame = 0; frame < data->nbFrames; frame++)
        {
            spec[0] += data->frame[frame].photo1[lData];
            spec[1] += data->frame[frame].photo2[lData];
            if (nbTels == 3)
            {
                spec[2] += data->frame[frame].photo3[lData];
            }
        }
        /* and copy them */
        for (tel = 0; tel < nbTels; tel++)
        {
            spectrum->specErr[tel][l] = amdlibSignedSqrt(spec[tel]);
        }
    }
    return amdlibSUCCESS;
}

/**
 * Get spectra from science data. 
 * 
 * This function gets the spectra form the science data which corresponds to
 * the object spectra. If wave is given, it just gets the spectrum for the
 * wavelengths of this table, otherwise all spectrum is copied.
 *
 * @param data structure containing the science observation (where the fringes 
 * are)
 * @param waveData structure containing spectral dispersion table 
 * @param wave pointer to the structure containing the wavelengths for which
 * spectrum has to be get. If NULL, all spectrum values are read.
 * @param spectrum pointer to spectrum structure
 * @param errMsg error description message returned if function fails.
 * 
 * @return 
 * amdlibSUCCESS on successful completion. Otherwise amdlibFAILURE is returned.  */
amdlibCOMPL_STAT amdlibGetSpectrumFromScienceData(amdlibSCIENCE_DATA *data, 
                                                  amdlibWAVEDATA     *waveData,
                                                  amdlibWAVELENGTH   *wave,
                                                  amdlibSPECTRUM     *spectrum, 
                                                  amdlibERROR_MSG    errMsg)
{
    int nbTels;
    int nbWlen;

    amdlibLogTrace("amdlibGetSpectrumFromScienceData()");

    /* Re-allocate spectrum structure */
    nbTels = (data->nbCols == 3) ? 2 : 3;
    if (wave == NULL)
    {
        nbWlen = data->nbChannels;
    }
    else
    {
        nbWlen = wave->nbWlen;
    }
    if (spectrum->thisPtr == spectrum)
    {
        amdlibFreeSpectrum(spectrum);
    }

    if (amdlibAllocateSpectrum(spectrum, nbTels, nbWlen) != amdlibSUCCESS)
    {
        amdlibSetErrMsg("Could not allocate memory for spectra");
        return amdlibFAILURE;
    }

    /* Copy spectra */
    int l;
    for (l = 0; l < spectrum->nbWlen; l++)   
    {
        double spec[amdlibNB_TEL];
        int tel;
        int frame;
        int lData = 0;
        if (wave == NULL)
        {
            lData = l;
        }
        else
        {
            while (wave->wlen[l] != waveData->wlen[data->channelNo[lData]])
            {
                if (lData == data->nbChannels)
                {
                    amdlibSetErrMsg("Science data does not contain wavelength "
                                    "%.3f", wave->wlen[l]);
                    return amdlibFAILURE;
                }
                lData++;
            } 
        }

        /* Compute beam spectra over all frames */
        for (tel = 0; tel < nbTels; tel++)
        {
            spec[tel]    = 0.0;
        }
        for (frame = 0; frame < data->nbFrames; frame++)
        {
            spec[0] += data->frame[frame].photo1[lData];
            spec[1] += data->frame[frame].photo2[lData];
            if (nbTels == 3)
            {
                spec[2] += data->frame[frame].photo3[lData];
            }
        }
        /* and copy them */
        for (tel = 0; tel < nbTels; tel++)
        {
            spectrum->spec[tel][l]    = spec[tel];
        }
        /* Compute beam spectra sigma2 over all frames */
        for (tel = 0; tel < nbTels; tel++)
        {
            spec[tel]    = 0.0;
        }
        for (frame = 0; frame < data->nbFrames; frame++)
        {
            spec[0] += data->frame[frame].photo1[lData];
            spec[1] += data->frame[frame].photo2[lData];
            if (nbTels == 3)
            {
                spec[2] += data->frame[frame].photo3[lData];
            }
        }
        /* and copy them */
        for (tel = 0; tel < nbTels; tel++)
        {
            spectrum->specErr[tel][l] = amdlibSignedSqrt(spec[tel]);
        }
    }
    return amdlibSUCCESS;
}

/**
 * Write AMBER_SPECTRUM table in OI-FITS file.
 *
 * This function writes the AMBER_SPECTRUM binary table in the OI-FITS file
 * given as parameter.
 *
 * @param filePtr pointer on the file to be created.
 * @param wave OI_WAVELENGTH produced.
 * @param spc amdlibSPECTRUM produced.
 * @param errMsg error description message returned if function fails.
 *
 * @return
 * amdlibSUCCESS on successful completion. Otherwise amdlibFAILURE is returned.
 */
amdlibCOMPL_STAT amdlibWriteAmberSpectrum(fitsfile         *filePtr,
                                          amdlibWAVELENGTH *wave,
                                          amdlibSPECTRUM   *spc,
                                          amdlibERROR_MSG  errMsg)
{
    int        status = 0;
    char       fitsioMsg[256];
    const int  tfields = 4;
    char       *Ttype[] = {"EFF_WAVE", "EFF_BAND", "SPECTRUM", "SPECTRUM_ERROR"};
    char       *Tform[] = {"E", "E", "?D", "?D"};
    char       *Tunit[] = {"m", "m", "\0", "\0"};
    char       *ttype[tfields];
    char       *tform[tfields];
    char       *tunit[tfields];
    
    char       extname[] = "AMBER_SPECTRUM";
    int        revision  = amdlib_OI_REVISION;
    int        i, l;
    char       tmp[16];
    double     specDouble[amdlibNB_TEL];
    double     specErrDouble[amdlibNB_TEL];
    char       insName[amdlibKEYW_VAL_LEN+1];

    amdlibLogTrace("amdlibWriteAmberSpectrum()");

    if (spc->thisPtr != spc) 
    {
        amdlibSetErrMsg("Unitialized spc structure");
        return amdlibFAILURE;
    }

    /* If initialized but empty, do nothing gracefully */
    if (spc->nbWlen < 1)
    {
        amdlibLogTrace("amdlibWriteAmberSpectrum done");
        return amdlibSUCCESS;
    }
    
    /* Create table structure. Make up Tform: substitute spc->nbTels for '?' */
    for (i = 0; i < tfields; i++) 
    {
        ttype[i] = calloc(strlen(Ttype[i]) + 1, sizeof(char));
        strcpy(ttype[i], Ttype[i]);
        tunit[i] = calloc(strlen(Tunit[i]) + 1, sizeof(char));
        strcpy(tunit[i], Tunit[i]);
        
        if (Tform[i][0] == '?') 
        {
            sprintf(tmp, "%d%s", spc->nbTels, &Tform[i][1]);
        }
        else
        {
            strcpy(tmp, Tform[i]);
        }
        tform[i] = calloc((strlen(tmp) + 1), sizeof(char));
        strcpy(tform[i], tmp);
    }

    if (fits_create_tbl(filePtr, BINARY_TBL, 0, tfields, ttype, tform, tunit,
                        extname, &status)) 
    {
        amdlibReturnFitsError("Creating Binary Table");
    }

    for (i = 0; i < tfields; i++)
    {
        free(ttype[i]);
        free(tform[i]);
        free(tunit[i]);
    }
    
    /* Write revision number of the table definition */
    if (fits_write_key (filePtr, TINT, "AMB_REVN", &revision,
                        "Revision number of the table definition", &status))
    {
        amdlibReturnFitsError("AMB_REVN");
    }

    /* Write spectral setup unique identifier */
    sprintf(insName, "AMBER");
    if (fits_write_key (filePtr, TSTRING, "INSNAME", insName,
                        "Instrument name", &status)) 
    {
        amdlibReturnFitsError("INSNAME");
    }

    for (l=0; l < wave->nbWlen; l++)
    {
        amdlibDOUBLE waveInM;
        waveInM = wave->wlen[l] * amdlibNM_TO_M; /* Convert nm to m */
        if (fits_write_col(filePtr, TDOUBLE, 1, l+1, 1, 1, &waveInM, &status))
        {
            amdlibReturnFitsError("EFF_WAVE");
        }

        amdlibDOUBLE bandInM;
        bandInM = wave->bandwidth[l] * amdlibNM_TO_M; /* Convert nm to m */
        if (fits_write_col(filePtr, TDOUBLE, 2, l+1, 1, 1, &bandInM, &status))
        {
            amdlibReturnFitsError("EFF_BAND");
        }

        for (i=0; i < spc->nbTels; i++)
        { 
            specDouble[i] = spc->spec[i][l];
            specErrDouble[i] = spc->specErr[i][l];
        }
        if (fits_write_col(filePtr, TDOUBLE, 3, l+1, 1, spc->nbTels,
                           specDouble, &status))
        {
            amdlibReturnFitsError("SPECTRUM");
        }
        
        if (fits_write_col(filePtr, TDOUBLE, 4, l+1, 1, spc->nbTels,
                           specErrDouble, &status))
        {
            amdlibReturnFitsError("SPECTRUM_ERROR");
        }
    }

    amdlibLogTrace("amdlibWriteAmberSpectrum done");
    return amdlibSUCCESS;
}

/**
 * Read AMBER_SPECTRUM table in OI-FITS file.
 *
 * This function reads the AMBER_SPECTRUM binary table in the OI-FITS file 
 * given as parameter.
 *
 * @param filePtr pointer on the file to be created.
 * @param nbBases input parameter giving the number of telescopes.
 * @param spectrum AMBER_DATA structure where read data will be stored.
 * @param errMsg error description message returned if function fails.
 *
 * @return
 * amdlibSUCCESS on successful completion. Otherwise amdlibFAILURE is returned.
 */
amdlibCOMPL_STAT amdlibReadAmberSpectrum(fitsfile         *filePtr,
                                         int              nbTels,
                                         amdlibSPECTRUM   *spectrum,
                                         amdlibERROR_MSG  errMsg)
{
    char       fitsioMsg[256];
    int        status = 0;
    int        specCol, specErrCol;
    double     specDouble[amdlibNB_TEL];
    double     specErrDouble[amdlibNB_TEL];
    
    amdlibLogTrace("amdlibReadAmberSpectrum()");

    /* Release previously allocated memory */
    amdlibReleaseSpectrum(spectrum);

    /* Check there is a AMBER_SPECTRUM binary table */
    if (fits_movnam_hdu(filePtr, BINARY_TBL, "AMBER_SPECTRUM", 0, &status))
    {
        amdlibReturnFitsError("AMBER_SPECTRUM");
    }

    /* Get number of channels */
    long nbWlen;
    if (fits_get_num_rows(filePtr, &nbWlen, &status))
    {
        amdlibReturnFitsError(
                              "Getting the number of spectral channels");
    }

    /* Allocate data structure and array to read values */
    if (amdlibAllocateSpectrum(spectrum, nbTels, nbWlen) != amdlibSUCCESS)
    {
        amdlibSetErrMsg("Could not allocate memory for spectra");
        return amdlibFAILURE;
    }

    /* Get column numbers for spectra and associated error */
    if (fits_get_colnum(filePtr, CASEINSEN, "SPECTRUM", &specCol, &status))
    {
        amdlibReturnFitsError("SPECTRUM");
    }
    if (fits_get_colnum(filePtr, CASEINSEN, "SPECTRUM_ERROR", &specErrCol,
                        &status))
    {
        amdlibReturnFitsError("SPECTRUM_ERROR");
    }

    /* Read data */
    double nulldouble = 0.0;
    int    l, anynull;
    for (l=1; l <= spectrum->nbWlen; l++) 
    {
        int tel;

        if (fits_read_col(filePtr, TDOUBLE, specCol, l, 1, nbTels,
                          &nulldouble, specDouble, &anynull, &status))
        {
            amdlibReturnFitsError("reading spectrum");
        }
        if (fits_read_col(filePtr, TDOUBLE, specErrCol, l, 1, nbTels,
                          &nulldouble, specErrDouble, &anynull, &status))
        {
            amdlibReturnFitsError("reading spectrum error");
        }

        /* Copy back in structure */
        for (tel = 0; tel < nbTels; tel++)
        {
            spectrum->spec[tel][l-1]    = specDouble[tel];
            spectrum->specErr[tel][l-1] = specErrDouble[tel];
        }
    }

    return amdlibSUCCESS;
}

/*
 * Local functions
 */
/**
 * Free memory allocated for spectrum. 
 *
 * @param spc pointer to amdlibSPECTRUM structure.
 */
void amdlibFreeSpectrum(amdlibSPECTRUM *spc)
{
    int i;
    
    amdlibLogTrace("amdlibFreeSpectrum()");

    /* Check thisPtr and do nothing if uninitialized */
    if (spc->thisPtr != spc)
    {
        return;
    }
    
    /* Spectrum table */
    for (i=0; i < spc->nbTels; i++)
    {
        if (spc->spec[i] != NULL)
        {
            free(spc->spec[i]);
        }
        if (spc->specErr[i] != NULL)
        {
            free(spc->specErr[i]);
        }
    }
    spc->thisPtr = NULL;
}

/**
 * Add source spectrum to dest spectrum in the data structure. 
 *
 * @param srcSpectrum pointer to source data structure 
 * @param dstSpectrum pointer to destination data structure (append to) 
 * @param errMsg error description message returned if function fails.
 *
 * @return
 * amdlibSUCCESS on successful completion. Otherwise amdlibFAILURE is returned.
 */
amdlibCOMPL_STAT amdlibAddSpectrum(amdlibSPECTRUM *dstSpectrum, 
                                   amdlibSPECTRUM *srcSpectrum, 
                                   amdlibERROR_MSG   errMsg)
{
    int wlen,tel;
    int nbWlen=dstSpectrum->nbWlen;
    int nbTels=dstSpectrum->nbTels;
    double err;

    amdlibLogTrace("amdlibAddSpectrum()");
    /* Perform simple checks */
    if (nbWlen != srcSpectrum->nbWlen)
    {
        amdlibSetErrMsg("Different numbers of "
                        "wavelengths (%d and %d) ", srcSpectrum->nbWlen,
                        nbWlen);
        return amdlibFAILURE;
    }
    if (nbTels != srcSpectrum->nbTels)
    {
        amdlibSetErrMsg("Different numbers of "
                        "Telescopes (%d and %d) ", srcSpectrum->nbTels,
                        nbTels);
        return amdlibFAILURE;
    }

    for (tel=0; tel<nbTels; tel++)
    {
        for (wlen=0; wlen<nbWlen; wlen++)
        {
            err=amdlibPow2(dstSpectrum->specErr[tel][wlen]+
                           srcSpectrum->specErr[tel][wlen]);
            dstSpectrum->specErr[tel][wlen]=amdlibSignedSqrt(err);
            dstSpectrum->spec[tel][wlen]+=srcSpectrum->spec[tel][wlen];
        }
    }
    
    return amdlibSUCCESS;
}

