#include <iostream>
#include <chrono>
#include <cmath>
#include <random>
#include <semaphore>
#include <google/protobuf/arena.h>
#include <thread>
#include <map>

#include "rpc_service.hpp"

struct StatusRequest {
};
struct SensorStatus {
    uint64_t online_since;
    uint32_t battery_charge;
    uint32_t battery_capacity;
    uint32_t max_sampling_rate;
    uint32_t cur_sampling_rate;
    uint32_t max_chunk_size;
    uint32_t cur_chunk_size;
    uint32_t n_chunk_capacity;
    uint32_t n_full_data_chunks;
    uint32_t n_empty_data_chunks;
};


struct SetSamplingPeriodRequest {
    uint32_t new_sampling_period;
};
enum SetSamplingPeriodResult {
    SAMPLING_PERIOD_OK,
    SAMPLING_PERIOD_OUT_OF_RANGE
};

struct PopDataChunkRequest {
};
struct DataChunk {
    uint64_t begin;
    uint32_t sampling_period;
    float temperature_data[];
};

template < size_t T_Capacity >
struct RandomSensor
{
private:
    std::chrono::time_point<std::chrono::high_resolution_clock> online_since;
    std::chrono::duration<uint64_t, std::milli> period_duration;
    uint32_t chunk_size;
    uint32_t next_free;
    uint32_t next_full;

    float bat_charge;

    std::counting_semaphore<> sem_full;
    std::counting_semaphore<> sem_free;

    float data[ T_Capacity ];
    std::thread sensor_thread;

    void generate_value()
    {
        sem_free.acquire();

        data[ next_free ] = (float) next_free / 100.0;
        next_free = (next_free + 1) % T_Capacity;

        sem_full.release();
    }

public:
    RandomSensor()
        : online_since(std::chrono::high_resolution_clock::now())
        , period_duration( 10 )
        , chunk_size( 8192 )
        , next_free( 0 )
        , next_full( 0 )
        , bat_charge( 8200.0 )
        , sem_full( 0 )
        , sem_free( T_Capacity )
        , sensor_thread([&] {
            while(bat_charge > 0) {
                generate_value();
                bat_charge -= 0.01;
                std::this_thread::sleep_for( period_duration );
            }
        })
    {}

    ~RandomSensor() {
        sensor_thread.join();
    }

    uint32_t n_chunks_capacity() const {
        return T_Capacity / chunk_size;
    }

    uint32_t n_full_chunks() const {
        return (( next_free - next_full ) % T_Capacity) / chunk_size;
    }

    uint32_t n_empty_chunks() const {
        return n_chunks_capacity() - n_full_chunks();
    }


    void get_status(
        StatusRequest const & request,
        SensorStatus & status
    ) {
        status.online_since = std::chrono::duration_cast<std::chrono::milliseconds>(online_since.time_since_epoch()).count();
        status.battery_capacity = 8200;
        status.battery_charge = bat_charge;
        status.max_sampling_rate = 10;
        status.cur_sampling_rate = std::chrono::duration_cast<std::chrono::milliseconds>(period_duration).count();
        status.cur_chunk_size = chunk_size;
        status.n_chunk_capacity = n_chunks_capacity();
        status.n_full_data_chunks = n_full_chunks();
        status.n_empty_data_chunks = n_empty_chunks();
    }

    void set_sampling_period(
        SetSamplingPeriodRequest const & request,
        SetSamplingPeriodResult & result
    ) {
        result = SAMPLING_PERIOD_OK;
    }

    void pop_data_chunk(
        PopDataChunkRequest const & request,
        DataChunk & data_chunk
    ) {

    }
};

enum SensorRpcTag {
    GET_STATUS = 0,
    SET_SAMPLING_PERIOD,
    POP_DATA_CHUNK
};

int main( int argc, char* argv[] ) {
    RandomSensor< 1048576 > sensor;

    RpcService<SensorRpcTag> service(8070);
    service.register_handler( GET_STATUS,
        std::function([&sensor]( StatusRequest const & request, SensorStatus & status ) {
            sensor.get_status(request, status);
        }));
    service.register_handler( SET_SAMPLING_PERIOD,
        std::function([&sensor]( SetSamplingPeriodRequest const & request, SetSamplingPeriodResult & result ) {
            sensor.set_sampling_period(request, result);
        }));
    service.register_handler( POP_DATA_CHUNK,
        std::function([&sensor]( PopDataChunkRequest const & request, DataChunk & result ) {
            sensor.pop_data_chunk(request, result);
        }));

    return service.run();
}