/*
 * This file is part of the loccorr project.
 * Copyright 2021 Edward V. Emelianov <edward.emelianoff@gmail.com>.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <pylonc/PylonC.h>

#include "basler.h"
#include "debug.h"
#include "imagefile.h"

static PYLON_DEVICE_HANDLE hDev;
static int isopened = FALSE;
static size_t payloadsize = 0; // size of imgBuf
static unsigned char *imgBuf = NULL;
static float expostime = 0.; // current exposition time
static PYLON_DEVICECALLBACK_HANDLE hCb;

typedef struct{
    int64_t min;
    int64_t max;
    int64_t incr;
    int64_t val;
} int64_values;

typedef struct{
    double min;
    double max;
    double val;
} float_values;

static char* describeError(GENAPIC_RESULT reserr){
    static char buf[1024];
    char* errMsg, *bptr = buf;
    size_t length, l = 1023;
    GenApiGetLastErrorMessage(NULL, &length);
    errMsg = MALLOC(char, length);
    GenApiGetLastErrorMessage(errMsg, &length);
    size_t ll = snprintf(bptr, l, "%s (%d); ", errMsg, (int)reserr);
    if(ll > 0){l -= ll; bptr += ll;}
    FREE(errMsg);
    GenApiGetLastErrorDetail(NULL, &length);
    errMsg = MALLOC(char, length);
    GenApiGetLastErrorDetail(errMsg, &length);
    snprintf(bptr, l, "%s", errMsg);
    FREE(errMsg);
    return buf;
}

#define PYLONFN(fn, ...) do{register GENAPIC_RESULT reserr; if(GENAPI_E_OK != (reserr=fn(__VA_ARGS__))){ \
    WARNX(#fn "(): %s", describeError(reserr)); return FALSE;}}while(0)

static void cam_disconnect(){
    FNAME();
    if(!isopened) return;
    FREE(imgBuf);
    PylonDeviceDeregisterRemovalCallback(hDev, hCb);
    PylonDeviceClose(hDev);
    PylonDestroyDevice(hDev);
    PylonTerminate();
    isopened = FALSE;
}

/**
 * @brief chkNode - get node & check it for read/write
 * @param phNode (io) - pointer to node
 * @param nodeType    - type of node
 * @param wr          - ==TRUE if need to check for writeable
 * @return TRUE if node found & checked
 */
static int chkNode(NODE_HANDLE *phNode, const char *featureName, EGenApiNodeType nodeType, int wr){
    if(!isopened || !phNode || !featureName) return FALSE;
    NODEMAP_HANDLE hNodeMap;
    EGenApiNodeType nt;
    _Bool bv;
    PYLONFN(PylonDeviceGetNodeMap, hDev, &hNodeMap);
    PYLONFN(GenApiNodeMapGetNode, hNodeMap, featureName, phNode);
    if(*phNode == GENAPIC_INVALID_HANDLE) return FALSE;
    PYLONFN(GenApiNodeGetType, *phNode, &nt);
    if(nodeType != nt) return FALSE;
    PYLONFN(GenApiNodeIsReadable, *phNode, &bv);
    if(!bv) return FALSE; // not readable
    if(wr){
        PYLONFN(GenApiNodeIsWritable, *phNode, &bv);
        if(!bv) return FALSE; // not writeable
    }
    return TRUE;
}

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-function"

// getters of different types of data
static int getBoolean(const char *featureName, _Bool *val){
    if(!isopened || !featureName) return FALSE;
    NODE_HANDLE hNode;
    if(!chkNode(&hNode, featureName, BooleanNode, FALSE)) return FALSE;
    if(!val) return TRUE;
    PYLONFN(GenApiBooleanGetValue, hNode, val);
    //DBG("Get boolean: %s = %s", featureName, val ? "true" : "false");
    return TRUE;
}
static int getInt(char *featureName, int64_values *val){
    if(!isopened || !featureName) return FALSE;
    NODE_HANDLE hNode;
    if(!chkNode(&hNode, featureName, IntegerNode, FALSE)) return FALSE;
    if(!val) return TRUE;
    PYLONFN(GenApiIntegerGetMin, hNode, &val->min);
    PYLONFN(GenApiIntegerGetMax, hNode, &val->max);
    PYLONFN(GenApiIntegerGetInc, hNode, &val->incr);
    PYLONFN(GenApiIntegerGetValue, hNode, &val->val);
    //DBG("Get integer %s = %ld: min = %ld, max = %ld, incr = %ld", featureName, val->val, val->min, val->max, val->incr);
    return TRUE;
}
static int getFloat(char *featureName, float_values *val){
    if(!isopened || !featureName) return FALSE;
    NODE_HANDLE hNode;
    if(!chkNode(&hNode, featureName, FloatNode, FALSE)) return FALSE;
    if(!val) return TRUE;
    PYLONFN(GenApiFloatGetMin, hNode, &val->min);
    PYLONFN(GenApiFloatGetMax, hNode, &val->max);
    PYLONFN(GenApiFloatGetValue, hNode, &val->val);
    //DBG("Get float %s = %g: min = %g, max = %g", featureName, val->val, val->min, val->max);
    return TRUE;
}

// setters of different types of data
static int setBoolean(char *featureName, _Bool val){
    if(!isopened || !featureName) return FALSE;
    NODE_HANDLE hNode;
    if(!chkNode(&hNode, featureName, BooleanNode, TRUE)) return FALSE;
    PYLONFN(GenApiBooleanSetValue, hNode, val);
    return TRUE;
}
static int setInt(char *featureName, int64_t val){
    if(!isopened || !featureName) return FALSE;
    NODE_HANDLE hNode;
    if(!chkNode(&hNode, featureName, IntegerNode, TRUE)) return FALSE;
    PYLONFN(GenApiIntegerSetValue, hNode, val);
    return TRUE;
}
static int setFloat(char *featureName, float val){
    if(!isopened || !featureName) return FALSE;
    NODE_HANDLE hNode;
    if(!chkNode(&hNode, featureName, FloatNode, TRUE)) return FALSE;
    PYLONFN(GenApiFloatSetValue, hNode, val);
    return TRUE;
}
#pragma GCC diagnostic pop

static void disableauto(){
    if(!isopened) return;
    const char *features[] = {"EnumEntry_TriggerSelector_AcquisitionStart",
                              "EnumEntry_TriggerSelector_FrameBurstStart",
                              "EnumEntry_TriggerSelector_FrameStart"};
    const char *triggers[] = {"AcquisitionStart", "FrameBurstStart", "FrameStart"};
    for(int i = 0; i < 3; ++i){
        if(PylonDeviceFeatureIsAvailable(hDev, features[i])){
            PylonDeviceFeatureFromString(hDev, "TriggerSelector", triggers[i]);
            PylonDeviceFeatureFromString(hDev, "TriggerMode", "Off");
        }
    }
    PylonDeviceFeatureFromString(hDev, "GainAuto", "Off");
    PylonDeviceFeatureFromString(hDev, "ExposureAuto", "Off");
    PylonDeviceFeatureFromString(hDev, "ExposureMode", "Timed");
    PylonDeviceFeatureFromString(hDev, "SequencerMode", "Off");
}

static void GENAPIC_CC removalCallbackFunction(_U_ PYLON_DEVICE_HANDLE hDevice){
    cam_disconnect();
}

static int cam_connect(){
    FNAME();
    size_t numDevices;
    cam_disconnect();
    PylonInitialize();
    PYLONFN(PylonEnumerateDevices, &numDevices);
    if(!numDevices){
        WARNX("No cameras found");
        return FALSE;
    }
    PYLONFN(PylonCreateDeviceByIndex, 0, &hDev);
    isopened = TRUE;
    PYLONFN(PylonDeviceOpen, hDev, PYLONC_ACCESS_MODE_CONTROL | PYLONC_ACCESS_MODE_STREAM | PYLONC_ACCESS_MODE_EXCLUSIVE);
    disableauto();
    PYLONFN(PylonDeviceFeatureFromString, hDev, "PixelFormat", "Mono8");
    PYLONFN(PylonDeviceFeatureFromString, hDev, "CameraOperationMode", "LongExposure");
    PYLONFN(PylonDeviceFeatureFromString, hDev, "UserSetSelector", "HighGain"); // set high gain selector
    PYLONFN(PylonDeviceFeatureFromString, hDev, "AcquisitionMode", "SingleFrame");
    PYLONFN(PylonDeviceExecuteCommandFeature, hDev, "UserSetLoad"); // load high gain mode
    PYLON_STREAMGRABBER_HANDLE hGrabber;
    PYLONFN(PylonDeviceGetStreamGrabber, hDev, 0, &hGrabber);
    PYLONFN(PylonStreamGrabberOpen, hGrabber);
//    PYLON_WAITOBJECT_HANDLE hWaitStream;
//    PYLONFN(PylonStreamGrabberGetWaitObject, hStreamGrabber, &hWaitStream);
    PYLONFN(PylonStreamGrabberGetPayloadSize, hDev, hGrabber, &payloadsize);
    PylonStreamGrabberClose(hGrabber);
    PylonDeviceRegisterRemovalCallback(hDev, removalCallbackFunction, &hCb);
    imgBuf = MALLOC(unsigned char, payloadsize);
    //PYLONFN(PylonDeviceExecuteCommandFeature, hDev, "AcquisitionStart");
    return TRUE;
}

static Image *capture(){
    //FNAME();
    static int toohot = FALSE;
    if(!isopened || !imgBuf) return NULL;
    float_values f;
    static double t0 = 0.;
    if(!getFloat("DeviceTemperature", &f)) WARNX("Can't get temperature");
    else{
        double t = sl_dtime();
        if(t - t0 >= 30.){ // log T each 30 seconds
            LOGMSG("Basler temperature: %.1f", f.val);
            t0 = t;
        }
        //DBG("Temperature: %.1f", f.val);
        if(f.val > 80.){
            WARNX("Device too hot");
            if(!toohot){
                LOGWARN("Device too hot");
                toohot = TRUE;
            }
        }else if(toohot && f.val < 75.){
            LOGDBG("Device temperature is normal");
            toohot = FALSE;
        }
    }
    PylonGrabResult_t grabResult;
    _Bool bufferReady;
    GENAPIC_RESULT res = PylonDeviceGrabSingleFrame(hDev, 0, imgBuf, payloadsize,
                                                    &grabResult, &bufferReady, 500 + (uint32_t)expostime);
    if(res != GENAPI_E_OK || !bufferReady){
        WARNX("res != GENAPI_E_OK || !bufferReady");
        return NULL;
    }
    if(grabResult.Status != Grabbed){
        WARNX("grabResult.Status != Grabbed");
        return NULL;
    }
    Image *oIma = u8toImage(imgBuf, grabResult.SizeX, grabResult.SizeY, grabResult.SizeX + grabResult.PaddingX);
    return oIma;
}

// Basler have no "brightness" parameter
static int setbrightness(_U_ float b){
    FNAME();
    return TRUE;
}

static int setexp(float e){
    FNAME();
    if(!isopened) return FALSE;
    e *= 1000.;
    if(!setFloat("ExposureTime", e)){
       LOGWARN("Can't set expose time %g", e);
       WARNX("Can't set expose time %g", e);
       return FALSE;
    }
    float_values f;
    if(!getFloat("ExposureTime", &f)) return FALSE;
    expostime = (float)f.val / 1000.;
    return TRUE;
}

static int setgain(_U_ float e){
    FNAME();
    if(!isopened) return FALSE;
    if(!setFloat("Gain", e)){
       LOGWARN("Can't set gain %g", e);
       WARNX("Can't set gain %g", e);
       return FALSE;
    }
    return TRUE;
}

static int changeformat(frameformat *fmt){
    FNAME();
    if(!isopened) return FALSE;
    setInt("Width", fmt->w);
    setInt("Height", fmt->h);
    setInt("OffsetX", fmt->xoff);
    setInt("OffsetY", fmt->yoff);
    int64_values i;
    if(getInt("Width", &i)) fmt->w = i.val;
    if(getInt("Height", &i)) fmt->h = i.val;
    if(getInt("OffsetX", &i)) fmt->xoff = i.val;
    if(getInt("OffsetY", &i)) fmt->yoff = i.val;
    return TRUE;
}

static int geometrylimits(frameformat *max, frameformat *step){
    FNAME();
    if(!isopened || !max || !step) return FALSE;
    int64_values i;
    if(!getInt("Width", &i)) return FALSE;
    max->w = i.max; step->w = i.incr;
    if(!getInt("Height", &i)) return FALSE;
    max->h = i.max; step->h = i.incr;
    if(!getInt("OffsetX", &i)) return FALSE;
    max->xoff = i.max; step->xoff = i.incr;
    if(!getInt("OffsetY", &i)) return FALSE;
    max->yoff = i.max; step->yoff = i.incr;
    return TRUE;
}

static float gainmax(){
    FNAME();
    float_values v;
    if(!getFloat("Gain", &v)) return 0.;
    return (float)v.max;
}

// exported object
camera Basler = {
    .disconnect = cam_disconnect,
    .connect = cam_connect,
    .capture = capture,
    .setbrightness = setbrightness,
    .setexp = setexp,
    .setgain = setgain,
    .setgeometry = changeformat,
    .getgeomlimits = geometrylimits,
    .getmaxgain = gainmax,
};

