#ifndef _VIDEORECORDER

#define _VIDEORECORDER

#include <queue>
#include <iostream>
#include <fstream>
#include <experimental/filesystem>

#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include "opencv2/imgproc.hpp"

#include "Spinnaker.h"

#include "ofThread.h"

#include "inih/cpp/INIReader.h"
#include "SimpleTimer.h"

#include "date/include/date/date.h"

#include "include/loguru/loguru.hpp"

#include "ImageUnpack.h"

#define FILE_TYPE 0

using namespace std;
using namespace cv;
using namespace Spinnaker;
namespace fs = std::experimental::filesystem;

class RawFrameRecorder : public ofThread {
  
public:

    RawFrameRecorderRecorder() {
        frameCounter = 0;
        framesPerFile = 0;
        videoPeriod = 0;
        videoOpen = false;
        enabled = true;
        paused = false;
        configured = false;
        convertFrame = false;
        
    }
    
    ~VideoRecorder() {
        delete cfg;
    }
    
    void setConfig(INIReader * cfg, string camName) {
        
        this->cfg = cfg;
        this->camName = camName;
        
        if (cfg != NULL) {
            framesPerFile = cfg->GetInteger("video","frames_in_file",10);
            videoPeriod = cfg->GetInteger("video", "video_period", 3600);
            enabled = cfg->GetBoolean("video", "enabled", false);
            frameModulus = cfg->GetInteger("video", "frame_modulus", 1);
            
            LOG_S(INFO) << "Video Config:" << endl;
            LOG_S(INFO) << "Frames per File: " << framesPerFile << endl;
            LOG_S(INFO) << "Video Period: " << videoPeriod << endl;
            LOG_S(INFO) << "Enabled: " << enabled << endl; 
            
            configured = true;
        }
        
        // start the recording timer
        recordingTimer.start();
    }
    
    void updateDataDir(string dataDir) {
        fs::path dataPath = dataDir;
        dataPath /= camName + "_video";
        fs::create_directories(dataPath);
        this->dataDir = dataPath;
        
    }
    
    void threadedFunction() {
        
        loguru::set_thread_name("Video Recorder Thread");
        
        while (isThreadRunning() || frameBuffer.size() > 0) {
            
            if (frameBuffer.size() > 0 && configured) {
                
                lock();
                ImagePtr img = frameBuffer.front();
                frameBuffer.pop();
                unlock();
                
                // convert frame if needed
                unsigned int bytesPerImage;
                unsigned int XPadding = img->GetXPadding();
                unsigned int YPadding = img->GetYPadding();
                unsigned int rowsize = img->GetWidth();
                unsigned int colsize = img->GetHeight();
                unsigned int nPixels = (rowsize + XPadding) * (colsize + YPadding);
                
                if (convertFrame) {
                    bytesPerImage = 2*nPixels;
                }
                else {
                    bytesPerImage = nPixels;
                }
                
                
                if (!outputFile.bad()) {
                    uint64_t marker = 0;
                    uint64_t frameID = img->GetFrameID();
                    uint64_t frameTimestamp = img->GetTimeStamp();
                    auto duration = std::chrono::system_clock::now().time_since_epoch();
                    auto micros = std::chrono::duration_cast<std::chrono::microseconds>(duration).count();
                    uint64_t systemTimestamp = micros;
                    outputFile.write(reinterpret_cast<char*>(&marker), sizeof(uint64_t));
                    outputFile.write(reinterpret_cast<char*>(&frameID), sizeof(uint64_t));
                    outputFile.write(reinterpret_cast<char*>(&frameTimestamp), sizeof(uint64_t));
                    outputFile.write(reinterpret_cast<char*>(&systemTimestamp), sizeof(uint64_t));
                    outputFile.write(reinterpret_cast<char*>(img->GetData()), bytesPerImage);
                }
                
                framesWritten += 1;
                
                // Close file if we have written the desired number of frames
                if (framesWritten >= framesPerFile) {
                    outputFile.close();
                    recording = false;
                }
                
                frameCounter++;
            }
            else {
                sleep(10);
            }
        }
        
        if (!outputFile.bad())
            outputFile.close();
        
        LOG_S(INFO) << "Ended Video Recording Thread for " << camName;
    }
    
    unsigned int getFrameCount() {
        return frameCounter;
    }
    
    unsigned int getQueueSize() {
        return frameBuffer.size();
    }
    
    void setFramesPerFile(int n) {
        framesPerFile = n;
    }
    
    void pause() {
        paused = true;
    }
    
    void unpause() {
        paused = false;
    }
    
    bool addImage(ImagePtr img) {
        
        if (!enabled)
            return false;
            
        if (paused) 
            return false;
        
        
        if (isThreadRunning() && configured && (frameCounter % frameModulus == 0)) {
            if (!recording && recordingTimer.elapsedSeconds() > videoPeriod) {
                // open new file and restart recdording timer
                string filename = date::format("%Y-%m-%d-%H-%M-%S.bin", std::chrono::system_clock::now());
                fs::path filepath = dataDir / filename;
                
                outputFile.open(filepath.c_str(), ios::binary | ios::out);
                if (!outputFile.bad()) {
                    //write header
                    int fileType = FILE_TYPE;
                    size_t width = img->GetWidth();
                    size_t height = img->GetHeight();
                    size_t bpp = img->GetBitsPerPixel();
                    int format = img->GetPixelFormat();
                    int fpf = framesPerFile;
                    if (bpp > 8) {
                        // convertion is needed from packed data
                        convertFrame = true;
                    }
                    outputFile.write(reinterpret_cast<char*>(&fileType), sizeof(int));
                    outputFile.write(reinterpret_cast<char*>(&fpf), sizeof(fpf));
                    outputFile.write(reinterpret_cast<char*>(&bpp), sizeof(size_t));
                    outputFile.write(reinterpret_cast<char*>(&format), sizeof(int));
                    outputFile.write(reinterpret_cast<char*>(&width), sizeof(width));
                    outputFile.write(reinterpret_cast<char*>(&height), sizeof(size_t));
                }
                else {
                    LOG_S(ERROR) << "Could not open file: " << filepath.c_str();
                }
                
                recording = true;
                framesWritten = 0;
                recordingTimer.reset();
                recordingTimer.start();
            }
            if (recording) {
                lock();
                frameBuffer.push(img);
                unlock();
            }
                
            return true;

        }
        
        // Update the frame counter regardless of if frame was set for procdessing
        frameCounter += 1;
        
        return false;
        
    }
    
    void setDepth(float depth) {
        this->depth = depth;
    }
    
    void setTemperature(float temperature) {
        this->temperature = temperature;
    }
    
private:

    std::queue<ImagePtr> frameBuffer;
    std::vector<Mat> imagesToWrite;
    unsigned int frameCounter;
    
    int framesPerFile;
    int frameModulus;
    int videoPeriod;
    int framesWritten;
    bool videoOpen;
    bool enabled;
    bool paused;
    bool recording;
    bool configured;
    bool convertFrame;
    
    // sensor values
    float depth;
    float temperature;
    
    INIReader * cfg;
    
    fs::path dataDir;
    
    SimpleTimer recordingTimer;
    
    string camName;

    fstream outputFile;
    
};

#endif