/*
 *  TV Input - Linux DVB interface
 *  Copyright (C) 2007 Andreas Öman
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#define _GNU_SOURCE
#include <pthread.h>
#include <assert.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <dirent.h>
#include <fcntl.h>
#include <errno.h>
#include <ctype.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#include <linux/dvb/frontend.h>
#include <linux/dvb/dmx.h>

#include "settings.h"

#include "tvheadend.h"
#include "dvb.h"
#include "dvb_support.h"
#include "tsdemux.h"
#include "notify.h"
#include "service.h"
#include "epggrab.h"
#include "diseqc.h"
#include "atomic.h"

struct th_dvb_adapter_queue dvb_adapters;
struct th_dvb_mux_instance_tree dvb_muxes;
static void *dvb_adapter_input_dvr(void *aux);
static void tda_init(th_dvb_adapter_t *tda);

/**
 * Adapters that are known to have SNR support
 */
static const char* dvb_adapter_snr_whitelist[] = {
  "Sony CXD2820R",
  "stv090x",
  "TurboSight",
  NULL
};

/**
 *
 */
static th_dvb_adapter_t *
tda_alloc(void)
{
  int i;
  th_dvb_adapter_t *tda = calloc(1, sizeof(th_dvb_adapter_t));
  pthread_mutex_init(&tda->tda_delivery_mutex, NULL);

  for (i = 0; i < TDA_SCANQ_NUM; i++ )
    TAILQ_INIT(&tda->tda_scan_queues[i]);
  TAILQ_INIT(&tda->tda_initial_scan_queue);
  TAILQ_INIT(&tda->tda_satconfs);
  streaming_pad_init(&tda->tda_streaming_pad);
  return tda;
}


/**
 * Save config for the given adapter
 */
static void
tda_save(th_dvb_adapter_t *tda)
{
  htsmsg_t *m = htsmsg_create_map();

  lock_assert(&global_lock);

  htsmsg_add_u32(m, "enabled", tda->tda_enabled);
  if (tda->tda_fe_path)
    htsmsg_add_str(m, "fe_path", tda->tda_fe_path);
  if (tda->tda_demux_path)
    htsmsg_add_str(m, "dmx_path", tda->tda_demux_path);
  if (tda->tda_dvr_path)
    htsmsg_add_str(m, "dvr_path", tda->tda_dvr_path);
  htsmsg_add_str(m, "type", dvb_adaptertype_to_str(tda->tda_type));
  htsmsg_add_str(m, "displayname", tda->tda_displayname);
  htsmsg_add_u32(m, "autodiscovery", tda->tda_autodiscovery);
  htsmsg_add_u32(m, "idlescan", tda->tda_idlescan);
  htsmsg_add_u32(m, "idleclose", tda->tda_idleclose);
  htsmsg_add_u32(m, "skip_checksubscr", tda->tda_skip_checksubscr);
  htsmsg_add_u32(m, "sidtochan", tda->tda_sidtochan);
  htsmsg_add_u32(m, "qmon", tda->tda_qmon);
  htsmsg_add_u32(m, "poweroff", tda->tda_poweroff);
  htsmsg_add_u32(m, "nitoid", tda->tda_nitoid);
  htsmsg_add_u32(m, "diseqc_version", tda->tda_diseqc_version);
  htsmsg_add_u32(m, "diseqc_repeats", tda->tda_diseqc_repeats);
  htsmsg_add_u32(m, "extrapriority", tda->tda_extrapriority);
  htsmsg_add_u32(m, "skip_initialscan", tda->tda_skip_initialscan);
  htsmsg_add_u32(m, "disable_pmt_monitor", tda->tda_disable_pmt_monitor);
  htsmsg_add_s32(m, "full_mux_rx", tda->tda_full_mux_rx);
  htsmsg_add_u32(m, "grace_period", tda->tda_grace_period);
  hts_settings_save(m, "dvbadapters/%s", tda->tda_identifier);
  htsmsg_destroy(m);
}

/**
 *
 */
void
dvb_adapter_set_enabled(th_dvb_adapter_t *tda, int on)
{
  if(tda->tda_enabled == on)
    return;

  lock_assert(&global_lock);

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" %s",
         tda->tda_displayname, on ? "Enabled" : "Disabled");

  tda->tda_enabled = on;
  tda_save(tda);
  if (!on) {
    gtimer_disarm(&tda->tda_mux_scanner_timer);
    if (tda->tda_mux_current)
      dvb_fe_stop(tda->tda_mux_current, 0);
    dvb_adapter_stop(tda, TDA_OPT_ALL);
  } else {
    tda_init(tda);
  }
}

/**
 *
 */
void
dvb_adapter_set_displayname(th_dvb_adapter_t *tda, const char *s)
{
  lock_assert(&global_lock);

  if(!strcmp(s, tda->tda_displayname))
    return;

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" renamed to \"%s\"",
	 tda->tda_displayname, s);

  free(tda->tda_displayname);
  tda->tda_displayname = strdup(s);
  
  tda_save(tda);

  dvb_adapter_notify(tda);
}

/**
 *
 */
void
dvb_adapter_set_auto_discovery(th_dvb_adapter_t *tda, int on)
{
  if(tda->tda_autodiscovery == on)
    return;

  lock_assert(&global_lock);

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" mux autodiscovery set to: %s",
	 tda->tda_displayname, on ? "On" : "Off");

  tda->tda_autodiscovery = on;
  tda_save(tda);
}


/**
 *
 */
void
dvb_adapter_set_skip_initialscan(th_dvb_adapter_t *tda, int on)
{
  if(tda->tda_skip_initialscan == on)
    return;

  lock_assert(&global_lock);

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" skip initial scan set to: %s",
	 tda->tda_displayname, on ? "On" : "Off");

  tda->tda_skip_initialscan = on;
  tda_save(tda);
}


/**
 *
 */
void
dvb_adapter_set_idlescan(th_dvb_adapter_t *tda, int on)
{
  if(tda->tda_idlescan == on)
    return;

  lock_assert(&global_lock);

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" idle mux scanning set to: %s",
	 tda->tda_displayname, on ? "On" : "Off");

  tda->tda_idlescan = on;
  tda_save(tda);
}

/**
 *
 */
void
dvb_adapter_set_idleclose(th_dvb_adapter_t *tda, int on)
{
  if(tda->tda_idleclose == on)
    return;

  lock_assert(&global_lock);

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" idle fd close set to: %s",
	 tda->tda_displayname, on ? "On" : "Off");

  tda->tda_idleclose = on;
  tda_save(tda);
}


/**
 *
 */
void
dvb_adapter_set_skip_checksubscr(th_dvb_adapter_t *tda, int on)
{
  if(tda->tda_skip_checksubscr == on)
    return;

  lock_assert(&global_lock);

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" skip service availability check when mapping set to: %s",
   tda->tda_displayname, on ? "On" : "Off");

  tda->tda_skip_checksubscr = on;
  tda_save(tda);
}

/**
 *
 */
void
dvb_adapter_set_qmon(th_dvb_adapter_t *tda, int on)
{
  if(tda->tda_qmon == on)
    return;

  lock_assert(&global_lock);

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" quality monitoring set to: %s",
	 tda->tda_displayname, on ? "On" : "Off");

  tda->tda_qmon = on;
  tda_save(tda);
}

/**
 *
 */
void
dvb_adapter_set_sidtochan(th_dvb_adapter_t *tda, int on)
{
  if(tda->tda_sidtochan == on)
    return;

  lock_assert(&global_lock);

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" use SID as channel number when mapping set to: %s",
	 tda->tda_displayname, on ? "On" : "Off");

  tda->tda_sidtochan = on;
  tda_save(tda);
}

/**
 *
 */
void
dvb_adapter_set_poweroff(th_dvb_adapter_t *tda, int on)
{
  if(tda->tda_poweroff == on)
    return;

  lock_assert(&global_lock);

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" idle poweroff set to: %s",
	 tda->tda_displayname, on ? "On" : "Off");

  tda->tda_poweroff = on;
  tda_save(tda);
}


/**
 *
 */
void
dvb_adapter_set_nitoid(th_dvb_adapter_t *tda, int nitoid)
{
  lock_assert(&global_lock);

  if(tda->tda_nitoid == nitoid)
    return;

  tvhlog(LOG_NOTICE, "dvb", "NIT-o network id \"%d\" changed to \"%d\"",
	 tda->tda_nitoid, nitoid);

  tda->tda_nitoid = nitoid;
  
  tda_save(tda);

}


/**
 *
 */
void
dvb_adapter_set_diseqc_version(th_dvb_adapter_t *tda, unsigned int v)
{
  if(v > 1)
    v = 1;

  if(tda->tda_diseqc_version == v)
    return;

  lock_assert(&global_lock);

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" DiSEqC version set to: %s",
	 tda->tda_displayname, v ? "1.1 / 2.1" : "1.0 / 2.0" );

  tda->tda_diseqc_version = v;
  tda_save(tda);
}

/**
 * sets the number of diseqc repeats to perform
 */
void
dvb_adapter_set_diseqc_repeats(th_dvb_adapter_t *tda, unsigned int repeats)
{
  if(tda->tda_diseqc_repeats == repeats)
    return;
  lock_assert(&global_lock);
  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" DiSEqC repeats set to: %i",
         tda->tda_displayname, repeats);
  tda->tda_diseqc_repeats = repeats;
  tda_save(tda);
}

/**
 *
 */
void
dvb_adapter_set_extrapriority(th_dvb_adapter_t *tda, int extrapriority)
{
  lock_assert(&global_lock);

  if(tda->tda_extrapriority == extrapriority)
    return;

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" extra priority \"%d\" changed to \"%d\"",
         tda->tda_displayname, tda->tda_extrapriority, extrapriority);

  tda->tda_extrapriority = extrapriority;
  tda_save(tda);
}

/**
 *
 */
void
dvb_adapter_set_disable_pmt_monitor(th_dvb_adapter_t *tda, int on)
{
  if(tda->tda_disable_pmt_monitor == on)
    return;

  lock_assert(&global_lock);

  tvhlog(LOG_NOTICE, "dvb", "Adapter \"%s\" disabled PMT monitoring set to: %s",
	 tda->tda_displayname, on ? "On" : "Off");

  tda->tda_disable_pmt_monitor = on;
  tda_save(tda);
}


/**
 *
 */
void
dvb_adapter_set_full_mux_rx(th_dvb_adapter_t *tda, int on)
{
  const char* label[] = { "Auto", "Off", "On" };
  
  if (on < -1) on = -1;
  if (on >  1) on = 1;

  if(tda->tda_full_mux_rx == on)
    return;

  lock_assert(&global_lock);

  tvhlog(LOG_NOTICE, "dvb",
         "Adapter \"%s\" disabled full MUX receive set to: %s",
	 tda->tda_displayname, label[on+1]);

  tda->tda_full_mux_rx = on;
  tda_save(tda);
}

/**
 *
 */
void
dvb_adapter_set_grace_period(th_dvb_adapter_t *tda, uint32_t p)
{
  if (tda->tda_grace_period == p)
    return;

  tvhlog(LOG_NOTICE, "dvb",
         "Adapter \"%s\" set grace period to %d", tda->tda_displayname, p);

  tda->tda_grace_period = p;
  tda_save(tda);
}


/**
 *
 */
static void
dvb_adapter_checkspeed(th_dvb_adapter_t *tda)
{
  char dev[64];

  snprintf(dev, sizeof(dev), "dvb/dvb%d.dvr0", tda->tda_adapter_num);
  tda->tda_hostconnection = get_device_connection(dev);
}




/**
 * Return 1 if an adapter is capable of receiving a full mux
 */
static int
check_full_stream(th_dvb_adapter_t *tda)
{
  struct dmx_pes_filter_params dmx_param;
  int r;

  if(tda->tda_full_mux_rx != -1)
    return tda->tda_full_mux_rx;

  if(tda->tda_hostconnection == HOSTCONNECTION_USB12)
    return 0; // Don't even bother, device <-> host interface is too slow

  if(tda->tda_hostconnection == HOSTCONNECTION_USB480)
    return 0; // USB in general appears to have CPU loading issues?

  int fd = tvh_open(tda->tda_demux_path, O_RDWR, 0);
  if(fd == -1)
    return 0;

  memset(&dmx_param, 0, sizeof(dmx_param));
  dmx_param.pid = 0x2000;
  dmx_param.input = DMX_IN_FRONTEND;
  dmx_param.output = DMX_OUT_TS_TAP;
  dmx_param.pes_type = DMX_PES_OTHER;
  dmx_param.flags = DMX_IMMEDIATE_START;

  r = ioctl(fd, DMX_SET_PES_FILTER, &dmx_param);
  close(fd);
  return !r;
}


/**
 *
 */
static void
tda_add(int adapter_num)
{
  char buf[1024], path[200], fepath[256], dmxpath[256], dvrpath[256];
  int i, j, fd;
  th_dvb_adapter_t *tda;
  struct dvb_frontend_info fe_info;
  DIR *dirp;
  const char **str;

  /* Check valid adapter */
  snprintf(path, sizeof(path), "/dev/dvb/adapter%d", adapter_num);
  dirp = opendir(path);
  if (!dirp)
    return;
  closedir(dirp);

  /* Check each frontend */
  // Note: this algo will fail if there are really exotic variations
  //       of tuners and demuxers etc... but you can always manually
  //       override the config
  for (i = 0; i < 32; i++) {

    /* Open/Query frontend */
    snprintf(fepath, sizeof(fepath), "%s/frontend%d", path, i);
    fd = tvh_open(fepath, O_RDONLY, 0);
    if (fd == -1) {
      if (errno != ENOENT)
        tvhlog(LOG_ALERT, "dvb",
	             "%s: unable to open (err=%s)", fepath, strerror(errno));
      continue;
    }
    if (ioctl(fd, FE_GET_INFO, &fe_info)) {
      tvhlog(LOG_ALERT, "dvb",
	           "%s: unable to query (err=%s)", fepath, strerror(errno));
      close(fd);
      continue;
    }
    close(fd);

    /* Find Demux */
    snprintf(dmxpath, sizeof(dmxpath), "%s/demux%d", path, i);
    fd = tvh_open(dmxpath, O_RDONLY, 0);
    if (fd == -1) {
      snprintf(dmxpath, sizeof(dmxpath), "%s/demux%d", path, 0);
      fd = tvh_open(dmxpath, O_RDONLY, 0);
    }
    if (fd == -1) {
      tvhlog(LOG_ALERT, "dvb", "%s: unable to find demux", fepath);
      continue;
    }
    close(fd);

    /* Find DVR */
    snprintf(dvrpath, sizeof(dvrpath), "%s/dvr%d", path, i);
    fd = tvh_open(dvrpath, O_RDONLY, 0);
    if (fd == -1) {
      snprintf(dvrpath, sizeof(dvrpath), "%s/dvr%d", path, 0);
      fd = tvh_open(dvrpath, O_RDONLY, 0);
    }
    if (fd == -1) {
      tvhlog(LOG_ALERT, "dvb", "%s: unable to find dvr", fepath);
      continue;
    }
    close(fd);

    /* Create entry */
    tda = tda_alloc();
    tda->tda_adapter_num   = adapter_num;
    tda->tda_rootpath      = strdup(path);
    tda->tda_fe_path       = strdup(fepath);
    tda->tda_demux_path    = strdup(dmxpath);
    tda->tda_dvr_path      = strdup(dvrpath);
    tda->tda_fe_fd         = -1;
    tda->tda_dvr_pipe.rd   = -1;
    tda->tda_full_mux_rx   = -1;
    tda->tda_type          = fe_info.type;
    tda->tda_autodiscovery = tda->tda_type != FE_QPSK;
    tda->tda_idlescan      = 1;
    tda->tda_sat           = tda->tda_type == FE_QPSK;
    tda->tda_displayname   = strdup(fe_info.name);
    tda->tda_fe_info       = malloc(sizeof(struct dvb_frontend_info));
    memcpy(tda->tda_fe_info, &fe_info, sizeof(struct dvb_frontend_info));

    /* ID the device (for configuration etc..) */
    // Note: would be better to include frontend dev in name, but that
    //       would require additional work to migrate config
    snprintf(buf, sizeof(buf), "%s_%s", tda->tda_rootpath,
	           tda->tda_fe_info->name);
    for(j = 0; j < strlen(buf); j++)
      if(!isalnum((int)buf[j]))
        buf[j] = '_';
    tda->tda_identifier = strdup(buf);

    /* Check device connection */
    dvb_adapter_checkspeed(tda);

    /* Adapters known to provide valid SNR */
    str = dvb_adapter_snr_whitelist;
    while (*str) {
      if (strcasestr(fe_info.name, *str)) {
        tda->tda_snr_valid = 1;
        break;
      }
      str++;
    }

    /* Store */
    tvhlog(LOG_INFO, "dvb",
	         "Found adapter %s (%s) via %s%s", path, tda->tda_fe_info->name,
	         hostconnection2str(tda->tda_hostconnection),
           tda->tda_snr_valid ? ", Reports valid SNR values" : "");
    TAILQ_INSERT_TAIL(&dvb_adapters, tda, tda_global_link);
  }
}

/**
 *
 */
static void
tda_add_from_file(const char *filename)
{
  int i, r;
  th_dvb_adapter_t *tda;
  char buf[400];

  tda = tda_alloc();

  tda->tda_adapter_num = -1;
  tda->tda_fe_fd       = -1;
  tda->tda_dvr_pipe.rd = -1;

  tda->tda_enabled = 1;

  tda->tda_type = -1;

  snprintf(buf, sizeof(buf), "%s", filename);

  r = strlen(buf);
  for(i = 0; i < r; i++)
    if(!isalnum((int)buf[i]))
      buf[i] = '_';

  tda->tda_identifier = strdup(buf);
  
  tda->tda_autodiscovery = 0;
  tda->tda_idlescan = 0;

  tda->tda_sat = 0;
 
  tda->tda_full_mux_rx = 1;

  /* Come up with an initial displayname, user can change it and it will
     be overridden by any stored settings later on */

  tda->tda_displayname = strdup(filename);

  TAILQ_INSERT_TAIL(&dvb_adapters, tda, tda_global_link);
}

/**
 * Initiliase
 */
static void tda_init (th_dvb_adapter_t *tda)
{
  /* Disabled - ignore */
  if (!tda->tda_enabled || !tda->tda_rootpath) return;

  /* Initiliase input mode */
  if(!tda->tda_open_service) {
    if(tda->tda_type == -1 || check_full_stream(tda)) {
      tvhlog(LOG_INFO, "dvb", "Adapter %s will run in full mux mode", tda->tda_rootpath);
      dvb_input_raw_setup(tda);
    } else {
      tvhlog(LOG_INFO, "dvb", "Adapter %s will run in filtered mode", tda->tda_rootpath);
      dvb_input_filtered_setup(tda);
    }
  }

  /* Enable mux scanner */
  gtimer_arm(&tda->tda_mux_scanner_timer, dvb_adapter_mux_scanner, tda, 1);
}

/**
 *
 */
void
dvb_adapter_start ( th_dvb_adapter_t *tda, int opt )
{
  if(tda->tda_enabled == 0) {
    tvhlog(LOG_INFO, "dvb", "Adapter \"%s\" cannot be started - it's disabled", tda->tda_displayname);
    return;
  }
  
  /* Default to ALL */
  if (!opt)
    opt = TDA_OPT_ALL;

  /* Open front end */
  if ((opt & TDA_OPT_FE) && (tda->tda_fe_fd == -1)) {
    tda->tda_fe_fd = tvh_open(tda->tda_fe_path, O_RDWR | O_NONBLOCK, 0);
    if (tda->tda_fe_fd == -1) return;
    tvhlog(LOG_DEBUG, "dvb", "%s opened frontend %s", tda->tda_rootpath, tda->tda_fe_path);
  }

  /* Start DVR thread */
  if ((opt & TDA_OPT_DVR) && (tda->tda_dvr_pipe.rd == -1)) {
    int err = tvh_pipe(O_NONBLOCK, &tda->tda_dvr_pipe);
    assert(err != -1);
    pthread_create(&tda->tda_dvr_thread, NULL, dvb_adapter_input_dvr, tda);
    tvhlog(LOG_DEBUG, "dvb", "%s started dvr thread", tda->tda_rootpath);
  }
}

void
dvb_adapter_stop ( th_dvb_adapter_t *tda, int opt )
{
  /* Poweroff */
  if (opt & TDA_OPT_PWR)
    dvb_adapter_poweroff(tda);

  /* Stop DVR thread */
  if ((opt & TDA_OPT_DVR) && (tda->tda_dvr_pipe.rd != -1)) {
    tvhlog(LOG_DEBUG, "dvb", "%s stopping thread", tda->tda_rootpath);
    int err = tvh_write(tda->tda_dvr_pipe.wr, "", 1);
    assert(!err);
    pthread_join(tda->tda_dvr_thread, NULL);
    close(tda->tda_dvr_pipe.rd);
    close(tda->tda_dvr_pipe.wr);
    tda->tda_dvr_pipe.rd = -1;
    tvhlog(LOG_DEBUG, "dvb", "%s stopped thread", tda->tda_rootpath);
  }

  dvb_adapter_notify(tda);

  /* Don't close FE */
  if (!tda->tda_idleclose && tda->tda_enabled) return;

  /* Close front end */
  if ((opt & TDA_OPT_FE) && (tda->tda_fe_fd != -1)) {
    tvhlog(LOG_DEBUG, "dvb", "%s closing frontend", tda->tda_rootpath);
    close(tda->tda_fe_fd);
    tda->tda_fe_fd = -1;
  }
}

/**
 *
 */
void
dvb_adapter_init(uint32_t adapter_mask, const char *rawfile)
{
  htsmsg_t *l, *c;
  htsmsg_field_t *f;
  const char *s;
  int i, type;
  uint32_t u32;
  th_dvb_adapter_t *tda;

  TAILQ_INIT(&dvb_adapters);

  /* Initialise hardware */
  for(i = 0; i < 32; i++) 
    if ((1 << i) & adapter_mask) 
      tda_add(i);

  /* Initialise rawts test file */
  if(rawfile)
    tda_add_from_file(rawfile);

  /* Load configuration */
  if ((l = hts_settings_load("dvbadapters")) != NULL) {
    HTSMSG_FOREACH(f, l) {
      if((c = htsmsg_get_map_by_field(f)) == NULL)
        continue;
      
      if((s = htsmsg_get_str(c, "type")) == NULL ||
         (type = dvb_str_to_adaptertype(s)) < 0)
        continue;

      // TODO: do we really want to do this? useful for debug?
      if((tda = dvb_adapter_find_by_identifier(f->hmf_name)) == NULL) {
        /* Not discovered by hardware, create it */
        tda                 = tda_alloc();
        tda->tda_identifier = strdup(f->hmf_name);
        tda->tda_type       = type;
        TAILQ_INSERT_TAIL(&dvb_adapters, tda, tda_global_link);
      } else {
        if(type != tda->tda_type)
          continue; /* Something is wrong, ignore */
      }

      tvh_str_update(&tda->tda_displayname, htsmsg_get_str(c, "displayname"));
      tvh_str_update(&tda->tda_dvr_path,    htsmsg_get_str(c, "dvr_path"));
      tvh_str_update(&tda->tda_demux_path,  htsmsg_get_str(c, "dmx_path"));

      if (htsmsg_get_u32(c, "enabled",             &tda->tda_enabled))
        tda->tda_enabled = 1;
      htsmsg_get_u32(c, "autodiscovery",       &tda->tda_autodiscovery);
      htsmsg_get_u32(c, "idlescan",            &tda->tda_idlescan);
      htsmsg_get_u32(c, "idleclose",           &tda->tda_idleclose);
      htsmsg_get_u32(c, "skip_checksubscr",    &tda->tda_skip_checksubscr);
      htsmsg_get_u32(c, "sidtochan",           &tda->tda_sidtochan);
      htsmsg_get_u32(c, "qmon",                &tda->tda_qmon);
      htsmsg_get_u32(c, "poweroff",            &tda->tda_poweroff);
      htsmsg_get_u32(c, "nitoid",              &tda->tda_nitoid);
      htsmsg_get_u32(c, "diseqc_version",      &tda->tda_diseqc_version);
      htsmsg_get_u32(c, "diseqc_repeats",      &tda->tda_diseqc_repeats);
      htsmsg_get_u32(c, "extrapriority",       &tda->tda_extrapriority);
      htsmsg_get_u32(c, "skip_initialscan",    &tda->tda_skip_initialscan);
      htsmsg_get_u32(c, "disable_pmt_monitor", &tda->tda_disable_pmt_monitor);
      if (htsmsg_get_u32(c, "grace_period",        &tda->tda_grace_period))
        tda->tda_grace_period = 10;
      if (htsmsg_get_s32(c, "full_mux_rx", &tda->tda_full_mux_rx))
        if (!htsmsg_get_u32(c, "disable_full_mux_rx", &u32) && u32)
          tda->tda_full_mux_rx = 0;
    }
    htsmsg_destroy(l);
  }

  /* Initialise devices */
  TAILQ_FOREACH(tda, &dvb_adapters, tda_global_link) {
    tda_init(tda);

    if(tda->tda_sat)
      dvb_satconf_init(tda);

    dvb_mux_load(tda);
  }
}


/**
 * If nobody is subscribing, cycle thru all muxes to get some stats
 * and EIT updates
 */
void
dvb_adapter_mux_scanner(void *aux)
{
  th_dvb_adapter_t *tda = aux;
  th_dvb_mux_instance_t *tdmi;
  int i;

  if(tda->tda_rootpath == NULL)
    return; // No hardware

  if(!tda->tda_enabled)
    return; // disabled

  // default period
  gtimer_arm(&tda->tda_mux_scanner_timer, dvb_adapter_mux_scanner, tda, 20);

  /* No muxes */
  if(LIST_FIRST(&tda->tda_muxes) == NULL) {
    dvb_adapter_poweroff(tda);
    return;
  }

  /* Someone is actively using */
  if(service_compute_weight(&tda->tda_transports) > 0)
    return;

  if(tda->tda_mux_current != NULL &&
     LIST_FIRST(&tda->tda_mux_current->tdmi_subscriptions) != NULL)
    return; // Someone is doing full mux dump

  /* Check if we have muxes pending for quickscan, if so, choose them */
  if((tdmi = TAILQ_FIRST(&tda->tda_initial_scan_queue)) != NULL) {
    dvb_fe_tune(tdmi, "Initial autoscan");
    return;
  }

  /* Check EPG */
  if (tda->tda_mux_epg) {
    epggrab_mux_stop(tda->tda_mux_epg, 1); // timeout anything not complete
    tda->tda_mux_epg = NULL; // skip this time
  } else {
    tda->tda_mux_epg = epggrab_mux_next(tda);
  }

  /* EPG */
  if (tda->tda_mux_epg) {
    int period = epggrab_mux_period(tda->tda_mux_epg);
    if (period > 20)
      gtimer_arm(&tda->tda_mux_scanner_timer,
                 dvb_adapter_mux_scanner, tda, period);
    dvb_fe_tune(tda->tda_mux_epg, "EPG scan");
    return;

  /* Normal */
  } else if (tda->tda_idlescan) {

    /* Alternate queue */
    for(i = 0; i < TDA_SCANQ_NUM; i++) {
      tda->tda_scan_selector++;
      if (tda->tda_scan_selector == TDA_SCANQ_NUM)
        tda->tda_scan_selector = 0;
      tdmi = TAILQ_FIRST(&tda->tda_scan_queues[tda->tda_scan_selector]);
      if (tdmi) {
        dvb_fe_tune(tdmi, "Autoscan");
        return;
      }
    }
  }

  /* Ensure we stop current mux and power off (if required) */
  if (tda->tda_mux_current)
    dvb_fe_stop(tda->tda_mux_current, 0);
}

/**
 * 
 */
void
dvb_adapter_clone(th_dvb_adapter_t *dst, th_dvb_adapter_t *src)
{
  th_dvb_mux_instance_t *tdmi_src, *tdmi_dst;

  lock_assert(&global_lock);

  while((tdmi_dst = LIST_FIRST(&dst->tda_muxes)) != NULL)
    dvb_mux_destroy(tdmi_dst);

  LIST_FOREACH(tdmi_src, &src->tda_muxes, tdmi_adapter_link)
    dvb_mux_copy(dst, tdmi_src, NULL);

  tda_save(dst);
}


/**
 * 
 */
int
dvb_adapter_destroy(th_dvb_adapter_t *tda)
{
  th_dvb_mux_instance_t *tdmi;

  if(tda->tda_rootpath != NULL)
    return -1;

  lock_assert(&global_lock);

  hts_settings_remove("dvbadapters/%s", tda->tda_identifier);

  while((tdmi = LIST_FIRST(&tda->tda_muxes)) != NULL)
    dvb_mux_destroy(tdmi);
  
  TAILQ_REMOVE(&dvb_adapters, tda, tda_global_link);

  free(tda->tda_identifier);
  free(tda->tda_displayname);
  free(tda->tda_fe_info);
  free((void*)tda->tda_rootpath); /* need cast because it's a const char* */
  free(tda->tda_fe_path);
  free(tda->tda_demux_path);
  free(tda->tda_dvr_path);

  free(tda);

  return 0;
}


/**
 *
 */
void
dvb_adapter_clean(th_dvb_adapter_t *tda)
{
  service_t *t;
  
  lock_assert(&global_lock);

  while((t = LIST_FIRST(&tda->tda_transports)) != NULL)
    /* Flush all subscribers */
    service_remove_subscriber(t, NULL, SM_CODE_SUBSCRIPTION_OVERRIDDEN);
}

/**
 * Install RAW PES filter
 */
static int
dvb_adapter_raw_filter(th_dvb_adapter_t *tda)
{
  int dmx = -1;
  struct dmx_pes_filter_params dmx_param;

  dmx = tvh_open(tda->tda_demux_path, O_RDWR, 0);
  if(dmx == -1) {
    tvhlog(LOG_ALERT, "dvb", "Unable to open %s -- %s",
           tda->tda_demux_path, strerror(errno));
    return -1;
  }

  memset(&dmx_param, 0, sizeof(dmx_param));
  dmx_param.pid      = 0x2000;
  dmx_param.input    = DMX_IN_FRONTEND;
  dmx_param.output   = DMX_OUT_TS_TAP;
  dmx_param.pes_type = DMX_PES_OTHER;
  dmx_param.flags    = DMX_IMMEDIATE_START;

  if(ioctl(dmx, DMX_SET_PES_FILTER, &dmx_param) == -1) {
    tvhlog(LOG_ERR, "dvb",
    "Unable to configure demuxer \"%s\" for all PIDs -- %s",
    tda->tda_demux_path, strerror(errno));
    close(dmx);
    return -1;
  }

  return dmx;
}

/**
 *
 */
static void *
dvb_adapter_input_dvr(void *aux)
{
  th_dvb_adapter_t *tda = aux;
  int fd = -1, i, r, c, efd, nfds, dmx = -1;
  uint8_t tsb[188 * 10];
  service_t *t;
  struct epoll_event ev;
  int delay = 10;

  /* Install RAW demux */
  if (tda->tda_rawmode) {
    if ((dmx = dvb_adapter_raw_filter(tda)) == -1) {
      tvhlog(LOG_ALERT, "dvb", "Unable to install raw mux filter");
      return NULL;
    }
  }

  /* Open DVR */
  if ((fd = tvh_open(tda->tda_dvr_path, O_RDONLY | O_NONBLOCK, 0)) == -1) {
    close(dmx);
    return NULL;
  }

  /* Create poll */
  efd = epoll_create(2);
  memset(&ev, 0, sizeof(ev));
  ev.events  = EPOLLIN;
  ev.data.fd = tda->tda_dvr_pipe.rd;
  epoll_ctl(efd, EPOLL_CTL_ADD, tda->tda_dvr_pipe.rd, &ev);
  ev.data.fd = fd;
  epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ev);

  r = i = 0;
  while(1) {

    /* Wait for input */
    nfds = epoll_wait(efd, &ev, 1, delay);

    /* No data */
    if (nfds < 1) continue;

    /* Exit */
    if (ev.data.fd != fd) break;

    /* Read data */
    c = read(fd, tsb+r, sizeof(tsb)-r);
    if (c < 0) {
      if (errno == EAGAIN || errno == EINTR)
        continue;
      else if (errno == EOVERFLOW) {
        tvhlog(LOG_WARNING, "dvb", "\"%s\" read() EOVERFLOW",
               tda->tda_identifier);
        continue;
      } else {
        // TODO: should we try to recover?
        tvhlog(LOG_ERR, "dvb", "\"%s\" read() error %d",
               tda->tda_identifier, errno);
        break;
      }
    }
    r += c;
    atomic_add(&tda->tda_bytes, c);

    /* not enough data */
    if (r < 188) continue;

    int wakeup_table_feed = 0;  // Just wanna wakeup once

    pthread_mutex_lock(&tda->tda_delivery_mutex);

    if(LIST_FIRST(&tda->tda_streaming_pad.sp_targets) != NULL) {
      streaming_message_t sm;
      pktbuf_t *pb = pktbuf_alloc(tsb, r);
      memset(&sm, 0, sizeof(sm));
      sm.sm_type = SMT_MPEGTS;
      sm.sm_data = pb;
      streaming_pad_deliver(&tda->tda_streaming_pad, &sm);
      pktbuf_ref_dec(pb);
    }

    /* Process */
    while (r >= 188) {
  
      /* sync */
      if (tsb[i] == 0x47) {
	      int pid = (tsb[i+1] & 0x1f) << 8 | tsb[i+2];

	      if(tda->tda_table_filter[pid]) {
	        if(!(tsb[i+1] & 0x80)) { // Only dispatch to table parser if not error
	          dvb_table_feed_t *dtf = malloc(sizeof(dvb_table_feed_t));
	          memcpy(dtf->dtf_tsb, tsb + i, 188);
	          TAILQ_INSERT_TAIL(&tda->tda_table_feed, dtf, dtf_link);
	          wakeup_table_feed = 1;
	        }
	      } else {
          LIST_FOREACH(t, &tda->tda_transports, s_active_link)
            if(t->s_dvb_mux_instance == tda->tda_mux_current)
              ts_recv_packet1(t, tsb + i, NULL);
        }

        i += 188;
        r -= 188;

      /* no sync */
      } else {
        tvhlog(LOG_DEBUG, "dvb", "\"%s\" ts sync lost", tda->tda_identifier);
        if (ts_resync(tsb, &r, &i)) break;
        tvhlog(LOG_DEBUG, "dvb", "\"%s\" ts sync found", tda->tda_identifier);
      }
    }

    if(wakeup_table_feed)
      pthread_cond_signal(&tda->tda_table_feed_cond);

    pthread_mutex_unlock(&tda->tda_delivery_mutex);

    /* reset buffer */
    if (r) memmove(tsb, tsb+i, r);
    i = 0;
  }

  if(dmx != -1)
    close(dmx);
  close(efd);
  close(fd);
  return NULL;
}


/**
 *
 */
htsmsg_t *
dvb_adapter_build_msg(th_dvb_adapter_t *tda)
{
  char buf[100];
  htsmsg_t *m = htsmsg_create_map();
  th_dvb_mux_instance_t *tdmi;
  service_t *t;
  int nummux = 0;
  int numsvc = 0;
  int fdiv;

  htsmsg_add_str(m, "identifier", tda->tda_identifier);
  htsmsg_add_str(m, "name", tda->tda_displayname);

  // XXX: bad bad bad slow slow slow
  LIST_FOREACH(tdmi, &tda->tda_muxes, tdmi_adapter_link) {
    nummux++;
    LIST_FOREACH(t, &tdmi->tdmi_transports, s_group_link) {
      numsvc++;
    }
  }

  htsmsg_add_str(m, "type", "dvb");

  htsmsg_add_u32(m, "services", numsvc);
  htsmsg_add_u32(m, "muxes", nummux);
  htsmsg_add_u32(m, "initialMuxes", tda->tda_initial_num_mux);

  if(tda->tda_mux_current != NULL) {
    th_dvb_mux_instance_t *tdmi = tda->tda_mux_current;

    dvb_mux_nicename(buf, sizeof(buf), tda->tda_mux_current);
    htsmsg_add_str(m, "currentMux", buf);

    htsmsg_add_u32(m, "signal", MIN(tdmi->tdmi_signal * 100 / 65535, 100));
    htsmsg_add_u32(m, "snr", tdmi->tdmi_snr);
    htsmsg_add_u32(m, "ber", tdmi->tdmi_ber);
    htsmsg_add_u32(m, "unc", tdmi->tdmi_unc);
    htsmsg_add_u32(m, "uncavg", tdmi->tdmi_unc_avg);
  } else {
    htsmsg_add_str(m, "currentMux", "");
    htsmsg_add_u32(m, "signal", 0);
    htsmsg_add_u32(m, "snr", 0);
    htsmsg_add_u32(m, "ber", 0);
    htsmsg_add_u32(m, "unc", 0);
    htsmsg_add_u32(m, "uncavg", 0);
  }

  if(tda->tda_rootpath == NULL)
    return m;

  htsmsg_add_str(m, "path", tda->tda_rootpath);
  htsmsg_add_str(m, "hostconnection", 
		 hostconnection2str(tda->tda_hostconnection));
  htsmsg_add_str(m, "devicename", tda->tda_fe_info->name);

  htsmsg_add_str(m, "deliverySystem", 
		 dvb_adaptertype_to_str(tda->tda_type) ?: "");

  htsmsg_add_u32(m, "satConf", tda->tda_sat);

  fdiv = tda->tda_type == FE_QPSK ? 1 : 1000;

  htsmsg_add_u32(m, "freqMin", tda->tda_fe_info->frequency_min / fdiv);
  htsmsg_add_u32(m, "freqMax", tda->tda_fe_info->frequency_max / fdiv);
  htsmsg_add_u32(m, "freqStep", tda->tda_fe_info->frequency_stepsize / fdiv);

  htsmsg_add_u32(m, "symrateMin", tda->tda_fe_info->symbol_rate_min);
  htsmsg_add_u32(m, "symrateMax", tda->tda_fe_info->symbol_rate_max);
  return m;
}


/**
 *
 */
void
dvb_adapter_notify(th_dvb_adapter_t *tda)
{
  notify_by_msg("tvAdapter", dvb_adapter_build_msg(tda));
}


/**
 *
 */
static void
fe_opts_add(htsmsg_t *a, const char *title, int value)
{
  htsmsg_t *v = htsmsg_create_map();

  htsmsg_add_str(v, "title", title);
  htsmsg_add_u32(v, "id", value);
  htsmsg_add_msg(a, NULL, v);
}

/**
 *
 */
htsmsg_t *
dvb_fe_opts(th_dvb_adapter_t *tda, const char *which)
{
  htsmsg_t *a = htsmsg_create_list();
  fe_caps_t c = tda->tda_fe_info->caps;

  if(!strcmp(which, "constellations")) {
    if(c & FE_CAN_QAM_AUTO)    fe_opts_add(a, "Auto", QAM_AUTO);
    if(c & FE_CAN_QPSK) {
      fe_opts_add(a, "QPSK",     QPSK);
#if DVB_API_VERSION >= 5
      fe_opts_add(a, "PSK_8",    PSK_8);
      fe_opts_add(a, "APSK_16",  APSK_16);
      fe_opts_add(a, "APSK_32",  APSK_32);
#endif
    }
    if(c & FE_CAN_QAM_16)      fe_opts_add(a, "QAM-16",   QAM_16);
    if(c & FE_CAN_QAM_32)      fe_opts_add(a, "QAM-32",   QAM_32);
    if(c & FE_CAN_QAM_64)      fe_opts_add(a, "QAM-64",   QAM_64);
    if(c & FE_CAN_QAM_128)     fe_opts_add(a, "QAM-128",  QAM_128);
    if(c & FE_CAN_QAM_256)     fe_opts_add(a, "QAM-256",  QAM_256);
    return a;
  }

  if(!strcmp(which, "delsys")) {
    if(c & FE_CAN_QPSK) {
#if DVB_API_VERSION >= 5
      fe_opts_add(a, "SYS_DVBS",     SYS_DVBS);
      fe_opts_add(a, "SYS_DVBS2",    SYS_DVBS2);
#else
      fe_opts_add(a, "SYS_DVBS",     -1);
#endif
    }
    return a;
  }

  if(!strcmp(which, "transmissionmodes")) {
    if(c & FE_CAN_TRANSMISSION_MODE_AUTO) 
      fe_opts_add(a, "Auto", TRANSMISSION_MODE_AUTO);

    fe_opts_add(a, "2k", TRANSMISSION_MODE_2K);
    fe_opts_add(a, "8k", TRANSMISSION_MODE_8K);
    return a;
  }

  if(!strcmp(which, "bandwidths")) {
    if(c & FE_CAN_BANDWIDTH_AUTO) 
      fe_opts_add(a, "Auto", BANDWIDTH_AUTO);

    fe_opts_add(a, "8 MHz", BANDWIDTH_8_MHZ);
    fe_opts_add(a, "7 MHz", BANDWIDTH_7_MHZ);
    fe_opts_add(a, "6 MHz", BANDWIDTH_6_MHZ);
    return a;
  }

  if(!strcmp(which, "guardintervals")) {
    if(c & FE_CAN_GUARD_INTERVAL_AUTO)
      fe_opts_add(a, "Auto", GUARD_INTERVAL_AUTO);

    fe_opts_add(a, "1/32", GUARD_INTERVAL_1_32);
    fe_opts_add(a, "1/16", GUARD_INTERVAL_1_16);
    fe_opts_add(a, "1/8",  GUARD_INTERVAL_1_8);
    fe_opts_add(a, "1/4",  GUARD_INTERVAL_1_4);
    return a;
  }

  if(!strcmp(which, "hierarchies")) {
    if(c & FE_CAN_HIERARCHY_AUTO)
      fe_opts_add(a, "Auto", HIERARCHY_AUTO);

    fe_opts_add(a, "None", HIERARCHY_NONE);
    fe_opts_add(a, "1", HIERARCHY_1);
    fe_opts_add(a, "2", HIERARCHY_2);
    fe_opts_add(a, "4", HIERARCHY_4);
    return a;
  }

  if(!strcmp(which, "fec")) {
    if(c & FE_CAN_FEC_AUTO)
      fe_opts_add(a, "Auto", FEC_AUTO);

    fe_opts_add(a, "None", FEC_NONE);

    fe_opts_add(a, "1/2", FEC_1_2);
    fe_opts_add(a, "2/3", FEC_2_3);
    fe_opts_add(a, "3/4", FEC_3_4);
    fe_opts_add(a, "4/5", FEC_4_5);
#if DVB_API_VERSION >= 5
    fe_opts_add(a, "3/5", FEC_3_5);
#endif
    fe_opts_add(a, "5/6", FEC_5_6);
    fe_opts_add(a, "6/7", FEC_6_7);
    fe_opts_add(a, "7/8", FEC_7_8);
    fe_opts_add(a, "8/9", FEC_8_9);
#if DVB_API_VERSION >= 5
    fe_opts_add(a, "9/10", FEC_9_10);
#endif
    return a;
  }


  if(!strcmp(which, "polarisations")) {
    fe_opts_add(a, "Horizontal",     POLARISATION_HORIZONTAL);
    fe_opts_add(a, "Vertical",       POLARISATION_VERTICAL);
    fe_opts_add(a, "Circular left",  POLARISATION_CIRCULAR_LEFT);
    fe_opts_add(a, "Circular right", POLARISATION_CIRCULAR_RIGHT);
    return a;
  }

  htsmsg_destroy(a);
  return NULL;
}

/**
 * Turn off the adapter
 */
void
dvb_adapter_poweroff(th_dvb_adapter_t *tda)
{
  if (tda->tda_fe_fd == -1) return;
  lock_assert(&global_lock);
  if (!tda->tda_poweroff || tda->tda_type != FE_QPSK)
    return;
  diseqc_voltage_off(tda->tda_fe_fd);
  tvhlog(LOG_DEBUG, "dvb", "\"%s\" is off", tda->tda_rootpath);
}
