/**
 * DVB card access on the OS level (system function ioctl()), to tune.
 *
 * This needs to happen in C/C++, because ioctl() is not accessible from Java. 
 * This implements the JNI library libzeipisdvbtune.so, a native C++ library
 * accessible from Java. It implements the |native| methods from the
 * org.bucksch.zeipis.recorder.DVBTunerNativeJNILinux Java class.
 * We do as little as possible in C++.
 */

#include "DVBTunerNativeJNILinux.h"
#include <sys/ioctl.h>
#include <linux/dvb/version.h>
#include <linux/dvb/frontend.h>
#include <linux/dvb/dmx.h>
#include <fcntl.h>
#include <memory.h>
#include <errno.h>
#include <iostream>

#ifndef DVB_API_VERSION
#error DVB headers not found, they are expected in /usr/include/linux/ (./dvb/frontend.h etc.)
#endif
#if DVB_API_VERSION >= 5
#define HAVE_VERSION50
#else
#warning No DVB-S2 support (need driver API version 5.0 = S2API, incl. headers in /usr/include/linux/, for that)
#endif
	
void throwEx(JNIEnv* env, const char* message)
{
  jclass eClass = env->FindClass("org/bucksch/zeipis/recorder/CantUseTVCard");
  if (eClass == 0) // can't find exception class; unexpected
    return;
  env->ThrowNew(eClass, message);
}

#define CHECK_RV(message) \
  if (rv < 0) \
  { \
    throwEx(env, message); \
    close(frontendFD); \
    return -1; \
  }
#define ERROR(message) \
  { \
    throwEx(env, message); \
    close(frontendFD); \
    return -1; \
  }

/**
 * @param frontendFilename   e.g. "/dev/dvb/adapter0/frontend0"
 * @param delsys   fe_delivery_system - 5 = DVB-S, 6 = -S2, -T, -C etc.
 * @param frequency   In KHz, already considering LNB, e.g. 1587000 for 12187 MHz with universal LNB
 * @param symbolrate    e.g. 27500000
 * @param modulation   fe_modulation
 * @param fec_inner   fe_fec
 * @param rolloff   fe_rolloff (DVB-S2 only)
 * @param pilot   fe_pilot (dito)
 * @param bandwidth   fe_bandwidth (DVB-T only)
 * @param transmission_mode   fe_transmission_mode (dito)
 * @param hp_code_rate   fe_fec (dito)
 * @param lp_code_rate   fe_fec (dito)
 * @param guard_interval   fe_guard_interval (dito)
 * @param hierarchy   fe_hierarchy (dito)
 * @param inversion   fe_inversion (dito)
 * @param polv   disecq   true = vertical, false = horizontal
 * @param hiband   disecq, LNB   frequencies depend on LNB type
 * @param satNo   disecq satellite number, normally 0
 */
JNIEXPORT jint JNICALL Java_org_bucksch_zeipis_recorder_DVBTunerNativeJNILinux_tuneToNative
  (JNIEnv* env, jobject self, jstring frontendFilename,
   jint delsys,
   jint frequency, jint symbolrate, jint modulation,
   jint fec_inner, jint rolloff, jint pilot,
   jint bandwidth, jint transmission_mode,
   jint hp_code_rate, jint lp_code_rate,
   jint guard_interval, jint hierarchy, jint inversion,
   jboolean polv, jboolean hiband, jint satNo)
{
  // Open frontend device
  const char* frontendFilenameC = env->GetStringUTFChars(frontendFilename, 0);
  int frontendFD = open(frontendFilenameC, O_RDWR | O_NONBLOCK);
  env->ReleaseStringUTFChars(frontendFilename, frontendFilenameC);
  if (frontendFD < 0)
  {
    throwEx(env, "Can't open frontend device");
    return -1;
  }
  int rv;

  /* from szap
  // Remove stale events
  struct dvb_frontend_event ev;
  while (ioctl(frontendFD, FE_GET_EVENT, &ev) >= 0)
	;
  */


  // Tuning

  // Get info from device
  dvb_frontend_info info;
  rv = ioctl(frontendFD, FE_GET_INFO, &info);
  CHECK_RV("Cound not get info from device before tuning");
  std::cout << "Using device " << info.name << std::endl;

  // Check value range of frequency and symbolrate with device
  std::cout << "delsys: " << delsys << ", modulation: " << modulation
    << ", inversion: " << inversion << ", rolloff: " << rolloff << ", pilot: " << pilot
  	<< ", fec inner: " << fec_inner << ", bandwidth: " << bandwidth
  	<< ", transmission mode: " << transmission_mode << std::endl;
  std::cout << "Freq: " << info.frequency_min << "-" << info.frequency_max << ", is: " << frequency
  	<< ", symbolrate: " << info.symbol_rate_min << "-" << info.symbol_rate_max << ", is: " << symbolrate << std::endl;
  if (frequency == 0)
  	ERROR("Frequency is 0. Misconfiguration, check channel/transponder configuration.")
  if ((unsigned int) frequency < info.frequency_min)
  	ERROR("Frequency value too small. It should be in KHz. Multiply with 1000?")
  else if ((unsigned int) frequency > info.frequency_max)
  	ERROR("Frequency value too large. It should be in KHz. Devide by 1000?")
  if (symbolrate == 0)
  	ERROR("Symbol rate is 0. Misconfiguraion, check channel/transponder configuration.")
  if ((unsigned int) symbolrate < info.symbol_rate_min)
  	ERROR("Symbol rate too small. Multiply with 1000?")
  else if ((unsigned int) symbolrate > info.symbol_rate_max)
  	ERROR("Symbol rate too large. Devide by 1000?")

  // Disecq
  if (delsys == 5 || delsys == 6) // DVB-S[2]
  {
    rv = ioctl(frontendFD, FE_SET_TONE, SEC_TONE_OFF);
    CHECK_RV("Cound not turn disecq tone");
    rv = ioctl(frontendFD, FE_SET_VOLTAGE, polv ? SEC_VOLTAGE_13 : SEC_VOLTAGE_18);
    CHECK_RV("Could not set disecq voltage for polarity");
    usleep(15 * 1000);
    rv = ioctl(frontendFD, FE_SET_TONE, SEC_TONE_OFF);
    CHECK_RV("Cound not turn off disecq tone");
    usleep(15 * 1000);
    struct dvb_diseqc_master_cmd disecq_cmd = {{0xe0, 0x10, 0x38, 0xf0, 0x00, 0x00}, 4};
    disecq_cmd.msg[3] = 0xf0 | (((satNo * 4) & 0x0f) | (hiband ? 1 : 0) | (polv ? 0 : 2));
    rv = ioctl(frontendFD, FE_DISEQC_SEND_MASTER_CMD, &disecq_cmd);
    CHECK_RV("Cound send disecq master command");
    usleep(15 * 1000);
    rv = ioctl(frontendFD, FE_DISEQC_SEND_BURST, (satNo / 4) % 2 ? SEC_MINI_B : SEC_MINI_A);
    CHECK_RV("Cound set satellite on disecq");
    usleep(15 * 1000);
    rv = ioctl(frontendFD, FE_SET_TONE, hiband ? SEC_TONE_ON : SEC_TONE_OFF);
    CHECK_RV("Cound set disecq tone");
    usleep(15 * 1000);
    // TODO diseqc power on?
    // TODO more diseqc?
  }

#ifdef HAVE_VERSION50

  dtv_property p[15];
  int i = 0;
  p[i].cmd = DTV_DELIVERY_SYSTEM; p[i++].u.data = delsys;
  p[i].cmd = DTV_FREQUENCY; p[i++].u.data = frequency;
  p[i].cmd = DTV_MODULATION; p[i++].u.data = modulation;
  p[i].cmd = DTV_SYMBOL_RATE; p[i++].u.data = symbolrate;
  p[i].cmd = DTV_INVERSION; p[i++].u.data = inversion;
  if (delsys == SYS_DVBS2 || delsys == SYS_DVBS)
  {
  	p[i].cmd = DTV_INNER_FEC; p[i++].u.data = fec_inner;
  	p[i].cmd = DTV_ROLLOFF; p[i++].u.data = rolloff;
  	p[i].cmd = DTV_PILOT; p[i++].u.data = pilot;
  }
  if (delsys == SYS_DVBC_ANNEX_AC)
  {
  	p[i].cmd = DTV_INNER_FEC; p[i++].u.data = fec_inner;
  }
  if (delsys == SYS_DVBT)
  {
  	p[i].cmd = DTV_BANDWIDTH_HZ; p[i++].u.data = bandwidth;
  	p[i].cmd = DTV_TRANSMISSION_MODE; p[i++].u.data = transmission_mode;
  	p[i].cmd = DTV_GUARD_INTERVAL; p[i++].u.data = guard_interval;
  	p[i].cmd = DTV_CODE_RATE_HP; p[i++].u.data = hp_code_rate;
  	p[i].cmd = DTV_CODE_RATE_LP; p[i++].u.data = lp_code_rate;
  	p[i].cmd = DTV_HIERARCHY; p[i++].u.data = hierarchy;
  }
  p[i++].cmd = DTV_TUNE;
  dtv_properties cmdseq;
  cmdseq.num = i;
  cmdseq.props = p;

  rv = ioctl(frontendFD, FE_SET_PROPERTY, &cmdseq);
  CHECK_RV("Setting tuning params failed");

#else // = not HAVE_VERSION_50

  dvb_frontend_parameters p;

  // All
  p.frequency = frequency;
  p.inversion = (fe_spectral_inversion_t) inversion;

  if (delsys == 6) // DVB-S2
  {
  	ERROR("DVB-S2 not supported, because the new S2API was not available when this was compiled");
  }
  else if (delsys == 5) // DVB-S
  {
	p.u.qpsk.symbol_rate = symbolrate;
	p.u.qpsk.fec_inner = (fe_code_rate_t) fec_inner;
    /*
    if (fec_inner == FEC_AUTO &&
        !(info.caps & FE_CAN_FEC_AUTO))
      ERROR("Card needs to support FEC INNER AUTO");
    */
  }
  else if (delsys == 3) // DVB-T
  {
	p.u.ofdm.bandwidth = (fe_bandwidth_t) bandwidth;
	p.u.ofdm.code_rate_LP = (fe_code_rate_t) lp_code_rate;
	p.u.ofdm.code_rate_HP = (fe_code_rate_t) hp_code_rate;
	p.u.ofdm.constellation = (fe_modulation_t) modulation;
	p.u.ofdm.transmission_mode = (fe_transmit_mode_t) transmission_mode;
	p.u.ofdm.guard_interval = (fe_guard_interval_t) guard_interval;
	p.u.ofdm.hierarchy_information = (fe_hierarchy_t) hierarchy;
  }
  else if (delsys == 1) // DVB-C
  {
	p.u.qam.symbol_rate = symbolrate;
	p.u.qam.fec_inner = (fe_code_rate_t) fec_inner;
	p.u.qam.modulation = (fe_modulation_t) modulation;
  }

  rv = ioctl(frontendFD, FE_SET_FRONTEND, &p);
  CHECK_RV("Setting tuning params failed");

#endif // HAVE_VERSION_50

  // Wait for lock
  bool haveLock = false;
  int loops = 0;
  while (!haveLock && loops++ < 50) // 5s
  {
  	fe_status_t status;
	rv = ioctl(frontendFD, FE_READ_STATUS, &status);
	CHECK_RV("Reading status failed after setting tuning param");
	haveLock = status & FE_HAS_LOCK;
	usleep(100000); // 0.1s
  }
  if (!haveLock)
  {
  	uint16_t snr, sig;
	rv = ioctl(frontendFD, FE_READ_SIGNAL_STRENGTH, &sig);
	rv = ioctl(frontendFD, FE_READ_SNR, &snr);
	std::cout << "Tuning failed. Signal strength " << sig << ", Signal/noise ratio " << snr << "%" << std::endl; // TODO
	ERROR("Tuning failed");
  }

  return frontendFD;
}

JNIEXPORT jint JNICALL Java_org_bucksch_zeipis_recorder_DVBTunerNativeJNILinux_openDemuxNative
  (JNIEnv* env, jobject self, jstring demuxFilename, jint pid)
{
  // Open demux device
  const char* demuxFilenameC = env->GetStringUTFChars(demuxFilename, 0);
  int demuxFD = open(demuxFilenameC, O_RDWR);
  env->ReleaseStringUTFChars(demuxFilename, demuxFilenameC);
  if (demuxFD < 0)
  {
    throwEx(env, "Could not open de-multiplexer device");
    return -1;
  }
 
  // Buffer size From szap TODO needed?
  int rv = ioctl(demuxFD, DMX_SET_BUFFER_SIZE, 64 * 1024);
  if (rv < 0)
  {
    throwEx(env, "Setting buffer size for PID filter failed");
    close(demuxFD);
    return -1;
  }

  // Set PID
  struct dmx_pes_filter_params pesfilter;
  pesfilter.pid = pid;
  pesfilter.input = DMX_IN_FRONTEND;
  pesfilter.output = DMX_OUT_TS_TAP;
  pesfilter.pes_type = DMX_PES_OTHER;
  pesfilter.flags = DMX_IMMEDIATE_START;
  rv = ioctl(demuxFD, DMX_SET_PES_FILTER, &pesfilter);
  if (rv < 0)
  {
    throwEx(env, "Could not set PID");
    close(demuxFD);
    return -1;
  }

  return demuxFD;
}

JNIEXPORT void JNICALL Java_org_bucksch_zeipis_recorder_DVBTunerNativeJNILinux_closeNative
  (JNIEnv* env, jobject self, jint fd)
{
  if (fd > 0)
    close(fd);
}

JNIEXPORT jint JNICALL Java_org_bucksch_zeipis_recorder_DVBTunerNativeJNILinux_findPMTPIDNative
  (JNIEnv* env, jobject self, jstring demuxFilename, jint serviceID)
{
  struct dmx_sct_filter_params sct_filter;
  memset(&sct_filter, 0, sizeof(sct_filter));
  sct_filter.pid = 0;
  sct_filter.filter.filter[0] = 0x00;
  sct_filter.filter.mask[0] = 0xff;
  sct_filter.flags = DMX_IMMEDIATE_START | DMX_CHECK_CRC;

  const char* demuxFilenameC = env->GetStringUTFChars(demuxFilename, 0);
  int demuxPATFD = open(demuxFilenameC, O_RDWR);
  env->ReleaseStringUTFChars(demuxFilename, demuxFilenameC);
  if (demuxPATFD < 0)
  {
	throwEx(env, "Opening demux section filter for PAT failed");
    return -1;
  }

  int rv = ioctl(demuxPATFD, DMX_SET_FILTER, &sct_filter);
  if (rv < 0)
  {
	throwEx(env, "Setting demux section filter for PAT failed");
    close(demuxPATFD);
    return -1;
  }

  unsigned char section[4096];
  const int maxLoops = 100;
  int loop = maxLoops;
  while (loop-- > 0)
  {
    int bytesRead = read(demuxPATFD, section, sizeof(section));
    if (bytesRead < 0)
    {
      //if (errno == EOVERFLOW)
      //	continue;
	  throwEx(env, "Reading PAT failed");
      return -1;
    }
    if (bytesRead < 16)
      continue;

    int length = ((section[1] & 0x0f) << 8) | section[2];
    if (bytesRead != length + 3) // checks that read() returned on section borders/chunks? reliable?
      continue;

    unsigned char* p = section;
    int remaining = length;
    p += 8;
    remaining -= 8;
    while (remaining > 0)
    {
      int curServiceID = (p[0] << 8) | p[1];
      if (curServiceID == serviceID)
        return ((p[2] & 0x1f) << 8) | p[3]; // This is the PMT PID
      p += 4;
      remaining -= 4;
    }
  }
  return -1;
}

JNIEXPORT jint JNICALL Java_org_bucksch_zeipis_recorder_DVBTunerNativeJNILinux_openSectionFilterNative
  (JNIEnv* env, jobject self, jstring demuxFilename, jint sectionPID, jint tableID, jint tableIDExt)
{
  struct dmx_sct_filter_params sct_filter;
  memset(&sct_filter, 0, sizeof(sct_filter));
  sct_filter.pid = sectionPID;
  if (tableID < 0x100 && tableID >= 0)
  {
    sct_filter.filter.filter[0] = (uint8_t) tableID;
    sct_filter.filter.mask[0]   = 0xff;
  }
  if (tableIDExt < 0x10000 && tableIDExt >= 0)
  {
    sct_filter.filter.filter[1] = (uint8_t) ((tableIDExt >> 8) & 0xff);
    sct_filter.filter.filter[2] = (uint8_t) (tableIDExt & 0xff);
    sct_filter.filter.mask[1] = 0xff;
    sct_filter.filter.mask[2] = 0xff;
  }
  sct_filter.flags = DMX_IMMEDIATE_START | DMX_CHECK_CRC;

  const char* demuxFilenameC = env->GetStringUTFChars(demuxFilename, 0);
  int fd = open(demuxFilenameC, O_RDWR | O_NONBLOCK);
  env->ReleaseStringUTFChars(demuxFilename, demuxFilenameC);
  if (fd < 0)
  {
	throwEx(env, "Opening demux section filter failed");
    return -1;
  }

  int rv = ioctl(fd, DMX_SET_FILTER, &sct_filter);
  if (rv < 0)
  {
	throwEx(env, "Setting demux section filter failed");
    close(fd);
    return -1;
  }

  return fd;
}

JNIEXPORT jint JNICALL Java_org_bucksch_zeipis_recorder_DVBTunerNativeJNILinux_readSectionNative
  (JNIEnv* env, jobject self, jint fd, jbyteArray buffer, jint length)
{
  jbyte cbuffer[length];
  int bytesRead = read(fd, cbuffer, length);
  if (bytesRead < 0)
  {
    if (errno == EAGAIN)
      return -11;
    //if (errno == EOVERFLOW)
    //  continue;
    throwEx(env, "Reading MPEG TS section (transponder metadata) failed");
    return -1;
  }
  env->SetByteArrayRegion(buffer, 0, length, cbuffer);
  return bytesRead;
}
