/*
 * glue code for calling GDL from within DPUSER
 *
 * In order to use this, DPUSER has to be compiled with -DHAS_GDL
 *
 * GDL itself has to be compiled as a library. To do so, follow these steps:
 * 1. Configure GDL using cmake
 *  - Do not include any PYTHON options
 *  - on MacOS, if you want plotting capabilities from within GDL, include wxWidgets
 *  - on MacOS, if you included wxWidgets, add CMAKE_CXX_FLAGS -std=c++11
 * 2. Manually edit the generated config.h:
 *  - Comment out all lines mentioning READLINE, EDITLINE, CURSES, NCURSES
 *  - On Windows and MacOS, cmake fails to detect some PLPLOT options, therefore edit config.h:
 *    #define HAVE_PLPLOT_WIDTH
 *    #define PLPLOT_HAS_PLCALLBACK
 * 3. On Windows, edit 2 more files (gdljournal.cpp and gdl_fun.cpp):
 *  - add #include <time.h>
 * 4. On Windows, find and edit the file linklibs.rsp:
 *  - swap order of libplplot.a and libplplotcxx.a
 *  - after those, add libqsastime.a and libcsirocsa.a
 *  - move -lws2_32 to the end
 *  - replace libpcre.a by libgnurx.a
 *  - remove all occurences of -lgnurx
 *  - add libxdr.a
 * 5. Compile the gdl executable using make
 * 6. Create the gdl.a library:
 *  - rm src/CMakeFiles/gdl.dir/gdl.cpp.o
 *  - ar rv gdl.a src/CMakeFiles/gdl.dir/*.o src/antlr/CMakeFiles/antlr.dir/*.o
 */

#include <stdio.h>
#include "dpuserType.h"
#include "dpuser_utils.h"
#include <fits.h>
#include <string>
#include <vector>
#include <map>

#ifdef HAS_GDL

#include <dinterpreter.hpp>
//#include <plplot/plplot.h>

//class dpEnvUDT : public EnvUDT {
//public:
//    dpEnvUDT(ProgNodeP idN, DSubUD* pro_, CallContext lF = RFUNCTION);
//private:
//    DLong onError;
//};

//dpEnvUDT::dpEnvUDT(ProgNodeP idN, DSubUD* pro_, CallContext lF) : EnvUDT(idN, pro_, lF), onError(2) {
//}

//class DInterpreter;
//void InitObjects();
void LibInit();
DInterpreter *gdl_interpreter = NULL;

int tagcounter = 0;

BaseGDL *fromdpuserType(const dpuserType what) {
    BaseGDL *rv = NULL;
    switch (what.type) {
        case typeCon: rv = new DLongGDL(what.toInt()); break;
        case typeDbl: rv = new DDoubleGDL(what.toDouble()); break;
        case typeStr: rv = new DStringGDL(what.c_str()); break;
        case typeCom: rv = new DComplexDblGDL(DComplexDbl(what.toReal(), what.toImag())); break;
        case typeFits: { // lazy - we just cast everything as double
            int nDim = what.fvalue->Naxis(0);
            SizeT dimArr[MAXRANK];
            for( SizeT i=0; i <= nDim; ++i)
                dimArr[i] = what.fvalue->Naxis(i + 1);
            dimension dim(dimArr, nDim);
            SizeT nEl = what.fvalue->Nelements();

            switch (what.fvalue->getType()) {
            case I1: {
                DByteGDL *res = new DByteGDL(dim, BaseGDL::NOZERO);
//                for( SizeT i=0; i<nEl; ++i) (*res)[i] = what.fvalue->i1data[i];
                rv = res;
            }
                break;
            case I2: {
                DIntGDL *res = new DIntGDL(dim, BaseGDL::NOZERO);
//                for( SizeT i=0; i<nEl; ++i) (*res)[i] = what.fvalue->i2data[i];
                rv = res;
            }
                break;
            case I4: {
                DLongGDL *res = new DLongGDL(dim, BaseGDL::NOZERO);
//                for( SizeT i=0; i<nEl; ++i) (*res)[i] = what.fvalue->i4data[i];
                rv = res;
            }
                break;
            case I8: {
                DLong64GDL *res = new DLong64GDL(dim, BaseGDL::NOZERO);
//                for( SizeT i=0; i<nEl; ++i) (*res)[i] = what.fvalue->i4data[i];
                rv = res;
            }
                break;
            case R4: {
                DFloatGDL *res = new DFloatGDL(dim, BaseGDL::NOZERO);
//                for( SizeT i=0; i<nEl; ++i) (*res)[i] = what.fvalue->r4data[i];
                rv = res;
            }
                break;
            case R8: {
                DDoubleGDL *res = new DDoubleGDL(dim, BaseGDL::NOZERO);
//                for( SizeT i=0; i<nEl; ++i) (*res)[i] = what.fvalue->r8data[i];
                rv = res;
            }
                break;
            case C16: {
                DComplexDblGDL *res = new DComplexDblGDL(dim, BaseGDL::NOZERO);
//                for( SizeT i=0; i<nEl; ++i) (*res)[i] = DComplexDbl(what.fvalue->cdata[i].r, what.fvalue->cdata[i].i);
                rv = res;
            }
                break;
            default: break;
            }
            if (rv)
                memcpy(rv->DataAddr(), what.fvalue->dataptr, nEl * (abs(what.fvalue->getType()) / 8));

        }
	break;
        case typeStrarr: {
            int nDim = what.arrvalue->size();
            rv = new DStringGDL(dimension(nDim), BaseGDL::NOZERO);
            DStringGDL *s = dynamic_cast<DStringGDL *>(rv);
            for (SizeT i = 0; i < nDim; i++) (*s)[i] = what.arrvalue->at(i);
        }
        break;
    case typeDpArr: {
        SizeT nParam;
        nParam = what.dparrvalue->size();
/*
        dpStringList tagNames;
        char *key = (char *)malloc(81 * sizeof(char));
        for (int i = 0; i < what.dparrvalue->size(); i++) {
            strcpy(key, "");
            if (what.dparrvalue->at(i)->type == typeFits) {
                what.dparrvalue->at(i)->fvalue->GetStringKey("EXTNAME", key);
            } else if (what.dparrvalue->at(i)->type == typeDpArr) {
                if (what.dparrvalue->at(i)->dparrvalue->at(0)->type == typeFits) {
                    what.dparrvalue->at(i)->dparrvalue->at(0)->fvalue->GetStringKey("EXTNAME", key);
                }
            }
            if (strlen(key) == 0) {
                if (i == 0) strcpy(key, "PRIMARY");
                else sprintf(key, "EXT_%i", i);
            }
            tagNames.append(key);
        }
        free(key);
*/
        DStructDesc*          nStructDesc = new DStructDesc( "$truct");
        DStructGDL* instance = new DStructGDL( nStructDesc);
//        Guard<DStructGDL> instance_guard(instance);

        for (SizeT p=0; p < nParam; p++) {
            BaseGDL* par = fromdpuserType(*(what.dparrvalue->at(p)));
            if( par->Type() == GDL_STRUCT) {
            // add struct
            DStructGDL* parStruct = static_cast<DStructGDL*>( par);
            if (!parStruct->Scalar())
                throw GDLException("Expression must be a scalar in this context: "+p);

            DStructDesc* desc = parStruct->Desc();
            for( SizeT t=0; t< desc->NTags(); ++t) {
                dpString tagName = "EXT_" + dpString::number(p) + ".dp_" + dpString::number(++tagcounter);
//                dpString tagName = tagNames.at(p) + ".dp_" + dpString::number(++tagcounter);
                instance->NewTag( tagName, //desc->TagName( t),
                          parStruct->GetTag( t)->Dup());
              }
              }
            else
              {
            // add tag value pair
            dpString tagName = "EXT_" + dpString::number(p);
            tagcounter = 0;

//            for(++p; p<=tagEnd; ++p) {
                BaseGDL* value = fromdpuserType(*(what.dparrvalue->at(p)));

                // add
                instance->NewTag( tagName,
                          value);
//              }
              }
          }
rv = instance;

    }
        break;
        default: break;
    }
    if (rv == NULL) throw GDLException("Cannot convert dpuser type to GDL.");
    return rv;
}

dpuserType todpuserType(BaseGDL *what) {
    dpuserType rv;

    int n_dim = what->Rank();
    if (n_dim > 3) throw GDLException("Cannot convert " + what->TypeStr() + " array to dpuser. Too many axes.");
    int dimArr[3] = { 1, 1, 1 };
    for( int i=0; i<n_dim; ++i) dimArr[i]=what->Dim(i);
    int nel = what->N_Elements();
    if (nel <= 0) throw GDLException("Cannot convert " + what->TypeStr() + " array to dpuser. Number of elements less or equal to zero");
    int bitpix = 0;
    dpint64 bzero = 0;

    switch (what->Type()) {
        case GDL_BYTE: rv.type = typeFits; bitpix = 8; break;
        case GDL_UINT: bzero = 32768;
        case GDL_INT: rv.type = typeFits; bitpix = 16; break;
        case GDL_ULONG: bzero = 2147483648;
        case GDL_LONG: rv.type = typeFits; bitpix = 32; break;
        case GDL_ULONG64: bzero = 9223372036854775808;
        case GDL_LONG64: rv.type = typeFits; bitpix = 64; break;
        case GDL_FLOAT: rv.type = typeFits; bitpix = -32; break;
        case GDL_DOUBLE: rv.type = typeFits; bitpix = -64; break;
        case GDL_COMPLEX: rv.type = typeFits; bitpix = -128; break;
        case GDL_COMPLEXDBL: rv.type = typeFits; bitpix = -128; break;
        case GDL_STRING: {
                rv.type = typeStrarr;
                rv.arrvalue = CreateStringArray();
                DStringGDL *s = dynamic_cast<DStringGDL *>(what);
                if (s != NULL) {
                    for (int i = 0; i < nel; i++) rv.arrvalue->append((*s)[i]);
                }
            }
        break;
        case GDL_STRUCT: {
            DStructGDL* s = static_cast<DStructGDL*>(what);
//            std::cout << "gdl_struct" << std::endl;
            if (s != NULL) {
                rv.type = typeDpArr;
                rv.dparrvalue = CreateDpList();
                for (dpint64 n = 0; n < s->Desc()->NTags(); n++) {
                    dpuserType *element = new dpuserType();
                    *element = todpuserType(s->Get(n));
                    switch (element->type) {
                    case typeFits:
                        deleteFromListOfFits(element->fvalue);
                        break;
                    case typeStrarr:
                        deleteFromListOfdpStringArrays(element->arrvalue);
                        break;
                    default:
                        break;
                    }

                    rv.dparrvalue->push_back(element);
                }
            }
        }
        break;
        default: throw GDLException("Cannot convert " + what->TypeStr() + " array to dpuser.");
        break;
    }

    if (rv.type == typeFits) {
        rv.fvalue = CreateFits(dimArr[0], dimArr[1], dimArr[2], bitpix);
        if (bzero != 0) rv.fvalue->setBzero(bzero);
        if (rv.fvalue->dataptr == NULL)
            throw GDLException("Cannot allocate enough memory to convert " + what->TypeStr() + " array to dpuser.");
        if (what->Type() != GDL_COMPLEX) {
            memcpy(rv.fvalue->dataptr, what->DataAddr(), nel * (abs(bitpix) / 8));
        } else {
            DComplexGDL *c = dynamic_cast<DComplexGDL *>(what);
            if (c != NULL) {
                for (dpint64 n = 0; n < nel; n++) {
                    rv.fvalue->cdata[n].r = (*c)[n].real();
                    rv.fvalue->cdata[n].i = (*c)[n].imag();
                }
            }
        }
    }

    return rv;
}

dpuserType todpuserTypeScalar(BaseGDL *what) {
    dpuserType rv;
//    std::cout << "todpuserTypeScalar" << std::endl;

    DByteGDL *b = dynamic_cast<DByteGDL *>(what);
    if (b != NULL) {
        rv.type = typeCon;
        rv.lvalue = (*b)[0];
        return rv;
    }
    DIntGDL *i = dynamic_cast<DIntGDL *>(what);
    if (i != NULL) {
        rv.type = typeCon;
        rv.lvalue = (*i)[0];
        return rv;
    }
    DUIntGDL *ui = dynamic_cast<DUIntGDL *>(what);
    if (ui != NULL) {
        rv.type = typeCon;
        rv.lvalue = (*ui)[0];
        return rv;
    }
    DLongGDL *l = dynamic_cast<DLongGDL *>(what);
    if (l != NULL) {
        rv.type = typeCon;
        rv.lvalue = (*l)[0];
        return rv;
    }
    DULongGDL *ul = dynamic_cast<DULongGDL *>(what);
    if (ul != NULL) {
        rv.type = typeCon;
        rv.lvalue = (*ul)[0];
        return rv;
    }
    DLong64GDL *l64 = dynamic_cast<DLong64GDL *>(what);
    if (l64 != NULL) {
        rv.type = typeCon;
        rv.lvalue = (*l64)[0];
        return rv;
    }
    DULong64GDL *ul64 = dynamic_cast<DULong64GDL *>(what);
    if (ul64 != NULL) {
        rv.type = typeCon;
        rv.lvalue = (*ul64)[0];
        return rv;
    }
    DFloatGDL *f = dynamic_cast<DFloatGDL *>(what);
    if (f != NULL) {
        rv.type = typeDbl;
        rv.dvalue = (*f)[0];
        return rv;
    }
    DDoubleGDL *d = dynamic_cast<DDoubleGDL *>(what);
    if (d != NULL) {
        rv.type = typeDbl;
        rv.dvalue = (*d)[0];
        return rv;
    }
    DStringGDL *s = dynamic_cast<DStringGDL *>(what);
    if (s != NULL) {
        rv.type = typeStr;
        rv.svalue = CreateString((*s)[0].c_str());
        return rv;
    }
    DComplexGDL *c = dynamic_cast<DComplexGDL *>(what);
    if (c != NULL) {
        rv.type = typeCom;
        rv.cvalue = CreateComplex((*c)[0].real(), (*c)[0].imag());
        return rv;
    }
    DComplexDblGDL *dc = dynamic_cast<DComplexDblGDL *>(what);
    if (dc != NULL) {
        rv.type = typeCom;
        rv.cvalue = CreateComplex((*dc)[0].real(), (*dc)[0].imag());
        return rv;
    }

    return rv;
}

int call_gdl(std::string func_or_pro, std::vector<dpuserType> *args, std::vector<std::string> variables, std::map<std::string, std::pair<std::string, dpuserType> > *keyw, bool isFunction, dpuserType &rv) {
    int n;
//    dpuserType rv;

    std::vector<BaseGDL*> parRef;
    std::vector<BaseGDL*> kwRef;
    std::vector<std::string> keyw_variables;
    std::vector<std::string> keyw_keys;

    if (gdl_interpreter == NULL) {
        InitObjects();
        LibInit();
        gdl_interpreter = new DInterpreter();

// call on_error, 1 upon startup of GDL subsystem
        std::vector<dpuserType> arguments;
        std::vector<std::string> passed_variables;
        passed_variables.push_back("");
        std::map<std::string, std::pair<std::string, dpuserType> > keywords;
        dpuserType rvr, arg;
        arg = 1;
        arguments.push_back(arg);
        if (call_gdl(std::string("on_error"), &arguments, passed_variables, &keywords, false, rvr) != 1) dp_output("could not call on_error, 1");
    }
    DString pro = StrUpCase(func_or_pro);

    try {
        std::string gdlPath=GetEnvString("GDL_PATH");
        if( gdlPath == "") gdlPath=GetEnvString("IDL_PATH");
        if( gdlPath == "")
        {
            //        gdlPath = "+" GDLDATADIR "/lib";
        }
        SysVar::SetGDLPath( gdlPath);

//        std::cout << "GDL Path: " << gdlPath << std::endl;

        DSub*    sub;
        bool     libCall = false;
        int proIx;

        if (isFunction) {
            proIx = LibFunIx(pro);
            if (proIx != -1) {
                // PCALL_LIB
                sub = libFunList[proIx];
                libCall = true;
            } else {
                // FCALL - user defined procedures
                proIx = FunIx(pro);
                if (proIx == -1) {
                    /*bool found=*/ gdl_interpreter->SearchCompilePro(pro, false);
                    proIx = FunIx(pro);
                    if (proIx == -1) return -1;
                }
                sub = funList[proIx];
            }
        } else { // procedure
            proIx = LibProIx(pro);
            if (proIx != -1) {
                // PCALL_LIB
                sub = libProList[proIx];
                libCall = true;
            } else {
                // FCALL - user defined procedures
                proIx = ProIx(pro);
                if (proIx == -1) {
                    bool found= gdl_interpreter->SearchCompilePro(pro, true);
                    proIx = ProIx(pro);
                    if (proIx == -1) return -1;
                }
                sub = proList[proIx];
            }
        }
//        std::cout << pro << ": " << proIx << std::endl;
// TODO: Check that not too many arguments / keywords are supplied (CheckSub in pythongdl.cpp)


        // build the environment
        EnvBaseT* e;

        if( libCall)
            e = new EnvT( NULL, sub);
        else
            e = new EnvUDT( NULL, static_cast<DSubUD*>(sub));

        Guard< EnvBaseT> e_guard( e);

        bool notDirect = !isFunction;
        if (!notDirect) notDirect = !libCall;
        if (!notDirect) notDirect = !libFunList[proIx]->DirectCall();
//        if (notDirect) {
            parRef.reserve(args->size());
            for (int arg = 0; arg < args->size(); arg++) {
//                std::cout << "arg is variable: " << args->at(arg).variable << std::endl;
                BaseGDL *pP = fromdpuserType(args->at(arg));
                if (pP == NULL) throw GDLException( "Cannot pass argument.");
 //               if ((arg < variables.size()) && (variables.at(arg) != "")) {
                    parRef.push_back(pP);
                    e->SetNextPar(&(parRef.back()));
 //               } else {
 //                   parRef.push_back(NULL);
 //                   e->SetNextPar(pP);
 //               }
            }
            for (std::map<std::string, std::pair<std::string, dpuserType> >::iterator key=keyw->begin(); key != keyw->end(); ++key) {
                keyw_keys.push_back(key->first);
                DString keyString = StrUpCase(key->first);
                int kwIx = e->GetPro()->FindKey( keyString);
                if( kwIx == -1) {
                    DString errString = "Keyword " + key->first + " not allowed in call to: " + e->GetPro()->ObjectName();
                    throw GDLException(errString);
                }
                BaseGDL *pP = fromdpuserType(key->second.second);
                kwRef.push_back(pP);
                e->SetKeyword(keyString, &(kwRef.back()));
                if (key->second.first != "") {
                    keyw_variables.push_back(key->second.first);
                } else {
                    keyw_variables.push_back("");
                }
            }
//        }
        e->ResolveExtra();

        StackSizeGuard<EnvStackT> guard( GDLInterpreter::CallStack());

        if( !libCall)
        {
            GDLInterpreter::CallStack().push_back( static_cast<EnvUDT*>(e));
            e_guard.release();
        }

        BaseGDL* retValGDL = NULL;
        Guard<BaseGDL> retValGDL_guard;

        if (isFunction) {
            if( libCall) {
                if (libFunList[ proIx]->DirectCall()) {
//                    BaseGDL* directCallParameter = fromdpuserType(args->at(0)); //e->GetParDefined(1);
                    retValGDL = static_cast<DLibFunDirect*>(libFunList[ proIx])->FunDirect()(parRef.at(0), true /*isReference*/);
                } else {

            DLibFun *ddd = static_cast<DLibFun*>(e->GetPro());
            EnvT *envt = static_cast<EnvT*>(e);
              retValGDL = ddd->Fun()(envt);
                }
            } else
                retValGDL = gdl_interpreter->call_fun(static_cast<DSubUD*>
                                                      (static_cast<EnvUDT*>(e)
                                                       ->GetPro())->GetTree());
            retValGDL_guard.Reset( retValGDL);
//            std::cout << "called GDL function" << std::endl;
            if (retValGDL->Type() == GDL_STRUCT || !retValGDL->Scalar()) {
                rv = todpuserType(retValGDL);
            } else {
                rv = todpuserTypeScalar(retValGDL);
            }
        } else { //procedure
            if( libCall)
              static_cast<DLibPro*>(e->GetPro())->Pro()(static_cast<EnvT*>(e)); // throws
            else
              gdl_interpreter->call_pro(static_cast<DSubUD*>
                                    (e->GetPro())->GetTree()); //throws
            args->clear();
            for (int i = 0; i < parRef.size(); i++) {
                BaseGDL *par = parRef[i];
                if (par != NULL && variables.at(i) != "") {
//                    std::cout << "copying back argument " << i << " of " << parRef.size() << std::endl;
                    if (parRef.at(i)->Scalar()) {
//                        std::cout << "copying back scalar " << i << std::endl;
                        args->push_back(todpuserTypeScalar(parRef.at(i)));
                    } else {
//                        std::cout << "copying back vector " << i << std::endl;
                        args->push_back(todpuserType(parRef.at(i)));
                    }
                } else {
//                    std::cout << "copying back dummy" << std::endl;
                    dpuserType dummy;
                    args->push_back(dummy);
                }
            }
            rv = 0;
        }
//        std::cout << "kwRefsize: " << kwRef.size() << std::endl;
        for (int i = 0; i < kwRef.size(); i++) {
            BaseGDL *par = kwRef[i];
            if (par != NULL) {
//                std::cout << "copying back argument " << i << " of " << kwRef.size() << std::endl;
                if (kwRef.at(i)->Scalar()) {
//                    std::cout << "copying back scalar " << i << std::endl;
//                    std::cout << todpuserTypeScalar(kwRef.at(i)).toInt() << std::endl;
                    (*keyw)[keyw_keys.at(i)] = std::pair<std::string, dpuserType>(keyw_variables.at(i), todpuserTypeScalar(kwRef.at(i)));
                } else {
//                    std::cout << "copying back vector " << i << std::endl;
                    (*keyw)[keyw_keys.at(i)] = std::pair<std::string, dpuserType>(keyw_variables.at(i), todpuserType(kwRef.at(i)));
                }
            } else {
//                std::cout << "copying back dummy" << std::endl;
                dpuserType dummy;
                (*keyw)[keyw_keys.at(i)] = std::pair<std::string, dpuserType>("", dummy);
            }
        }
//        return rv;
    }

    catch ( GDLException ex)
    {
        // ERROR GDL exception
        std::cout << "Calling GDL " << ex.toString() << std::endl;
    }

    catch (...) {
        std::cout << "an error occured calling GDL" << std::endl;
    }

//    catch (MismatchedCharException e) {
//        std::cout << "an error occured" << std::endl;
//    }

    PurgeContainer(kwRef);
    PurgeContainer(parRef);

    return 1;
}

#else /* DP_GDL */

int call_gdl(std::string func_or_pro, std::vector<dpuserType> *args, std::vector<std::string> variables, std::map<std::string, std::pair<std::string, dpuserType> > *keyw, bool isFunction, dpuserType &rv) {
    return -1;
}

#endif /* DP_GDL */
