package org.bucksch.zeipis.recorder;
import org.bucksch.zeipis.all.Misconfiguration;
import org.bucksch.zeipis.all.Util;
import java.util.Map;

public class DVBTunerNativeJNILinux implements DVBTuner
{
  public DVBTunerNativeJNILinux(DVBCard card)
    throws CantUseTVCard
  {
    myCard = card;
  }
  static {
    System.loadLibrary("zeipisdvbtune"); // TODO catch and recover when not loadable
  }
  public void tuneTo(DVBChannel c)
    throws CantUseTVCard, Misconfiguration
  {
    assert(myFrontendFD == -1 && myDemuxFDs == null); // Something earlier in this app forgot to close the frontend by calling done()

    if (c.encrypted)
    {
      //myCA = new DVBCANativeLinuxZap();
      myCA = new DVBCAShell();
      myCA.start(myCard, c);

      long waitStart = System.currentTimeMillis();
      while (waitStart + 6000 > System.currentTimeMillis())
        ;
    }

    tuneToTransponder(c.transponder());
    addPIDs(c, false);
    keepAddingPIDs(c);

    Util.debug("Tuning succeeded");
  }
  protected void tuneToTransponder(DVBTransponder c)
    throws CantUseTVCard, Misconfiguration
  {
    assert(myFrontendFD == -1 && myDemuxFDs == null);
    myDemuxFDs = Util.newMap();

    int delsys;
    if (c instanceof DVBS2Transponder) delsys = DVBParamsMulti.DELSYS_DVBS2;
    else if (c instanceof DVBSTransponder) delsys = DVBParamsMulti.DELSYS_DVBS;
    else if (c instanceof DVBCTransponder) delsys = DVBParamsMulti.DELSYS_DVBC_ANNEX_AC;
    else if (c instanceof DVBTTransponder) delsys = DVBParamsMulti.DELSYS_DVBT;
    else throw new Misconfiguration("Unknown channel type");

    // All
    int frequency = c.frequency;
    int fec = fec(c.fec); 

    int modulation;
    if (c instanceof DVBSTransponder) modulation = DVBParamsMulti.MOD_QPSK; // DVB-S1 is always QPSK
    else if (c.modulation.equals("8PSK")) modulation = DVBParamsMulti.MOD_PSK8;
    else if (c.modulation.equals("PSK8")) modulation = DVBParamsMulti.MOD_PSK8;
    else if (c.modulation.equals("APSK16")) modulation = DVBParamsMulti.MOD_APSK16;
    else if (c.modulation.equals("APSK32")) modulation = DVBParamsMulti.MOD_APSK32;
    else if (c.modulation.equals("QPSK")) modulation = DVBParamsMulti.MOD_QPSK;
    else if (c.modulation.equals("DQPSK")) modulation = DVBParamsMulti.MOD_DQPSK;
    else if (c.modulation.equals("8VSB")) modulation = DVBParamsMulti.MOD_VSB8;
    else if (c.modulation.equals("16VSB")) modulation = DVBParamsMulti.MOD_VSB16;
    else if (c.modulation.equals("QAM16")) modulation = DVBParamsMulti.MOD_QAM16;
    else if (c.modulation.equals("QAM32")) modulation = DVBParamsMulti.MOD_QAM32;
    else if (c.modulation.equals("QAM64")) modulation = DVBParamsMulti.MOD_QAM64;
    else if (c.modulation.equals("QAM128")) modulation = DVBParamsMulti.MOD_QAM128;
    else if (c.modulation.equals("QAM256")) modulation = DVBParamsMulti.MOD_QAM256;
    else if (c.modulation.equals("QAMAUTO")) modulation = DVBParamsMulti.MOD_QAMAUTO;
    else if (c.modulation.equals("AUTO"))
    {
      if (c instanceof DVBS2Transponder) modulation = DVBParamsMulti.MOD_QPSK;
      else if (c instanceof DVBSTransponder) modulation = DVBParamsMulti.MOD_QPSK;
      else if (c instanceof DVBCTransponder) modulation = DVBParamsMulti.MOD_QAMAUTO;
      else if (c instanceof DVBTTransponder) throw new Misconfiguration("Need explicit constellation (= modulation) for DVB-T");
      else throw new Misconfiguration("Don't recognize channel type");
    }
    else throw new Misconfiguration("modulation " + c.modulation + " not supported");

    // DVB-C covered by generic stuff above

    // DVB-S
    char polarity = 'V';
    boolean hiband = false;
    int satNo = 0;
    int rolloff = DVBParamsMulti.ROLLOFF_AUTO;
    if (c instanceof DVBSTransponder)
    {
      polarity = ((DVBSTransponder) c).polarity;
      rolloff = DVBParamsMulti.ROLLOFF_35; // 35 is implied for DVB-S1
      // LNB
      final int lnb_low_val = 9750000; // Universal LNB, TODO support others
      final int lnb_high_val = 10600000;
      final int lnb_switch_val = 11700000;
      if (frequency >= lnb_switch_val)
        hiband = true;
      if (hiband)
        frequency = frequency - lnb_high_val;
      else
      {
        if (frequency < lnb_low_val)
          frequency = lnb_low_val - frequency;
        else
          frequency = frequency - lnb_low_val;
      }
    }

    // DVB-S2
    int pilot = DVBParamsMulti.PILOT_AUTO;
    if (c instanceof DVBS2Transponder) // ?
    {
      DVBS2Transponder cs2 = (DVBS2Transponder) c;
      if (cs2.rolloff.equals("20")) rolloff = DVBParamsMulti.ROLLOFF_20;
      else if (cs2.rolloff.equals("25")) rolloff = DVBParamsMulti.ROLLOFF_25;
      else if (cs2.rolloff.equals("35")) rolloff = DVBParamsMulti.ROLLOFF_35;
      else if (cs2.rolloff.equals("AUTO")) rolloff = DVBParamsMulti.ROLLOFF_AUTO;
      else throw new Misconfiguration("rolloff " + cs2.rolloff + " not supported");
      if (cs2.pilot == '1') pilot = DVBParamsMulti.PILOT_ON;
      else if (cs2.pilot == '0') pilot = DVBParamsMulti.PILOT_OFF;
      else if (cs2.pilot == 'A') pilot = DVBParamsMulti.PILOT_AUTO;
      else throw new Misconfiguration("pilot " + cs2.pilot + " not supported");
    }

    // DVB-T
    int bandwidth = DVBParamsMulti.BANDWIDTH_AUTO;
    int guard_interval = DVBParamsMulti.GUARDINTERVAL_AUTO;
    int hp_code_rate = DVBParamsMulti.FEC_AUTO;
    int lp_code_rate = DVBParamsMulti.FEC_AUTO;
    int transmission_mode = DVBParamsMulti.TRANSMISSIONMODE_AUTO;
    int hierarchy = DVBParamsMulti.HIERARCHY_AUTO;
    int inversion = DVBParamsMulti.INVERSION_AUTO;
    if (c instanceof DVBTTransponder)
    {
      DVBTTransponder ct = (DVBTTransponder) c;
      hp_code_rate = fec(ct.hp_code_rate);
      lp_code_rate = fec(ct.lp_code_rate);
      if (ct.bandwidth == '8') bandwidth = DVBParamsMulti.BANDWIDTH_8MHZ;
      else if (ct.bandwidth == '7') bandwidth = DVBParamsMulti.BANDWIDTH_7MHZ;
      else if (ct.bandwidth == '6') bandwidth = DVBParamsMulti.BANDWIDTH_6MHZ;
      else if (ct.bandwidth == 'A') bandwidth = DVBParamsMulti.BANDWIDTH_AUTO;
      else throw new Misconfiguration("bandwidth " + ct.bandwidth + " not supported");
      if (ct.guard_interval.equals("1/4")) guard_interval = DVBParamsMulti.GUARDINTERVAL_1_4;
      else if (ct.guard_interval.equals("1/8")) guard_interval = DVBParamsMulti.GUARDINTERVAL_1_8;
      else if (ct.guard_interval.equals("1/16")) guard_interval = DVBParamsMulti.GUARDINTERVAL_1_16;
      else if (ct.guard_interval.equals("1/32")) guard_interval = DVBParamsMulti.GUARDINTERVAL_1_32;
      else if (ct.guard_interval.equals("AUTO")) guard_interval = DVBParamsMulti.GUARDINTERVAL_AUTO;
      else throw new Misconfiguration("guard interval " + ct.guard_interval + " not supported");
      if (ct.transmission_mode == '2') transmission_mode = DVBParamsMulti.TRANSMISSIONMODE_2K;
      else if (ct.transmission_mode == '8') transmission_mode = DVBParamsMulti.TRANSMISSIONMODE_8K;
      else if (ct.transmission_mode == 'A') transmission_mode = DVBParamsMulti.TRANSMISSIONMODE_AUTO;
      else throw new Misconfiguration("transmission mode " + ct.transmission_mode + " not supported");
      if (ct.hierarchy == '1') hierarchy = DVBParamsMulti.HIERARCHY_1;
      else if (ct.hierarchy == '2') hierarchy = DVBParamsMulti.HIERARCHY_2;
      else if (ct.hierarchy == '4') hierarchy = DVBParamsMulti.HIERARCHY_4;
      else if (ct.hierarchy == 'N') hierarchy = DVBParamsMulti.HIERARCHY_NONE;
      else if (ct.hierarchy == 'A') hierarchy = DVBParamsMulti.HIERARCHY_AUTO;
      else throw new Misconfiguration("hierarchy " + ct.hierarchy + " not supported");
      if (ct.inversion == '1') inversion = DVBParamsMulti.INVERSION_ON;
      else if (ct.inversion == '0') inversion = DVBParamsMulti.INVERSION_OFF;
      else if (ct.inversion == 'A') inversion = DVBParamsMulti.INVERSION_AUTO;
      else throw new Misconfiguration("inversion " + ct.inversion + " not supported");
    }

    // Tune
    long start = System.currentTimeMillis();
    Util.debug("Tuning to " + c.frequency + " KHz (" + frequency + " KHz considering LNB), LNB hiband " + hiband
            + ", pol " + polarity + ", symbolrate " + c.symbolrate
            + ", modulation " + c.modulation + ", fec inner " + c.fec
            + ", using " + myCard.frontendDeviceFilename() + " in mode " + delsys);

    myFrontendFD = tuneToNative(myCard.frontendDeviceFilename(), delsys,
            frequency, c.symbolrate, modulation,
            fec, rolloff, pilot,
            bandwidth, transmission_mode, hp_code_rate, lp_code_rate, guard_interval, hierarchy, inversion,
            polarity == 'V', hiband, satNo);
    
    if (myFrontendFD == -1)
      throw new CantUseTVCard("Could not open frontend");

    Util.debug("Physical tuning to transponder took " + (System.currentTimeMillis() - start) + "ms");
  }
  /**
   * Given that a DVB TS / multiplex consists of several (typically 4-10) channels,
   * you need to find the right streams (video, 1+ audio, CA data) to record.
   * These streams (ES, elementary streams) are identified by IDs called PID.
   *
   * 1. The TS itself tells what these are, in some metadata streams, but we need to
   * read them out of that.
   * 2. Alternatively, we can read them out of the database. They could be entered there
   * manually by the user (the PIDs are relatively stable) or by the channel scan.
   * 3. Or, if we tuned to this channel before in this server instance, we can just reuse
   * the PIDs from the last time.
   * 
   * The problem with re-using and channel scan is that the number of streams change
   * depending on the material sent: Only some movies/series have an AC3 stream,
   * and only some are broadcasted with the original language audio track.
   * So, when we tuned to the channel the last time, or during the channel scan,
   * these streams might not have been there are thus were not found (the metadata
   * does not contain them in this case), but they are here now, and we'll want to
   * record them now.
   * Therefore, while any method will work for video and one audio track, the only
   * safe way to get all streams and data is to always read the metadata before each
   * recording.
   * 
   * In fact, not even that might suffice, in case the recording starts
   * shortly *before* the movie, and the previous program does not have the
   * extra streams. That's why we need to keep scanning the PMT. :-(
   * TODO implement that.
   * 
   * @param c
   * @param quiet Don't make debug output unless a PID is added.
   * @throws CantUseTVCard
   * @throws Misconfiguration
   */
  protected void addPIDs(DVBChannel c, boolean quiet)
    throws CantUseTVCard, Misconfiguration
  {
    try {
      // DB
      if (c.pids.isEmpty())
        c.getPIDsFromDB();

      DVBScan scan = new DVBScan(myCard);

      // Record PAT
      c.pids.add(0);

      // Search PAT for PMT PID
      if (c.pmtPID < 0)
      {
        long start = System.currentTimeMillis();
        if ( ! quiet) Util.debug("Searching PAT for PMT PID");
        //int pmtPID = findPMTPIDNative(myCard.demuxDeviceFilename(), c.serviceID);
        c.pmtPID = scan.findPMTPID(c);
        c.pids.add(c.pmtPID);
        if ( ! quiet) Util.debug("Found PMT PID " + c.pmtPID + " within " + (System.currentTimeMillis() - start)+ "ms");
      }

      // Search PMT for ES (video and audio) PIDs
      if ( ! quiet) Util.debug("Searching PMT for ES (video and audio) PIDs");
      long start2 = System.currentTimeMillis();
      c.pids.addAll(scan.scanForPIDs(c.serviceID, c, false));
      if ( ! quiet) Util.debug("Found ES PIDs within " + (System.currentTimeMillis() - start2)+ "ms");
    } catch (Exception e) {
      done(); // TODO should do that in caller (in tuneTo(), but not in keepScanningPIDs()), but don't know how.
      throw new CantUseTVCard("Can't find PIDs", e);
    }
    try {
      //if ( ! quiet) Util.debug("Adding PID filters, using " + myCard.demuxDeviceFilename());
      for (int pid : c.pids)
        addPID(pid);
    } catch (CantUseTVCard e) {
      done();
      throw new CantUseTVCard("Can't add PID filters", e);
    }
  }
  /**
   * Continuously searches PMT for new ES PIDs,
   * @see description of addPIDs()
   * 
   * Immediately returns, but starts a new thread.
   * 
   * @param c
   */
  protected void keepAddingPIDs(final DVBChannel c)
  {
    // How often to scan the PMT
    final int kIntervalDuringStart = 1000; // in ms. 1s
    final int kIntervalLater = 10000; // in ms. 10s
    final int kDurationStart = 120; // in s. How long after rec start kIntervalDuringStart applies. Afterwards, kIntervalLater applies.

    myStopPMTScan = false;
    final String name = "Scanning PMT for recording on card " + myCard.name();
    assert(myPMTThread == null);
    myPMTThread = new Thread(name)
    {
      @Override
      public void run()
      {
        try {
          long recordingStart = System.currentTimeMillis();
          while ( ! myStopPMTScan &&
                  recordingStart - System.currentTimeMillis() < kDurationStart * 1000)
          {
            if (myDemuxFDs == null) // tuner was shut down while we were sleeping
              return;
            addPIDs(c, true);
            Thread.sleep(kIntervalDuringStart);
          }
          while ( ! myStopPMTScan)
          {
            if (myDemuxFDs == null)
              return;
            addPIDs(c, true);
            Thread.sleep(kIntervalLater);
          }
          Util.debug("Stopped PMT scan normally");
        }
        catch (InterruptedException e) { Util.debug("Stopped PMT scan"); } // when sleep() is interrupt()ed by done()
         catch (Exception e) { Util.error("Error in " + name, e); }
      }
    };
    myPMTThread.start();
  }
  public void addPID(int pid)
    throws CantUseTVCard
  {
    if (myDemuxFDs == null)
      return;
    synchronized (myDemuxFDs)
    {
      if ( ! myDemuxFDs.containsKey(pid))
      {
        Util.debug("Adding ES PID " + pid);
        myDemuxFDs.put(pid, openDemuxNative(myCard.demuxDeviceFilename(), pid));      
      }
    }
  }
  protected int fec(String fec)
    throws Misconfiguration
  {
    if (fec.equals("1/2")) return DVBParamsMulti.FEC_1_2;
    else if (fec.equals("2/3")) return DVBParamsMulti.FEC_2_3;
    else if (fec.equals("3/4")) return DVBParamsMulti.FEC_3_4;
    else if (fec.equals("4/5")) return DVBParamsMulti.FEC_4_5;
    else if (fec.equals("5/6")) return DVBParamsMulti.FEC_5_6;
    else if (fec.equals("6/7")) return DVBParamsMulti.FEC_6_7;
    else if (fec.equals("7/8")) return DVBParamsMulti.FEC_7_8;
    else if (fec.equals("9/10")) return DVBParamsMulti.FEC_9_10;
    else if (fec.equals("3/5")) return DVBParamsMulti.FEC_3_5;
    else if (fec.equals("AUTO")) return DVBParamsMulti.FEC_AUTO;
    else if (fec.equals("NONE")) return DVBParamsMulti.FEC_NONE;
    else throw new Misconfiguration("FEC " + fec + " not supported");
  }

  /***
   * Tunes using the frontend (control) device, and waits for tuning lock
   * 
   * You must hold on the FD during the record, and close it afterwards
   * 
   * @param deliverySystem @see dvbfe_delsys
   * @param modulation @see dvbfe_modulation
   * @return frontendFD
   * @throws CantUseTVCard
   */
  protected native int tuneToNative(String frontendFilename, int deliverySystem,
          int frequency, int symbolrate, int modulation,
          int fec, int rolloff, int pilot,
          int bandwidth, int transmission_mode, int hp_code_rate, int lp_code_rate,
          int guard_interval, int hierarchy, int inversion,
          boolean polv, boolean hiband, int satNo)
    throws CantUseTVCard;

  /***
   * Sets a PID filter using the demux device
   * 
   * You must hold on the FD during the record, and close it afterwards
   * 
   * @return demuxFD
   * @throws CantUseTVCard
   */
  protected native int openDemuxNative(String demuxFilename, int pid)
    throws CantUseTVCard;

  /**
   * Find the PMT (Program Map Table) PID from the PAT (Program Association table) stream.
   * Needed to record HDTV streams.
   * 
   * Blocks (reads in a loop until a PAT appears in the stream) 
   * 
   * @param demuxFD result of openDemuxNative(..., pid = 0);
   * @param serviceID - ID of this program/channel 
   * @return the PMT PID
   */
  protected native int findPMTPIDNative(String demuxFilename, int serviceID)
    throws CantUseTVCard;

  /***
   * Sets a section PID filter using the demux device.
   * Section filters are different from program stream filters.
   * You read the latter via dvr, but the former by read()ing the demux device.
   * 
   * You need to:
   * 1. Open the demux device using this function
   * 2. Read the device using readSection()
   * 3. Close the demux device using closeNative()
   * 
   * @ return fd to pass to readSection()
   * @throws CantUseTVCard
   */
  protected native int openSectionFilterNative(String demuxFilename, int sectionPID, int tableID, int tableIDExt)
    throws CantUseTVCard;

  /***
   * @see openSectionFilterNative
   * 
   * @return bytes read
   * @throws CantUseTVCard
   */
  protected native int readSectionNative(int fd, byte[] buffer, int length)
    throws CantUseTVCard;

  public void done()
    throws CantUseTVCard
  {
    //Util.debug("tuner done called");
    assert(myFrontendFD != -1 && myDemuxFDs != null);

    myStopPMTScan = true;
    if (myPMTThread != null)
      myPMTThread.interrupt();
    closeNative(myFrontendFD);

    synchronized (myDemuxFDs)
    {
      for (int demuxFD : myDemuxFDs.values())
        closeNative(demuxFD);
      myDemuxFDs = null;
    }

    myFrontendFD = -1;

    if (myCA != null)
    {
      myCA.stop();
      myCA = null;
    }
  }

  /***
   * Closes myFrontendFD
   */
  protected native void closeNative(int fd)
    throws CantUseTVCard;


  // Data
  protected DVBCard myCard;
  protected int myFrontendFD = -1;
  protected Map<Integer, Integer> myDemuxFDs = null; // PID -> FD (demux open file descriptor)
  protected DVBCA myCA = null;
  protected boolean myStopPMTScan = true;
  protected Thread myPMTThread = null;
}


/**
 * From frontend.h of DVB API version 5.0 (S2API) 
 */
class DVBParamsMulti
{
  // fe_delivery_system
  static int DELSYS_UNDEFINED = 0;
  static int DELSYS_DVBC_ANNEX_AC = 1; // Cable-TV. Used in Europe.
  static int DELSYS_DVBC_ANNEX_B = 2; // USA: Digital Cable-TV. Can't decode anyways due to encryption etc..
  static int DELSYS_DVBT = 3; // Terrestrial = airwaves. Good coverage in Europe (UK, Germany, France). 
  static int DELSYS_DSS = 4;
  static int DELSYS_DVBS = 5; // satellite. Very good coverage in Europe.
  static int DELSYS_DVBS2 = 6; // satellite, version 2. Usually for HDTV.
  static int DELSYS_DVBH = 7; // H = "handy" = cellphone. Europe. Barely used.
  static int DELSYS_ISDBT = 8; // Japan and Brazil?, terrestrial
  static int DELSYS_ISDBS = 9; // dito, sat
  static int DELSYS_ISDBC = 10; // dito, cable
  static int DELSYS_ATSC = 11; // USA, terrestrial, HDTV
  static int DELSYS_ATSCMH = 12;
  static int DELSYS_DMBTH = 13;
  static int DELSYS_CMMB = 14;
  static int DELSYS_DAB = 15; // Radio, Europe/Germany, (pretty) dead
  // fe_modulation
  static int MOD_QPSK      = 0;
  static int MOD_QAM16   = 1;
  static int MOD_QAM32   = 2;
  static int MOD_QAM64   = 3;
  static int MOD_QAM128  = 4;
  static int MOD_QAM256  = 5;
  static int MOD_QAMAUTO   = 6;
  static int MOD_VSB8      = 7;
  static int MOD_VSB16     = 8;
  static int MOD_PSK8      = 9;
  static int MOD_APSK16    = 10;
  static int MOD_APSK32    = 11;
  static int MOD_DQPSK     = 12;
  // fe_code_rate
  static int FEC_NONE      = 0;
  static int FEC_1_2       = 1;
  static int FEC_2_3       = 2;
  static int FEC_3_4       = 3;
  static int FEC_4_5       = 4;
  static int FEC_5_6       = 5;
  static int FEC_6_7       = 6;
  static int FEC_7_8       = 7;
  static int FEC_8_9       = 8;
  static int FEC_AUTO      = 9;
  static int FEC_3_5       = 10;
  static int FEC_9_10      = 11;
  // fe_spectral_inversion
  static int INVERSION_OFF   = 0;
  static int INVERSION_ON    = 1;
  static int INVERSION_AUTO  = 2;
  // fe_transmit_mode
  static int TRANSMISSIONMODE_2K   = 0;
  static int TRANSMISSIONMODE_8K   = 1;
  static int TRANSMISSIONMODE_AUTO = 2;
  // fe_bandwidth
  static int BANDWIDTH_8MHZ   = 0;
  static int BANDWIDTH_7MHZ   = 1;
  static int BANDWIDTH_6MHZ   = 2;
  static int BANDWIDTH_AUTO   = 3;
  // fe_guard_interval
  static int GUARDINTERVAL_1_32 = 0;
  static int GUARDINTERVAL_1_16 = 1;
  static int GUARDINTERVAL_1_8 = 2;
  static int GUARDINTERVAL_1_4 = 3;
  static int GUARDINTERVAL_AUTO = 4;
  // fe_hierarchy
  static int HIERARCHY_NONE = 0;
  static int HIERARCHY_1 = 1;
  static int HIERARCHY_2 = 2;
  static int HIERARCHY_4 = 3;
  static int HIERARCHY_AUTO = 4;
  // fe_rolloff
  static int ROLLOFF_35   = 0; // implied for DVB-S, default for DVB-S2
  static int ROLLOFF_20   = 1;
  static int ROLLOFF_25   = 2; 
  static int ROLLOFF_AUTO = 3;
  // fe_pilot
  static int PILOT_ON = 0;
  static int PILOT_OFF = 1;
  static int PILOT_AUTO = 2;
}
