/** @file FitsGenerator.c
 *
 *  @ingroup ippToPsps
 *
 *  @author IfA
 *  Copyright 2011 Institute for Astronomy, University of Hawaii
 */

#include "FitsGenerator.h"
#include <ctype.h>
#include <libxml/parser.h>
#include <libxml/tree.h>

#include "Fits.h"

/**
  Finds a column within this table
  */
static Column* findColumn(FitsGenerator* this, Table* table, const char* name) {

    Column* column = NULL;

    for (uint32_t i=0; i<table->numOfColumns; i++) {

        if (strcmp(table->columns[i].pspsName, name) == 0) {
            column = table->columns+i;
            break;
        }
    }

    if (!column) this->logger->print(this->logger, MSG_ERROR, "FitsGenerator", 
            "Could not find column '%s' in table '%s'\n", name, table->name);

    return column;
}

/**
  Finds a table from the array of tables
  */
static Table* findTable(FitsGenerator* this, const char* name) {

    Table* table = NULL;

    for (uint32_t i=0; i<this->numOfTables; i++) {

        if (strcmp(this->tables[i].name, name) == 0) {
            table = this->tables+i;
            break;
        }
    }

    if (!table) this->logger->print(this->logger, MSG_ERROR, "FitsGenerator", 
            "Could not find table '%s'\n", name);

    return table;
}

/**
  Gets an attribute value from XML node
  */
static bool getAttribute(xmlNode* node, const char* attName, char* _value) {

    xmlChar* value = xmlGetProp(node, (const xmlChar*)attName);
    if (!value) return false;
    sprintf(_value, "%s", value);
    xmlFree(value);

    return true;
}

/**
  Creates a FITS binary table and populates it with defaults based on definition in schema XML
  */
static bool createTable(Table* table, Fits* fitsOut, const long nRows) {

    if (!table) return false;
    if (nRows < 1) return false;

    long nCols = table->numOfColumns;

    char** colNames = (char**)calloc(nCols, sizeof(char*));
    char** colTypes = (char**)calloc(nCols, sizeof(char*));

    uint32_t i = 0;
    for (i=0;i<nCols;i++) colTypes[i] = (char*)calloc(5,sizeof(char));

    i = 0;
    for (uint32_t m=0; m<table->numOfColumns; m++) {

        colNames[i] = table->columns[i].pspsName;

        //        sprintf(colNames[i], table->columns[i].pspsName);

        switch (table->columns[i].pspsType) {

            case TBYTE: sprintf(colTypes[i],"1B");
                        break;
            case TSHORT: sprintf(colTypes[i],"1I");
                         break;
            case TLONG: sprintf(colTypes[i],"1J"); // TODO
                        break;
            case TLONGLONG: sprintf(colTypes[i],"1K");
                            break;
            case TFLOAT: sprintf(colTypes[i],"1E");
                         break;
            case TDOUBLE: sprintf(colTypes[i],"1D");
                          break;
            case TSTRING: sprintf(colTypes[i],"32A"); // TODO width?
                          break;

            default:
                          break;
        }

        i++;
    }

    fitsOut->createBinaryTable(fitsOut, nRows, nCols, colNames, colTypes, table->name);

    for (i=0;i<nCols;i++) free(colTypes[i]);
    free(colTypes);
    free(colNames);

    // now insert defaults
    int8_t* int8col = (int8_t*)calloc(nRows, sizeof(int8_t));
    int16_t* int16col = (int16_t*)calloc(nRows, sizeof(int16_t));
    long* longcol = (long*)calloc(nRows, sizeof(long));
    float* floatcol = (float*)calloc(nRows, sizeof(float));
    double* doublecol = (double*)calloc(nRows, sizeof(double));
    char** strcol = (char**)calloc(nRows, sizeof(char**));
    for (uint32_t i=0; i<nRows;i++)
        strcol[i] = (char*)calloc(20,sizeof(char)); // TODO 20? size issue

    uint32_t j;
    int64_t tempLong;
    double tempDouble;
    char tempStr[20];

    int col;
    for (uint32_t i=0; i<table->numOfColumns; i++) {

        col = i+1;

        strcpy(tempStr,table->columns[i].defaultValue);
        tempLong = atol(tempStr);
        tempDouble = atof(tempStr);

        switch (table->columns[i].pspsType) {

            case TBYTE:
                for(j=0;j<nRows;j++) int8col[j] = (int8_t)tempLong;
                fitsOut->writeColumn(fitsOut, TBYTE, col, 1, 1, nRows, int8col);
                break;
            case TSHORT:
                for(j=0;j<nRows;j++) int16col[j] = (int16_t)tempLong;
                fitsOut->writeColumn(fitsOut, TSHORT, col, 1, 1, nRows, int16col);
                break;
            case TLONG:
                for(j=0;j<nRows;j++) longcol[j] = (long)tempLong;
                fitsOut->writeColumn(fitsOut, TLONG, col, 1, 1, nRows, longcol);
                break;

            case TLONGLONG:
                for(j=0;j<nRows;j++) longcol[j] = tempLong;
                fitsOut->writeColumn(fitsOut, TLONGLONG, col, 1, 1, nRows, longcol);
                break;

            case TFLOAT:
                for(j=0;j<nRows;j++) floatcol[j] = (float)tempDouble;
                fitsOut->writeColumn(fitsOut, TFLOAT, col, 1, 1, nRows, floatcol);
                break;

            case TDOUBLE:
                for(j=0;j<nRows;j++) doublecol[j] = tempDouble;
                fitsOut->writeColumn(fitsOut, TDOUBLE, col, 1, 1, nRows, doublecol);
                break;

            case TSTRING:
                for(j=0;j<nRows;j++) strcpy(strcol[j], tempStr);
                fitsOut->writeColumn(fitsOut, TSTRING, col, 1, 1, nRows, strcol);
                break;

            default:
                break;
        }
    }

    free(int8col);
    free(int16col);
    free(longcol);
    free(floatcol);
    free(doublecol);
    for (uint32_t i=0; i<nRows;i++) free(strcol[i]);
    free(strcol);

    return true;
}

/*
   Counts number of children with the provided name
   */
static int countChildren(xmlNode* rootNode, const char* name) {

    int count = 0;
    xmlNode* node = NULL;

    for (node = rootNode->children; node; node = node->next) {

        if (strcmp((const char*)node->name, name)==0) count++;
    } 

    return count;
}

/*
   opens an XML file and returns the document
   */
static xmlDoc* openXmlFile(FitsGenerator* this, const char* path) {

    xmlDoc* doc = xmlReadFile(path, NULL, 0);
    if (doc == NULL) this->logger->print(this->logger, MSG_ERROR, "FitsGenerator", 
            "Unable to open XML file at %s\n", path);
    return doc;
}

/*
   closes an XML file 
   */
static bool closeXmlFile(xmlDoc* doc) {

    xmlFreeDoc(doc);
    xmlCleanupParser();

    return true;
}

/**
  Populates the provided table with data from XML
  */
static bool populateTable(FitsGenerator* this, Table* table, xmlNode* rootElement, Fits *fitsOut) {

    char tempStr[100];
    xmlNode* node = NULL;
    xmlNode* tableNode = NULL;

    // first check we have data for this table in XML file
    for (node = rootElement->children; node; node = node->next) {
        if (node->type == XML_ELEMENT_NODE) {

            if (strcmp((const char*)node->name, "table")!=0) continue;
            if (!getAttribute(node, "name", tempStr)) continue;
            if (strcmp(tempStr, table->name)==0) {tableNode = node; break;}
        }
    }

    if(!tableNode) {

        this->logger->print(this->logger, MSG_ERROR, "FitsGenerator", 
                "Could not find table: %s\n", table->name);
        return false;
    }

    // now count the number of rows
    int nRows = countChildren(tableNode, "row");
    if (nRows < 1) return false;
    if (!createTable(table, fitsOut, nRows)) return false;

    int rows=0;
    int status;
    char** strcol = (char**)calloc(1, sizeof(char**));
    strcol[0] = (char*)calloc(32,sizeof(char)); // TODO 20? size issue

    int8_t* bytecol = calloc(1, sizeof(uint8_t));
    int16_t* shortcol = calloc(1, sizeof(uint16_t));
    long* longcol = calloc(1, sizeof(long));
    double* doublecol = calloc(1, sizeof(double));
    float* floatcol = calloc(1, sizeof(float));

    int col;

    // now get rows
    for (node = tableNode->children; node; node = node->next) {

        if (strcmp((const char*)node->name, "row")!=0) continue;

        rows++;

        // get column data for this row
        for (int i=0;i<table->numOfColumns;i++) {

            col = i+1;

            if (!getAttribute(node, table->columns[i].pspsName, tempStr)) continue;

            strncpy(strcol[0], tempStr, 32);
            status=0;
            //psLogMsg("ippToPsps", PS_LOG_INFO, "Values %s - %ld - %f - %d", strcol[0], longcol[0], doublecol[0], bytecol[0]);
            switch (table->columns[i].pspsType) {

                case TBYTE:
                    bytecol[0] = atoi(tempStr);
                    fitsOut->writeColumn(fitsOut, table->columns[i].pspsType, col, rows, 1, 1, bytecol); 
                    break;
                case TSHORT:
                    shortcol[0] = atoi(tempStr);
                    fitsOut->writeColumn(fitsOut, table->columns[i].pspsType, col, rows, 1, 1, shortcol); 
                    break;
                case TLONG:
                case TLONGLONG:
                    longcol[0] = atol(tempStr);
                    fitsOut->writeColumn(fitsOut, table->columns[i].pspsType, col, rows, 1, 1, longcol); 
                    break;
                case TFLOAT:
                    floatcol[0] = atof(tempStr);
                    fitsOut->writeColumn(fitsOut, table->columns[i].pspsType, col, rows, 1, 1, floatcol);
                    break;
                case TDOUBLE:
                    doublecol[0] = atof(tempStr);
                    fitsOut->writeColumn(fitsOut, table->columns[i].pspsType, col, rows, 1, 1, doublecol);
                    break;
                case TSTRING:
                    fitsOut->writeColumn(fitsOut, table->columns[i].pspsType, col, rows, 1, 1, strcol); 
                    break;

            }
            if (status) this->logger->print(this->logger, MSG_ERROR, "FitsGenerator", 
                    "Unable to write value of %s to column %s in table %s\n", 
                    tempStr, table->columns[i].pspsName, table->name);
        }
    }

    free(bytecol);
    free(shortcol);
    free(longcol);
    free(floatcol);
    free(doublecol);
    free(strcol[0]);
    free(strcol);

    return true;
}

/**
  Loads a table description from XML
  */
static bool loadTableDescription(FitsGenerator* this, Table* table, xmlNode* tableNode) {

    bool ret = true;
    char tableName[100];
    getAttribute(tableNode, "name", tableName);
    strcpy(table->name, tableName);

    // count how many columns we have coming in and allocate memory for them
    table->numOfColumns = countChildren(tableNode, "column");
    table->columns = calloc(table->numOfColumns, sizeof(Column));
    //    psLogMsg("ippToPsps", PS_LOG_INFO, " Found '%d' columns", table->numOfColumns);

    char buffer[200];
    xmlNode* node;
    int columnNum = 0;

    for (node = tableNode->children; node; node = node->next) {

        if (strcmp((const char*)node->name, "column") != 0) continue;

        table->columns[columnNum].usingDefault = true;

        // column name
        getAttribute(node, "name", buffer);
        sprintf(table->columns[columnNum].pspsName, buffer);

        // column type
        getAttribute(node, "type", buffer);
        table->columns[columnNum].pspsType = Fits_getDataType(buffer);

        // default value
        getAttribute(node, "default", buffer);
        sprintf(table->columns[columnNum].defaultValue, buffer);

        // comment
        getAttribute(node, "comment", buffer);
        sprintf(table->columns[columnNum].comment, buffer);

        # if 0
        psLogMsg("ippToPsps", PS_LOG_INFO, "'%s'  name='%s' type='%d'  default='%s' comment='%s'", 
                table->name,
                table->columns[columnNum].pspsName, 
                table->columns[columnNum].pspsType, 
                table->columns[columnNum].defaultValue, 
                table->columns[columnNum].comment);
# endif
        columnNum++;
    }   


    if (columnNum < 1) {

        this->logger->print(this->logger, MSG_ERROR, "FitsGenerator", 
                "Found no columns for table '%s'\n", tableName);
        ret = false;
    }

    if (columnNum != table->numOfColumns)
        this->logger->print(this->logger, MSG_ERROR, "FitsGenerator", 
                "Mismatch between number of columns expected (%d) and those found (%d)\n", 
                table->numOfColumns, columnNum);

    return ret;
}

/**
  Reads table shapes from XML file
  */
static bool loadTableDescriptions(FitsGenerator* this) {

    bool ret = true;

    psString path = NULL;
    psStringAppend(&path, "%s/tables.xml", this->configsPath);

    xmlDoc* doc = openXmlFile(this, path);

    if (doc == NULL) {

        psFree(path);
        return false;
    }
    psFree(path);

    xmlNode* rootElement = xmlDocGetRootElement(doc);

    if (strcmp((const char*)rootElement->name, "tableDescriptions")!=0) {

        this->logger->print(this->logger, MSG_ERROR, "FitsGenerator", 
                "Root node of XML is not 'tableDescriptions', as it should be\n");
        return false;
    }

    // count how many tables we have coming in and allocate memory for them
    this->numOfTables = countChildren(rootElement, "table");
    this->tables = calloc(this->numOfTables, sizeof(Table));
    //    psLogMsg("ippToPsps", PS_LOG_INFO, " Found '%d' tables", this->numOfTables);

    xmlNode* node = NULL;
    Table* table = NULL;

    int tableNum = 0;
    // loop round all available table descriptions
    for (node = rootElement->children; node; node = node->next) {
        if (node->type == XML_ELEMENT_NODE) {

            if (strcmp((const char*)node->name, "table")==0) {

                table = this->tables+tableNum;

                if (!loadTableDescription(this, table, node)) ret = false;
                else tableNum++;
            }
        }
    }

    if (tableNum != this->numOfTables)
        this->logger->print(this->logger, MSG_ERROR, "FitsGenerator", 
                "Mismatch between number of tables expected (%d) and those found (%d)\n", 
                this->numOfTables, tableNum);

    closeXmlFile(doc);

    return ret;
}

/**
  Loads mappings from XML for a particular table
  */
static bool loadTableMappings(FitsGenerator* this, xmlNode* tableNode) {

    bool ret = false;

    xmlNode* node = NULL;

    char tableName[100];
    getAttribute(tableNode, "name", tableName);

    Table* table = findTable(this, tableName);
    Column* column = NULL;
    if (!table) return false;

    char buffer[100];
    //psLogMsg("ippToPsps", PS_LOG_INFO, "Table '%s'", tableName);

    // loop round all available mappings for this table
    for (node = tableNode->children; node; node = node->next) {
        if (node->type == XML_ELEMENT_NODE) {

            if (strcmp((const char*)node->name, "map")==0) {

                // column type
                getAttribute(node, "pspsName", buffer);
                column = findColumn(this, table, buffer);

                if (!column) continue;

                // IPP name
                getAttribute(node, "ippName", buffer);
                strcpy(column->ippName, buffer);

                // IPP type
                getAttribute(node, "ippType", buffer);
                column->ippType = Fits_getDataType(buffer); 

                column->usingDefault = false;

                //psLogMsg("ippToPsps", PS_LOG_INFO, "...mapping PSPS:'%s' to '%s' '%d'", column->pspsName, column->ippName, column->ippColNum );
            }
        }
    }

    return ret;
}

/**
  Reads table mappings from XML file
  */
static bool loadMappings(FitsGenerator* this) {

    bool ret = true;

    char path[1000];
    sprintf(path, "%s/map.xml", this->configsPath);

    struct stat sts;
    if ((stat(path, &sts)) == -1) {
        this->logger->print(this->logger, MSG_INFO, "FitsGenerator", 
                "No map file found at '%s'\n", path);
        return false;
    }

    xmlDoc* doc = openXmlFile(this, path);

    if (doc == NULL) {
        
        this->logger->print(this->logger, MSG_ERROR,"FitsGenerator",  
                "Could not create XML document from '%s'\n", path);
        return false;
    }

    this->logger->print(this->logger, MSG_INFO, "FitsGenerator", 
            "Using map file at '%s'\n", path);
    xmlNode* rootElement = xmlDocGetRootElement(doc);

    if (strcmp((const char*)rootElement->name, "tabledata")!=0) {
        this->logger->print(this->logger, MSG_ERROR,"FitsGenerator",  
                "Root node of XML is not 'tabledata', as it should be\n");
        return false;
    }

    xmlNode* node = NULL;

    // loop round all available tables
    for (node = rootElement->children; node; node = node->next) {
        if (node->type == XML_ELEMENT_NODE) {

            if (strcmp((const char*)node->name, "table")==0) {

                if (!loadTableMappings(this, node)) ret = false;
            }
        }
    }

    closeXmlFile(doc);

    return ret;
}

/**
  Populate the provided table with data from XML
  */
static bool populateFromFile(FitsGenerator* this, Fits *fitsOut) {

    bool ret = true;
    psString path = NULL;
    psStringAppend(&path, "%s/data.xml", this->configsPath);

    xmlDoc* doc = openXmlFile(this, path);

    if (doc == NULL) {
        psFree(path);
        return false;
    }
    psFree(path);

    xmlNode* rootElement = xmlDocGetRootElement(doc);

    if (strcmp((const char*)rootElement->name, "tabledata")!=0) {
        this->logger->print(this->logger, MSG_ERROR, "FitsGenerator", 
                "Root node of XML is not 'tabledata', as it should be\n");
        return false;
    }

    for (uint32_t i=0; i<this->numOfTables; i++) {
        if (!populateTable(this, this->tables+i, rootElement, fitsOut)) {
            ret = false;
        }
    }

    closeXmlFile(doc);

    return ret;
}

/**
  Populate with data from another FITS table into this one
  */
static bool populateTableFromFits(
        FitsGenerator* this,
        Table* table,
        Fits *fitsIn, 
        Fits *fitsOut,
        const long nRows, 
        const bool fromHeader) {

    if (!table) return false;
    if (!fitsIn) return false;

    int8_t* int8col = (int8_t*)calloc(nRows, sizeof(int8_t));
    int16_t* int16col = (int16_t*)calloc(nRows, sizeof(int16_t));
    long* longcol = (long*)calloc(nRows, sizeof(long));
    float* floatcol = (float*)calloc(nRows, sizeof(float));
    double* doublecol = (double*)calloc(nRows, sizeof(double));
    char** strcol = (char**)calloc(nRows, sizeof(char**));
    for (uint32_t i=0; i<nRows;i++)
        strcol[i] = (char*)calloc(50,sizeof(char)); // TODO 20? size issue

    // first loop round all columns and get IPP col numbers for provided column names TODO only do once, first time in
    if(!fromHeader) {

        for (uint32_t i=0; i<table->numOfColumns; i++) {

            if (strlen(table->columns[i].ippName) < 1) continue;

            int dummy; // TODO getting wrong type for some reason
            if (!fitsIn->getColumnMeta(fitsIn, table->columns[i].ippName, &table->columns[i].ippColNum, &dummy, &table->columns[i].ippRepeat)) return false;
        }
    }

    int col;
    for (uint32_t i=0; i<table->numOfColumns; i++) {

        if (!table->columns[i].usingDefault) {

            col = i+1;

            switch (table->columns[i].ippType) {

                case TBYTE:
                    if (fromHeader) {
                        fitsIn->getHeaderKeyValue(fitsIn, TBYTE, table->columns[i].ippName, &int8col[0]); 
                    }
                    else fitsIn->readColumn(fitsIn, TBYTE, table->columns[i].ippColNum, 1, 1, nRows, int8col);
                    fitsOut->writeColumn(fitsOut, TBYTE, col, 1, 1, nRows, int8col);
                    break;
                case TSHORT:
                    if (fromHeader) {
                        fitsIn->getHeaderKeyValue(fitsIn, TSHORT, table->columns[i].ippName, &int16col[0]);
                    }
                    else fitsIn->readColumn(fitsIn, TSHORT, table->columns[i].ippColNum, 1, 1, nRows, int16col);
                    fitsOut->writeColumn(fitsOut, TSHORT, col, 1, 1, nRows, int16col);
                    break;
                case TLONG:
                    if (fromHeader) {
                        fitsIn->getHeaderKeyValue(fitsIn, TLONG, table->columns[i].ippName, &longcol[0]);
                    }
                    else fitsIn->readColumn(fitsIn, TLONG, table->columns[i].ippColNum, 1, 1, nRows, longcol);
                    fitsOut->writeColumn(fitsOut, TLONG, col, 1, 1, nRows, longcol);
                    break;
                case TLONGLONG:
                    if (fromHeader) {
                        fitsIn->getHeaderKeyValue(fitsIn, TLONGLONG, table->columns[i].ippName, &longcol[0]);
                    }
                    else fitsIn->readColumn(fitsIn, TLONGLONG, table->columns[i].ippColNum, 1, 1, nRows, longcol);
                    fitsOut->writeColumn(fitsOut, TLONGLONG, col, 1, 1, nRows, longcol);
                    break;
                case TFLOAT:
                    if (fromHeader) {
                        fitsIn->getHeaderKeyValue(fitsIn, TFLOAT, table->columns[i].ippName,&floatcol[0]); 
                    }
                    else fitsIn->readColumn(fitsIn, TFLOAT, table->columns[i].ippColNum, 1, 1, nRows, floatcol);
                    fitsOut->writeColumn(fitsOut, TFLOAT, col, 1, 1, nRows, floatcol);
                    break;
                case TDOUBLE:
                    if (fromHeader) {
                        fitsIn->getHeaderKeyValue(fitsIn, TDOUBLE, table->columns[i].ippName, &doublecol[0]);
                    }
                    else fitsIn->readColumn(fitsIn, TDOUBLE, table->columns[i].ippColNum, 1, 1, nRows, doublecol);
                    fitsOut->writeColumn(fitsOut, TDOUBLE, col, 1, 1, nRows, doublecol);
                    break;
                case TSTRING:
                    if (fromHeader) fitsIn->getHeaderKeyValue(fitsIn, TSTRING, table->columns[i].ippName, strcol[0]);
                    else fitsIn->readColumn(fitsIn, TSTRING, table->columns[i].ippColNum, 1, 1, nRows, strcol);
                    fitsOut->writeColumn(fitsOut, TSTRING, col, 1, 1, nRows, strcol);
                    break;

                default:
                    this->logger->print(this->logger, MSG_ERROR, "FitsGenerator", "Don't know IPP type (%d) for mapping from  '%d'  '%s' to '%s'\n", table->columns[i].ippType,
                            table->columns[i].ippColNum, table->columns[i].ippName, table->columns[i].pspsName);
                    break;
            }
        }
    }

    free(int8col);
    free(int16col);
    free(longcol);
    free(floatcol);
    free(doublecol);
    for (uint32_t i=0; i<nRows;i++) free(strcol[i]);
    free(strcol);

    return true;
}

/**
  Creates and populates a table
  */
static bool createAndPopulateTable(
        FitsGenerator* this, 
        Fits* fitsIn,
        Fits* fitsOut,
        const long nRows,
        const char* tableName,
        const int fromHeader) {

    Table* table = findTable(this, tableName);
    if (!table) return false;

    if(!createTable(table, fitsOut, nRows)) return false;
    return populateTableFromFits(this, table, fitsIn, fitsOut, nRows, fromHeader);
}

/**
  Destructor.
  */
static void destroy(FitsGenerator* this) {

    if (this == NULL) return;

    // TODO check logic here
    for (int i=0;i<this->numOfTables;i++)
        free(this->tables[i].columns);

    free(this->tables);
    psFree(this->configsPath);
    free(this);
}

/**
  Constructor.
  */
FitsGenerator* new_FitsGenerator(Logger* logger, const char* path) {

    FitsGenerator* this = (FitsGenerator*)calloc(1, sizeof(FitsGenerator));

    this->configsPath = NULL;
    psStringAppend(&this->configsPath, path);
    this->logger = logger;

    // method pointers
    this->createAndPopulateTable = createAndPopulateTable;
    this->populateFromFile = populateFromFile;
    this->destroy = destroy;

    // load tables descriptions from XML
    if (loadTableDescriptions(this)) {

        // load any mappings we may have from XML        
        loadMappings(this);
    }

    return this;
}

