/*
    indi_LMS_receiver - a software defined radio driver for INDI
    Copyright (C) 2017  Ilia Platone

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library 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
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/

#include "indi_limesdr_receiver.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <indilogger.h>
#include <memory>
#include <deque>

#define min(a, b)               \
    ({                          \
        __typeof__(a) _a = (a); \
        __typeof__(b) _b = (b); \
        _a < _b ? _a : _b;      \
    })
#define MAX_TRIES      20
#define MAX_DEVICES    4
#define SUBFRAME_SIZE  (16384)
#define MIN_FRAME_SIZE (512)
#define MAX_FRAME_SIZE (SUBFRAME_SIZE * 16)
#define SPECTRUM_SIZE  (256)

static class Loader
{
public:
    std::deque<std::unique_ptr<LIMESDR>> receivers;
    lms_info_str_t *lime_dev_list;
public:
    Loader()
    {
        int iNumofConnectedReceivers = LMS_GetDeviceList(lime_dev_list);

        if (iNumofConnectedReceivers <= 0)
        {
            //Try sending IDMessage as well?
            IDLog("No LIMESDR receivers detected. Power on?");
            IDMessage(nullptr, "No LIMESDR receivers detected. Power on?");
            return;
        }

        for (int i = 0; i < iNumofConnectedReceivers; i++)
        {
            receivers.push_back(std::unique_ptr<LIMESDR>(new LIMESDR(i)));
        }
    }
} loader;

LIMESDR::LIMESDR(uint32_t index)
{
    InIntegration = false;
    receiverIndex = index;

    char name[MAXINDIDEVICE];
    snprintf(name, MAXINDIDEVICE, "%s %d", getDefaultName(), index);
    setDeviceName(name);
}

/**************************************************************************************
** Client is asking us to establish connection to the device
***************************************************************************************/
bool LIMESDR::Connect()
{
    int r = LMS_Open(&lime_dev, loader.lime_dev_list[receiverIndex], NULL);
    if (r < 0)
    {
        LOGF_ERROR("Failed to open limesdr device index %d.", receiverIndex);
        return false;
    }
    LMS_Init(lime_dev);
    LMS_EnableChannel(lime_dev, LMS_CH_RX, 0, true);
    LOG_INFO("LIME-SDR Receiver connected successfully!");
    // Let's set a timer that checks teleReceivers status every POLLMS milliseconds.
    // JM 2017-07-31 SetTimer already called in updateProperties(). Just call it once
    //SetTimer(getCurrentPollingPeriod());

    return true;
}

/**************************************************************************************
** Client is asking us to terminate connection to the device
***************************************************************************************/
bool LIMESDR::Disconnect()
{
    InIntegration = false;
    LMS_Close(lime_dev);
    setBufferSize(1);
    LOG_INFO("LIME-SDR Receiver disconnected successfully!");
    return true;
}

/**************************************************************************************
** INDI is asking us for our default device name
***************************************************************************************/
const char *LIMESDR::getDefaultName()
{
    return "LIME-SDR Receiver";
}

/**************************************************************************************
** INDI is asking us to init our properties.
***************************************************************************************/
bool LIMESDR::initProperties()
{
    // We set the Receiver capabilities
    uint32_t cap = SENSOR_CAN_ABORT | SENSOR_HAS_STREAMING | SENSOR_HAS_DSP;
    SetReceiverCapability(cap);

    // Must init parent properties first!
    INDI::Receiver::initProperties();

    setMinMaxStep("SENSOR_INTEGRATION", "SENSOR_INTEGRATION_VALUE", 0.001, 86164.092, 0.001, false);
    setMinMaxStep("RECEIVER_SETTINGS", "RECEIVER_FREQUENCY", 400.0e+6, 3.8e+9, 1, false);
    setMinMaxStep("RECEIVER_SETTINGS", "RECEIVER_SAMPLERATE", 2.0e+6, 28.0e+6, 1, false);
    setMinMaxStep("RECEIVER_SETTINGS", "RECEIVER_GAIN", 0.0, 1.0, 0.01, false);
    setMinMaxStep("RECEIVER_SETTINGS", "RECEIVER_BANDWIDTH", 400.0e+6, 3.8e+9, 1, false);
    setMinMaxStep("RECEIVER_SETTINGS", "RECEIVER_BITSPERSAMPLE", -32, -32, 0, false);
    setIntegrationFileExtension("fits");
    /*
    // PrimaryReceiver Device Continuum Blob
    IUFillBLOB(&TFitsB[0], "TRMT", "Transmit1", "");
    IUFillBLOB(&TFitsB[1], "TRMT", "Transmit2", "");
    IUFillBLOB(&TFitsB[2], "TRMT", "Transmit3", "");
    IUFillBLOB(&TFitsB[3], "TRMT", "Transmit4", "");
    IUFillBLOB(&TFitsB[4], "TRMT", "Transmit5", "");
    IUFillBLOBVector(&TFitsBP, TFitsB, 5, getDeviceName(), "LIME_TRMT", "Transmit Data", INTEGRATION_INFO_TAB, IP_WO, 60, IPS_IDLE);
*/
    // Add Debug, Simulator, and Configuration controls
    addAuxControls();

    setDefaultPollingPeriod(500);

    return true;
}

/********************************************************************************************
** INDI is asking us to update the properties because there is a change in CONNECTION status
** This fucntion is called whenever the device is connected or disconnected.
*********************************************************************************************/
bool LIMESDR::updateProperties()
{
    // Call parent update properties first
    INDI::Receiver::updateProperties();

    if (isConnected())
    {
        // Inital values
        setupParams(1000000, 1420000000, 10000, 10);
        //defineProperty(&TFitsBP);

        // Start the timer
        SetTimer(getCurrentPollingPeriod());
    }
    else
    {
        //deleteProperty(TFitsBP.name);
    }

    return true;
}

/**************************************************************************************
** Client is asking us to start an exposure
***************************************************************************************/
bool LIMESDR::StartIntegration(double duration)
{
    IntegrationRequest = duration;

    // Since we have only have one Receiver with one chip, we set the exposure duration of the primary Receiver
    setIntegrationTime(duration);
    b_read  = 0;
    to_read = getSampleRate() * getIntegrationTime();

    setBufferSize(to_read * sizeof(float));

    if (to_read > 0)
    {
        lime_stream.channel             = 0;
        lime_stream.isTx                = false;
        lime_stream.fifoSize            = to_read;
        lime_stream.dataFmt             = lms_stream_t::LMS_FMT_F32;
        lime_stream.throughputVsLatency = 0.5;
        LMS_SetupStream(lime_dev, &lime_stream);
        LMS_StartStream(&lime_stream);
        gettimeofday(&CapStart, nullptr);
        InIntegration = true;
        LOG_INFO("Integration started...");
        return true;
    }

    // We're done
    return false;
}

/**************************************************************************************
** Client is updating capture settings
***************************************************************************************/
void LIMESDR::setupParams(float sr, float freq, float bw, float gain)
{
    setBPS(-32);
    int r = 0;
    r |= LMS_SetAntenna(lime_dev, LMS_CH_RX, 0, 0);
    r |= LMS_SetNormalizedGain(lime_dev, LMS_CH_RX, 0, gain);
    r |= LMS_SetLOFrequency(lime_dev, LMS_CH_RX, 0, freq);
    r |= LMS_SetSampleRate(lime_dev, sr, 0);
    r |= LMS_Calibrate(lime_dev, LMS_CH_RX, 0, bw, 0);

    if (r != 0)
    {
        LOG_INFO("Error(s) setting parameters.");
    }
}

bool LIMESDR::ISNewNumber(const char *dev, const char *name, double values[], char *names[], int n)
{
    bool r = false;
    if (dev && !strcmp(dev, getDeviceName()) && !strcmp(name, ReceiverSettingsNP.name)) {
        for(int i = 0; i < n; i++) {
            if (!strcmp(names[i], "RECEIVER_GAIN")) {
                setupParams(getSampleRate(), getFrequency(), getBandwidth(), values[i]);
            } else if (!strcmp(names[i], "RECEIVER_BANDWIDTH")) {
                setupParams(getSampleRate(), getFrequency(), values[i], getGain());
            } else if (!strcmp(names[i], "RECEIVER_FREQUENCY")) {
                setupParams(getSampleRate(), values[i], getBandwidth(), getGain());
            } else if (!strcmp(names[i], "RECEIVER_SAMPLERATE")) {
                setupParams(values[i], getFrequency(), getBandwidth(), getGain());
            }
        }
        IDSetNumber(&ReceiverSettingsNP, nullptr);
    }
    return processNumber(dev, name, values, names, n) & !r;
}

/**************************************************************************************
** Client is asking us to abort a capture
***************************************************************************************/
bool LIMESDR::AbortIntegration()
{
    if (InIntegration)
    {
        lms_stream_status_t status;
        LMS_GetStreamStatus(&lime_stream, &status);
        if (status.fifoFilledCount <= 0)
        {
            InIntegration = false;
            LMS_StopStream(&lime_stream);
            LMS_DestroyStream(lime_dev, &lime_stream);
        }
    }
    return true;
}

/**************************************************************************************
** How much longer until exposure is done?
***************************************************************************************/
float LIMESDR::CalcTimeLeft()
{
    double timesince;
    double timeleft;
    struct timeval now;
    gettimeofday(&now, nullptr);

    timesince = (double)(now.tv_sec * 1000.0 + now.tv_usec / 1000) -
                (double)(CapStart.tv_sec * 1000.0 + CapStart.tv_usec / 1000);
    timesince = timesince / 1000;

    timeleft = IntegrationRequest - timesince;
    return timeleft;
}

/**************************************************************************************
** Main device loop. We check for capture progress here
***************************************************************************************/
void LIMESDR::TimerHit()
{
    long timeleft;

    if (isConnected() == false)
        return; //  No need to reset timer if we are not connected anymore

    if (InIntegration)
    {
        timeleft = CalcTimeLeft();
        if (timeleft < 0.1)
        {
            /* We're done capturing */
            LOG_INFO("Integration done, expecting data...");
            lms_stream_status_t status;
            LMS_GetStreamStatus(&lime_stream, &status);
            if (status.active)
            {
                if (status.fifoFilledCount >= status.fifoSize)
                {
                    n_read = status.fifoFilledCount;
                    grabData();
                }
            }
            timeleft = 0.0;
        }

        // This is an over simplified timing method, check ReceiverSimulator and limesdrReceiver for better timing checks
        setIntegrationLeft(timeleft);
    }

    SetTimer(getCurrentPollingPeriod());
    return;
}

/**************************************************************************************
** Create the spectrum
***************************************************************************************/
void LIMESDR::grabData()
{
    if (InIntegration)
    {
        continuum = getBuffer();
        LOG_INFO("Downloading...");
        LMS_RecvStream(&lime_stream, continuum, n_read, NULL, 1000);
        LMS_StopStream(&lime_stream);
        LMS_DestroyStream(lime_dev, &lime_stream);
        InIntegration = false;

        LOG_INFO("Download complete.");
        IntegrationComplete();
    }
}
