#include <chrono>

#include "GeoNetworkingLayer.hh"
#include "GeoNetworkingTypes.hh"

#include "loggers.hh"

using namespace LibItsGeoNetworking__TypesAndValues;

GeoNetworkingLayer::GeoNetworkingLayer(const std::string & p_type, const std::string & param) : TLayer<LibItsGeoNetworking__TestSystem::GeoNetworkingPort>(p_type), _params(), _codec(), _beacon(NULL), _thread(NULL), _sendData(), _running(FALSE) {
  loggers::get_instance().log(">>> GeoNetworkingLayer::GeoNetworkingLayer: %s, %s", to_string().c_str(), param.c_str());
  // Setup parameters
  Params::convert(_params, param);
  // Sanity checks
  INTEGER latitude;
  Params::const_iterator it = _params.find(Params::latitude);
  if (it != _params.cend()) {
    latitude = str2int(CHARSTRING(it->second.c_str()));
  }
  INTEGER longitude;
  it = _params.find(Params::longitude);
  if (it != _params.cend()) {
    longitude = str2int(CHARSTRING(it->second.c_str()));
  }
  OCTETSTRING ll_address;
  it = _params.find(Params::ll_address);
  if (it != _params.cend()) {
    ll_address = str2oct(CHARSTRING(it->second.c_str()));
  }
  // Add broadcast address if needed
  it = _params.find("mac_bc");
  if (it == _params.cend()) {
    _params.insert(std::pair<std::string, std::string>(std::string("mac_bc"), "FFFFFFFFFFFF"));
  }
  fill_beacon(latitude, longitude, ll_address);
  // Prepare the static part of the TestSystem beacon
  it = _params.find("");
  if ((it != _params.cend()) && (it->second.compare("1") == 0)) {
    start_beaconing();
  }
}

GeoNetworkingLayer::~GeoNetworkingLayer() {
  stop_beaconing();
  delete _beacon;
}
void GeoNetworkingLayer::sendMsg(const LibItsGeoNetworking__TestSystem::GeoNetworkingReq& p, Params& params) {
  loggers::get_instance().log(">>> GeoNetworkingLayer::sendMsg");

  // Encode GeoNetworking PDU
  OCTETSTRING data;
  _codec.encode(p.msgOut(), data);
  sendData(data, params);
}

void GeoNetworkingLayer::sendData(OCTETSTRING& data, Params& params) {
  loggers::get_instance().log_msg(">>> GeoNetworkingLayer::sendData: ", data);
  while (_sendData.try_lock() == FALSE) {
    // not ready yet
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
  } // End of 'while' statement
  sendToAllLayers(data, params);
  _sendData.unlock();
  loggers::get_instance().log("<<< GeoNetworkingLayer::sendData");
}

void GeoNetworkingLayer::receiveData(OCTETSTRING& data, Params& params) { 
  loggers::get_instance().log_msg(">>> GeoNetworkingLayer::receiveData: ", data);
  // Decode the payload
  LibItsGeoNetworking__TestSystem::GeoNetworkingInd p;
  _codec.decode(data, p.msgIn(), &params);
  // Add lower layers parameters
  // 1. Destination MAC address
  Params::const_iterator it = params.find(Params::mac_dst);
  if (it != params.cend()) {
    loggers::get_instance().log("GeoNetworkingLayer::receiveData: dst=%s", it->second.c_str());
    p.macDestinationAddress() = str2oct(CHARSTRING(it->second.c_str()));
  } else {
    p.macDestinationAddress() = str2oct(CHARSTRING(_params["mac_bc"].c_str()));
  }
  // 2. ssp
  it = params.find(Params::ssp);
  if (it != params.cend()) {
    loggers::get_instance().log("GeoNetworkingLayer::receiveData: ssp=%s", it->second.c_str());
    p.ssp() = str2bit(CHARSTRING(it->second.c_str()));
  } else {
    p.ssp().set_to_omit();
  }
  // 3. its_aid
  it = params.find(Params::its_aid);
  if (it != params.cend()) {
    loggers::get_instance().log("GeoNetworkingLayer::receiveData: its_aid=%s", it->second.c_str());
    p.its__aid() = std::stoi(it->second.c_str());
  } else {
    p.its__aid().set_to_omit();
  }
  
  // Pass the GeoNetworking raw payload to the upper layers if any
  it = params.find(Params::gn_payload);
  if (it != params.cend()) {
    loggers::get_instance().log("GeoNetworkingLayer::receiveData: gn_payload=%s", it->second.c_str());
    OCTETSTRING os(str2oct(CHARSTRING(it->second.c_str())));
    receiveToAllLayers(os, params);
  } else {
    loggers::get_instance().warning("GeoNetworkingLayer::receiveData: No payload to pass to upper layers");
  }
  // Pass it to the ports
  toAllUpperPorts(p, params);
}

void GeoNetworkingLayer::start_beaconing() {
  loggers::get_instance().log(">>> GeoNetworkingLayer::start_beaconing");

  // Sanity checks
  if (_thread != NULL) { // Already started
    return;
  }

  // Create the offline reader thread
  _thread = new std::thread(&GeoNetworkingLayer::run_beaconing, (void *)this);
  if (_thread == NULL) {
    loggers::get_instance().error("GeoNetworkingLayer::start_beaconing: Failed to start beaconing thread");
  }
  // Wait for the thread
  while (_running == FALSE) {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
  }
  // Thread was started
  loggers::get_instance().log("<<< PcapLayer::PcapLayer");    
}

void GeoNetworkingLayer::stop_beaconing() {
  loggers::get_instance().log(">>> GeoNetworkingLayer::stop_beaconing");
  if (_thread != NULL) {
    _running = FALSE;
    // Wait for the working thread to terminate
    _thread->join();
    loggers::get_instance().log("GeoNetworkingLayer::stop_beaconing: Thread were stops");
    _thread = NULL;
  }
}

void* GeoNetworkingLayer::run_beaconing(void* p_this) {
  loggers::get_instance().log(">>> GeoNetworkingLayer::run_beaconing");

  // Pointer the main object
  GeoNetworkingLayer& p = *static_cast<GeoNetworkingLayer *>(p_this);
  Params params(p._params);
  std::chrono::milliseconds expiry(1000); // Default is set to 1 second
  Params::const_iterator it = params.find("TsBeaconInterval");
  if (it != params.cend()) {
    expiry = std::chrono::milliseconds(std::stoul(it->second));
  }
  ExtendedHeader* eh = static_cast<ExtendedHeader *>(p._beacon->gnPacket().packet().extendedHeader().get_opt_value());
  if (eh == NULL) {
    loggers::get_instance().error("GeoNetworkingLayer::run_beaconing: Wrong cast");
  }
  GeoNetworkingCodec codec;
  // Start beaconing
  p._running = TRUE;
  std::this_thread::sleep_for(std::chrono::milliseconds(100));
  while (p._running == TRUE) { // Loop while _running flag is up
    // Update timestamp
    unsigned long long ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count() - 1072911600000L; // TODO Add method such as its_tme() & its_time_mod()
    eh->beaconHeader().srcPosVector().timestamp__() = ms;
    // Encode message using TITAN because of payload in omited
    TTCN_Buffer encoding_buffer;
    p._beacon->encode(*(p._beacon->get_descriptor()), encoding_buffer, TTCN_EncDec::CT_RAW);
    OCTETSTRING data(encoding_buffer.get_len(), encoding_buffer.get_data());
    // Send it
    p.sendData(data, params);
    // Timer
    ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count() + expiry.count();
    while ((p._running == TRUE) && (ms > std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count()));
  } // End of 'while' statement<
  
  loggers::get_instance().log("<<< GeoNetworkingLayer::run_beaconing");
  return NULL;
}

void GeoNetworkingLayer::fill_beacon(INTEGER& p_latitude, INTEGER& p_longitude, OCTETSTRING& p_ll_address)
{
  _beacon = new GeoNetworkingPdu();
  HeaderTST h;
  h.beaconHdr() = BeaconHeaderType(
				   HeaderType(HeaderType::e__beacon),
				   0
				   );
  ExtendedHeader eh;
  eh.beaconHeader() = BeaconHeader(
				   LongPosVector(
						 GN__Address(
							     TypeOfAddress(TypeOfAddress::e__manual),
							     StationType(StationType::e__roadSideUnit),
							     33,
							     p_ll_address
							     ),
						 0,
						 p_latitude,
						 p_longitude,
						 int2bit(0, 1),
						 0,
						 0
						 )
				   );
  _beacon->basicHeader() = BasicHeader(
				       0,
				       BasicNextHeader(
						       BasicNextHeader::e__commonHeader
						       ),
				       0,
				       Lifetime(
						4,
						LtBase(LtBase::e__50ms)
						),
				       5
				       );
  _beacon->gnPacket().packet() = GnNonSecuredPacket(
						    CommonHeader(
								 NextHeader(
									    NextHeader::e__any
									    ),
								 0,
								 h,
								 TrafficClass(
									      SCF(SCF::e__scfDisabled),
									      ChannelOffload(ChannelOffload::e__choffDisabled),
									     0
									     ),
								 int2bit(0, 8),
								 0,
								 1,
								 0
								 ),
						    OPTIONAL<ExtendedHeader>(eh),
						    OPTIONAL<GnRawPayload>()
						    );
  _beacon->gnPacket().packet().payload().set_to_omit();
  _beacon->gnPacket().securedMsg().set_to_omit();
  //  loggers::get_instance().log_msg("GeoNetworkingLayer::GeoNetworkingLayer: beacon value: ", *p._beacon);
}

class GeoNetworkingFactory: public LayerFactory {
  static GeoNetworkingFactory _f;
public:
  GeoNetworkingFactory();
  virtual Layer * createLayer(const std::string & type,
			      const std::string & param);
};

GeoNetworkingFactory::GeoNetworkingFactory() {
  // Register factory
  loggers::get_instance().log(">>> GeoNetworkingFactory::GeoNetworkingFactory");
  LayerStackBuilder::RegisterLayerFactory("GN", this);
}

Layer * GeoNetworkingFactory::createLayer(const std::string & type, const std::string & param) {
  return new GeoNetworkingLayer(type, param);
}

GeoNetworkingFactory GeoNetworkingFactory::_f;
