#ifndef _FRAMERECORDER
#define _FRAMERECORDER

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

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

// Submodules
#include "../libs/inih/INIReader.h"
#include "../libs/date/include/date/date.h"

// Allow streams in loguru for cout << style logging
#define LOGURU_WITH_STREAMS 1
#include "../libs/loguru/loguru.hpp"

// Project Files
#include "camunits.h"
#include "ofThread.h"
#include "SimpleTimer.h"

// namespaces 
using namespace std;
using namespace cv;
namespace fs = std::experimental::filesystem;

// File type defines
#define FILE_TYPE 24                ///< File header type id is the number of bytes of data in header

class FrameRecorder : public ofThread {

protected:

    uint32_t frameCounter;          ///< Frame counter for all frames seen
    
    int32_t framesPerFile;          ///< How many frames to put in each file
    int32_t frameModulus;           ///< Process every frameModulus frame seen   
    int32_t framesWritten;          ///< Total number of frames written
    int32_t filesWritten;           ///< Total number of files written
    int32_t frameWidth;             ///< Width of the frame in pixels
    int32_t frameHeight;            ///< Height of the frame in pixels
    int32_t pixelFormat;            ///< pixel type from camunits
    int32_t threadSleep;            ///< Milliseconds to sleep between iterations of the thread

    bool fileOpen;                  ///< True when a file is open for writing
    bool enabled;                   ///< True when the recorder is enabled
    bool paused;                    ///< True when the recorder is enabhled but paused (not recording)
    bool recording;                 ///< True when the recorder is recording a frame to a file
    bool configured;                ///< True when the recorder is configured
    bool sumFrames;                 ///< Sum frame together between frameModulus frames

    string camName;                 ///< The name of the camera frames are coming from
    string subDir;                  ///< The sub directory when new files are created
    string baseDir;                 ///< The base directory that contains the subdirectory

    std::queue<Mat> frameBuffer;    ///< The queue of frame waiting to be recorded
    
    fs::path dataDir;               ///< The full path to the date directory
    
    SimpleTimer recordingTimer;     ///< Recording timer for counting how long the recording has been running

    fstream outputFile;             ///< The output file to write pixels to
  
public:

    /**
     * @brief Construct a new FrameRecorder object
     * 
     */
    FrameRecorder() {
        frameCounter = 0;
        framesPerFile = 0;
        frameModulus = 0;
		filesWritten = 0;
        threadSleep = 10;
        fileOpen = false;
        enabled = true;
        paused = false;
        configured = false;
        subDir = "raw_frames";
        baseDir = "./";

    }
    
    /**
     * @brief Destroy the FrameRecorder object
     * 
     */
	~FrameRecorder() {
    }
    
    /**
     * @brief Load config from ini file
     * 
     * @param cfg The INIReader reference with the recorder config
     */
    virtual void setConfig(INIReader & cfg) {
            
        // Get config values
        baseDir = cfg.Get("application", "base_path", "./");
        subDir = cfg.Get("video", "subdir_name", "video");
        framesPerFile = cfg.GetInteger("video","frames_in_file",10);
        enabled = cfg.GetBoolean("video", "enabled", true);
        frameModulus = cfg.GetInteger("video", "frame_modulus", 1);
        pixelFormat = cfg.GetInteger("video", "pixel_format", PIXEL_FORMAT_RGB);
        threadSleep = cfg.GetInteger("video", "thread_sleep", 20);
        sumFrames = cfg.GetBoolean("video", "sum_frames", false);

        // Log recorder config
        LOG_S(INFO) << "Config:";
        LOG_S(INFO) << "Data Path: " << baseDir;
        LOG_S(INFO) << "Frames per File: " << framesPerFile;
        LOG_S(INFO) << "Enabled: " << enabled; 
        
        // Create output dirs
        updateDataDir(baseDir);

        configured = true;
        
        // start the recording timer
        recordingTimer.start();
    }

    /**
     * @brief update the path to recorded data and create directories if needed
     * 
     * @param dataDir 
     */
    void updateDataDir(string dataDir) {
        fs::path dataPath = dataDir;
        dataPath /= subDir;
        fs::create_directories(dataPath);
        this->dataDir = dataPath;
        
    }

    /**
     * @brief create a new file name from timestamp and file extension 
     */
    virtual string getFilepath() {

        string filename = date::format("%Y-%m-%d-%H-%M-%S.raw", std::chrono::system_clock::now());
		fs::path filepath = dataDir / filename;
        return filepath.c_str();
    }
	
    /**
     * @brief Open a new video file using date/time format for the file name and write file header
     * 
     */
	void openFile() {
		
        string filepath = getFilepath();
		
		try {
            
            outputFile.open(filepath.c_str(), ios::binary | ios::out);
            if (!outputFile.bad()) {
                //write header
                int32_t fileType = FILE_TYPE;
                int32_t width = frameWidth;
                int32_t height = frameHeight;
                int32_t format = pixelFormat;
                int32_t bpp = getBitsPerPixel(pixelFormat);
                int32_t fpf = framesPerFile;
                outputFile.write(reinterpret_cast<char*>(&fileType), sizeof(int32_t));
                outputFile.write(reinterpret_cast<char*>(&fpf), sizeof(int32_t));
                outputFile.write(reinterpret_cast<char*>(&bpp), sizeof(int32_t));
                outputFile.write(reinterpret_cast<char*>(&format), sizeof(int32_t));
                outputFile.write(reinterpret_cast<char*>(&width), sizeof(int32_t));
                outputFile.write(reinterpret_cast<char*>(&height), sizeof(int32_t));
                // Pad up to 128 bytes
                int32_t bytesWritten = FILE_TYPE;
                char zeroByte = 0;
                while (bytesWritten < 128) {
                    outputFile.write(&zeroByte, 1);
                    bytesWritten++;
                }
            }
            else {
                LOG_S(ERROR) << "Could not open file: " << filepath.c_str();
            }
            
            recording = true;
            fileOpen = true;
            framesWritten = 0;
            recordingTimer.reset();
            recordingTimer.start();

		}
		catch (exception& e)
		{
			LOG_S(ERROR) << e.what() << '\n';
		}
	}
	
    /**
     * @brief Close the current video file
     * 
     */
	void closeFile() {
		outputFile.close();
        recording = false;
        fileOpen = false;
	}

    /**
     * @brief Write frame to file, open/close file as needed
     * 
     * @param Mat frame The frame to write to the file
     */
    void writeFrame(Mat frame) {
        
        // Open new video file if needed
		if (!fileOpen) {
			openFile();
		}

        
        // Get the number of pixels to write
        unsigned int nBytes = (getBitsPerPixel(pixelFormat) / 8) * frame.size().width * frame.size().height;
        
        
        // Write to the file with frame header info
        if (!outputFile.bad()) {
            uint64_t marker = 0;
            uint64_t frameID = frameCounter;
            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*>(&systemTimestamp), sizeof(uint64_t));
            outputFile.write(reinterpret_cast<char*>(frame.data), nBytes);
        }
        else {
            LOG_S(ERROR) << "Output file corrupted.";
        }

        framesWritten++;

        // Close the video file if we have exceed the frames per file
        if (framesWritten >= framesPerFile) {
            closeFile();
            filesWritten++;
        }

    }
    
    /**
     * @brief Threaded consumer to pull frames from queue and pass to video writer object
     * 
     */
    virtual void threadedFunction() {
        
        loguru::set_thread_name("Frame Recorder Thread");
		
		LOG_S(INFO) << "Starting Frame Recorder Thread";
        
        frameCounter = 0;

        recording = true;

        Mat agFrame;

        int sumCounter = 0;

        // Continue till were are stopped AND there are no more frames to writen
        while (isThreadRunning() || frameBuffer.size() > 0) {
            
            if (frameBuffer.size() > 0 && configured) {
                
                // pull the latest frame from the queue
                lock();
                Mat frame = frameBuffer.front();
                frameBuffer.pop();
                unlock();

                if (!enabled) {
                    continue;
                }

                // Convert to 16 bit image
                Mat outputFrame(1, frame.size().width/2,CV_16UC1,frame.data);
				
                // Write every frameModulus frames (set to 1 for all frames)
                if (frameCounter % frameModulus == 0)
                {
                    // save frame and increment frame counter
                    writeFrame(outputFrame);
                }
                else if (sumFrames) {
                    if (sumCounter == 0) {
                        agFrame = outputFrame.clone();
                    }
                    else {
                        agFrame = agFrame + outputFrame;
                    }
                    sumCounter++;

                    if (sumCounter >= frameModulus-1) {
                        writeFrame(agFrame);
                        sumCounter = 0;
                        agFrame = agFrame * 0;
                    }
                }

                // Increment frame counter AFTER checking modulus and writing => zero-indexing
                frameCounter++;
            }
            else {
                sleep(threadSleep); 
            }
        }
        
        // Close any open files
		if (fileOpen) {
			closeFile();
		}
        
        LOG_S(INFO) << "Ended Video Recorder Thread";

        recording = false;
    }
    
    /**
     * @brief Get the current number of frames received
     * 
     * @return unsigned int 
     */
    unsigned int getFrameCount() {
        return frameCounter;
    }
    
    /**
     * @brief Get the current size of the frame queue
     * 
     * @note This should always be close to zero
     * @return unsigned int 
     */
    unsigned int getQueueSize() {
        return frameBuffer.size();
    }
    
    /**
     * @brief Set the number of frames to store in each video file
     * 
     * @param n 
     */
    void setFramesPerFile(int n) {
        framesPerFile = n;
    }
    
    /**
     * @brief Add a frame to the frame queue
     * 
     * @note Locks the shared frame buffer while inserting
     * @param frame 
     */
    virtual void addFrame(Mat frame) {
    
		lock();
		frameBuffer.push(frame);
		unlock();
        
    }
	
    /**
     * @brief Get a status string from the recorder
     * 
     * @return string 
     */
	virtual string getStatus() {
		stringstream ss;
		ss << "FR,"    << frameCounter << "," 
                        << framesWritten << "," 
                        << filesWritten << "," 
                        << frameBuffer.size();
		return ss.str();
	}
    
};

#endif