package org.bucksch.zeipis.recorder;
import org.bucksch.zeipis.all.*;
import org.bucksch.zeipis.recorder.DVBChannel.ChannelContentType;
import java.sql.SQLException;
import java.util.*;

public class DVBScan extends DVBTunerNativeJNILinux
{
  public DVBScan(DVBCard card)
    throws CantUseTVCard
  {
    super(card);
  }


  ////////////////////////////////////////////////////////////////
  //Data structures
  ////////////////////////////////////////////////////////////////

  static public class Transponder
  {
    public Transponder()
    {}

    public int dbID = -1;
    public DVBTransponder tuning = null;
    public Vector<DVBTransponder> tuningAlternatives = Util.newVector(); // same frequency, but other tuning params differ
    public Vector<DVBChannel> channels = Util.newVector(); // found channels. does not contain the tuning params, just serviceID etc..
    public int nitPID = -1; // PID for NIT. Set by parsePAT(), needed before parseNIT().
    public int networkID = -1;
    public String networkName = null;
    public int originalNetworkID = -1;
    public int transponderID = -1;
    public boolean haveTuned = false;
    public boolean haveScaned = false;
  }


  /**
   * This is to be implemented by the caller of DVBScan.scanEverything().
   * TODO avoid callbacks
   */
  public interface ScanProgressCallback
  {
    /**
     * Called when a transponder has been finished scanning.
     * I.e. all the information in |t| is filled in as much as possible.
     * @param t   The newly scanned transponder
     */
    public void scannedTransponder(Transponder t);
    /**
     * Up
     * @param doneCount   Number of transponders already scanned
     * @param todoCount   Number of transponders known but not scanned yet
     * @param todoEstimateCount   Number of transponders estimated to exist which are not scanned yet.
     *    Different from todoCount, because we may not know about all transponders yet.
     * @param timeElapsed  (ms)   Time between start of scan and now
     * @param timeUntilETA   (ms)  Time between now and estimated finish of scan
     */
    public void progress(int doneCount, int todoCount, int todoEstimateCount,
            int timeElapsed, int timeUntilETA);
  }

  /**
   * Try to find all channels receivable by this card.
   * This starts with all transponders configured in the database,
   * tunes to each, and goes from there, by reading NITs
   * (information in DVB stream about other frequencies / transponders),
   * and scanning these too.
   *
   * This can take a long time (up to 1 hour!), so ScanProgressCallback
   * gives you a way to observe the progress and do something
   * with the already found transponders.
   * This is advisable, as it's possible that the process is aborted by
   * the users or other invents, and the DB-configured transponders
   * will be scanned first.
   *
   * @param findNewTransponders   If false, only scan the transponders
   *    configured in the database and return all channels on them.
   *    If true, do a full network (e.g. satellite) scan, trying to find
   *    *all* transponders (frequencies) that this card can receive.
   * @param progressCB
   * @return all transponders found.
   *   Some of them may have failed tuning, so check haveTuned
   *   and haveScanned.
   *   This implies that this contains more transponders than
   *   ScanProgressCallback.scannedTransponder() has been called for,
   *   but you're probably not terribly interested in the additional ones,
   *   so you may well ignore this.
   */
  public Vector<Transponder> scanEverything(boolean findNewTransponders, ScanProgressCallback progressCB)
    throws Misconfiguration, SQLException, CantUseTVCard
  {
    Util.debug("scan all channels" + (findNewTransponders ? ", and try to find new ones" : " (only known ones)"));
    Vector<DVBTransponder> startTransponders = Util.newVector();
    for (DVBTransponder t : CoreData.me.myDVBTransponders.values())
    {
      if (myCard.canRecord(t))
        startTransponders.add(t);
    }
    return scanForTranspondersOnAllNITs(startTransponders, findNewTransponders, progressCB); 
  }

  public Vector<Transponder> scanForTranspondersOnAllNITs(Vector<DVBTransponder> startTransponders,
          boolean findNewTransponders,
          ScanProgressCallback progressCB)
    throws Misconfiguration, CantUseTVCard
  {
    Map<Integer, Transponder> transponders = Util.newMap();
    Vector<Transponder> newTransponders = Util.newVector();
    for (DVBTransponder t : startTransponders)
    {
      if (transponders.containsKey(t.frequency))
        continue;
      Transponder dsT = new Transponder();
      dsT.tuning = t;
      newTransponders.add(dsT);
      transponders.put(t.frequency, dsT);
    }

    Vector<Transponder> allKnownTranspondersForStatus = Util.newVector();
    final long startTimeScan = System.currentTimeMillis();

    while ( ! newTransponders.isEmpty())
    {
      Vector<Transponder> scanningTransponders = Util.newVector(); // avoid concurrent modification
      scanningTransponders.addAll(newTransponders);
      allKnownTranspondersForStatus.addAll(newTransponders);
      newTransponders = Util.newVector();
      for (Transponder dsTransponder : scanningTransponders)
      {
        // Status display
        int timeElapsed = (int) (System.currentTimeMillis() - startTimeScan);
        eta(allKnownTranspondersForStatus, timeElapsed, progressCB);

        try {
          Util.debug("scan transponder with freq " + dsTransponder.tuning.frequency);
          tuneToTransponder(dsTransponder.tuning);
          Util.debug("tuned");
          dsTransponder.haveTuned = true;
        } catch (Exception e) {
          dsTransponder.haveTuned = true;
          Util.error("Tuning failed, removing transponder from result", e);
          // Remove transponders we can't tune
          transponders.remove(dsTransponder.tuning.frequency);
          continue;
        }
        try {
          Util.debug("scaning transponder");
          scanTunedTransponder(dsTransponder);
          dsTransponder.haveScaned = true;

          if (findNewTransponders)
          {
            Util.debug("scaning NIT");
            Vector<Transponder> transpondersInNIT = scanNIT(dsTransponder);
            Util.debug("Found " + transpondersInNIT.size() + " transponders in NIT");
            for (Transponder candidateTransponder : transpondersInNIT)
            {
              int freq = candidateTransponder.tuning.frequency;
              if ( ! transponders.containsKey(freq))
              {
                newTransponders.add(candidateTransponder);
                transponders.put(freq, candidateTransponder);
                Util.debug("New transponder with freq " + freq + " found");              
              }
              else
              {
                Transponder existing = transponders.get(freq);
                // TODO check whether all tuning params are identical. If so, skip
                existing.tuningAlternatives.add(candidateTransponder.tuning);
                // TODO use the alternatives and compare the signal quality
              }
            }
          }
        } catch (Exception e) {
          Util.error("Scan of transponder with freq " + dsTransponder.tuning.frequency + " failed", e);
        } finally {
          done();
        }
        try {
          if (progressCB != null)
            progressCB.scannedTransponder(dsTransponder);
        } catch (Exception e) { Util.error("Error in callback", e); }
      }
    }
    return new Vector<Transponder>(transponders.values());
  }

  static public int eta(Vector<Transponder> allKnownTransponders, int timeElapsed, ScanProgressCallback progressCB)
  {
    int doneCount = 0;
    int todoCount = 0;
    for (Transponder t : allKnownTransponders)
    {
      //Util.debug("Status: Transp freq " + t.tuning.frequency + (t.haveTuned ? " DONE" : " not yet"));
      if (t.haveTuned)
        doneCount++;
      else
        todoCount++;
    }
    //if (doneCount < 5)
    //  todoCount *= 2; // estimate, because we don't know many NITs and therefore not many transponders yet
    int allCount = doneCount + todoCount;
    int timePerTransponder = 3000; // in ms.
    if (doneCount > 0) // don't / by 0
      timePerTransponder = timeElapsed / doneCount;
    int eta = todoCount * timePerTransponder;
    //Date etaRealtime = new Date(new Date().getTime() + eta);
    Util.debug("Status: " + doneCount + "/" + allCount + " transponders scanned. ETA "
            //+ new SimpleDateFormat().format(etaRealtime) + " = "
            + (eta / 1000 / 60) + " min " + (eta / 1000 % 60) + " s");
    if (progressCB != null)
      progressCB.progress(doneCount, todoCount, todoCount, timeElapsed, eta);
    return eta;
  }

  public Transponder scanTransponder(DVBTransponder origTransponder)
    throws CantUseTVCard, Misconfiguration
  {
    Util.debug("scan transponder of " + origTransponder);
    tuneToTransponder(origTransponder);
    try {
      Transponder dsTransponder = new Transponder();
      dsTransponder.tuning = origTransponder;
      scanTunedTransponder(dsTransponder);
      return dsTransponder;
    } finally {
      done();
    }
  }

  /**
   * Scan PAT and SDT and fill in data about transponder and channel.
   * Transponder needs to be already tuned and kept open during this function.
   * @param dsTransponder  ALTERs dsTransponder with found data
   */
  protected void scanTunedTransponder(Transponder dsTransponder)
    throws CantUseTVCard, Misconfiguration
  {
    int fd = openSectionFilterNative(myCard.demuxDeviceFilename(), PIDPAT, TablePAT, -1);
    Util.debug("Searching for PAT");
    try {
      int maxLoops = 3;
      boolean haveFullPAT = false;
      boolean startedPAT = false;
      while ( ! haveFullPAT && maxLoops-- > 0)
      {
        try {
          Section section = readSection(fd, TablePAT);
          parsePAT(section.data, section.subjectID, dsTransponder);

          if (section.sectionNumber == 0)
            startedPAT = true;
          if (startedPAT && section.sectionNumber == section.lastSectionNumber)
            haveFullPAT = true;
        } catch (Exception e) { Util.error("Read error", e); }
      }
    } finally {
      closeNative(fd);
    }

    fd = openSectionFilterNative(myCard.demuxDeviceFilename(), PIDSDT, TableSDT, -1);
    Util.debug("Searching for SDT");
    try {
      int maxLoops = 3;
      boolean haveFullSDT = false;
      boolean startedSDT = false;
      while ( ! haveFullSDT && maxLoops-- > 0)
      {
        try {
          Section section = readSection(fd, TableSDT);
          parseSDT(section.data, section.subjectID, dsTransponder);

          if (section.sectionNumber == 0)
            startedSDT = true;
          if (startedSDT && section.sectionNumber == section.lastSectionNumber)
            haveFullSDT = true;
        } catch (Exception e) { Util.error("Read error", e); }
      }
    } finally {
      closeNative(fd);
    }

    for (final DVBChannel dsChannel : dsTransponder.channels)
      scanForPIDs(dsChannel.serviceID, dsChannel, true);
  }

  /**
   * Find the PID for the PMT. This is a pre-condition for scanForPIDs.
   * 
   * Channel needs to be already tuned to transponder.
   * 
   * @param channel to find the PID for. Need only the serviceID from it.
   *    It will *not* write the found PMT PID into c.pmtPID.
   * @return the PMT PID for this channel.
   * @throws RuntimeException, when the PAT or PMT PID can't be found // TODO proper exception
   */
  public int findPMTPID(DVBChannel channel)
    throws CantUseTVCard, Misconfiguration
  {
    int fd = openSectionFilterNative(myCard.demuxDeviceFilename(), PIDPAT, TablePAT, -1);
    //Util.debug("Searching for PAT");
    Transponder dsTransponder = new Transponder();
    dsTransponder.tuning = channel.transponder();
    try {
      int maxLoops = 3;
      boolean haveFullPAT = false;
      boolean startedPAT = false;
      while ( ! haveFullPAT && maxLoops-- > 0)
      {
        try {
          Section section = readSection(fd, TablePAT);
          parsePAT(section.data, section.subjectID, dsTransponder);

          if (section.sectionNumber == 0)
            startedPAT = true;
          if (startedPAT && section.sectionNumber == section.lastSectionNumber)
            haveFullPAT = true;
        } catch (Exception e) { Util.error("Read error", e); }
      }
      if ( ! haveFullPAT)
        throw new RuntimeException("PAT not found in DVB stream");
    } finally {
      closeNative(fd);
    }

    int result = -1;
    for (DVBChannel c : dsTransponder.channels)
      if (c.serviceID == channel.serviceID)
        result = c.pmtPID;    
    if (result < 0)
      throw new RuntimeException("PMT not found in PAT in DVB stream");
    return result;
  }

  /**
   * Find the PIDs for the ES (elementary streams) for the channel.
   * This is a pre-condition to record the channel.
   * 
   * Channel needs to be already tuned to transponder.
   * 
   * @param channel to find the PID for. Need only the serviceID and pmtPID from it.
   * @param modify If true, will write the found PIDs into c.pids, and set several other attributes
   *    like dvbServiceName.
   * @return the PIDs for this channel.
   * @throws RuntimeException, when the PAT or PMT PID can't be found // TODO proper exception
   */
  public Set<Integer> scanForPIDs(int serviceID, DVBChannel dsChannel, boolean modify)
    throws CantUseTVCard, Misconfiguration
  {
    assert (serviceID == dsChannel.serviceID);
    //int pid = findPMTPIDNative(myCard.demuxDeviceFilename(), serviceID);
    if (serviceID < 0)
      throw new Misconfiguration("serviceID missing from channel");
    if (dsChannel.pmtPID < 0)
      throw new Misconfiguration("PMT PID missing from channel");
    if ( ! modify) // make a dummy copy for our own run, so that we don't modify the original app object
    {
      DVBChannel sw = new DVBChannel();
      sw.serviceID = dsChannel.serviceID;
      sw.pmtPID = dsChannel.pmtPID;
      sw.pids.addAll(dsChannel.pids);
      dsChannel = sw;
    }

    int fd = openSectionFilterNative(myCard.demuxDeviceFilename(), dsChannel.pmtPID, TablePMT, serviceID);
    //Util.debug("Searching for section PID " + dsChannel.pmtPID + ", tableID " + TablePMT + ", tableIDExt " + serviceID);
    try {
      int maxLoops = 3;
      boolean havePMT = false;
      while ( ! havePMT && maxLoops-- > 0)
      {
        try {
          Section section = readSection(fd, TablePMT);
          parsePMT(section.data, section.subjectID, dsChannel);
          havePMT = true; // PMT doesn't use section_number (it's always 0 per spec)
        } catch (Exception e) { Util.error("Read error", e); }
      }

      if ( ! havePMT)
        throw new RuntimeException("Can't read PMT in DVB stream");
      if (dsChannel.pids == null || dsChannel.pids.isEmpty())
        throw new RuntimeException("No PIDs found in PMT");
      return Collections.unmodifiableSet(dsChannel.pids);
    } finally {
      closeNative(fd);
    }
  }

  protected Vector<Transponder> scanNIT(Transponder startTransponder)
    throws CantUseTVCard, Misconfiguration
  {
    int fd = openSectionFilterNative(myCard.demuxDeviceFilename(), PIDNIT, TableNIT, -1);
    Util.debug("Searching for NIT");
    Vector<Transponder> result = null;
    try {
      int maxLoops = 3;
      boolean haveFullNIT = false;
      boolean startedNIT = false;
      while ( ! haveFullNIT && maxLoops-- > 0)
      {
        try {
          Section section = readSection(fd, TableNIT);
          result = parseNIT(section.data, section.subjectID, startTransponder);

          haveFullNIT = true; // ASTRA sends sectionNumber = 0, lastSectionNumber = 1, and never sectionNumber = 1 :-(
          if (section.sectionNumber == 0)
            startedNIT = true;
          if (startedNIT && section.sectionNumber == section.lastSectionNumber)
            haveFullNIT = true;
        } catch (Exception e) { Util.error("Read error", e); }
      }
    } finally {
      closeNative(fd);
    }
    return result;
  }

  static public final int PIDPAT = 0x00;
  static public final int PIDNIT = 0x10;
  static public final int PIDSDT = 0x11;
  //int pid = 0x02; int tableID = 0x03; int tableIDExt = ?; // TS Description

  static public final int TableNIT = 0x40;
  static public final int TableNITForOtherTS = 0x41;
  static public final int TablePAT = 0x00;
  static public final int TablePMT = 0x02;
  static public final int TableSDT = 0x42;
  static public final int TableSDTForOtherTS = 0x46;
  static public final int TableCAT = 0x01;

  public class Section
  {
    public int subjectID;
    public BufferPart data;
    public int sectionNumber;
    public int lastSectionNumber;
  }

  public Section readSection(int fd, int expectedTableID)
    throws Exception
  {
    // TODO assuming that DVB card / stream read starts on block boundaries. valid?
    //Util.debug("Waiting for data from DVB card");
    final int readAheadBytes = 8;
    final int kTimeout = 5000; // in ms 
    long startRead = System.currentTimeMillis();
    int count = -11; // -11 = no data yet (EAGAIN)
    while (count == -11 && System.currentTimeMillis() - startRead < kTimeout)
      count = readSectionNative(fd, myBuffer, readAheadBytes); // blocks until input data is available or EOF
    if (count == -11) // no data yet = timeout
      throw new RuntimeException("Can't read section: timeout. No data within " + kTimeout / 1000 + "s.");
    if (count == -1) // EOF or other error
      throw new RuntimeException("Can't read section");
    if (count != readAheadBytes)
      throw new RuntimeException("Can't read starting bytes for section");
    BufferPart sectionHeader = new BufferPart(myBuffer, 0, 8);
    int tableID = sectionHeader.readInt8(0); // tableID is the type/tag of this section
    int sectionLength = sectionHeader.readInt12from16(1);
    if (sectionLength < 0)
      throw new RuntimeException("section length malformed (negative)");
    Section section = new Section();
    section.subjectID = sectionHeader.readInt16(3);
    @SuppressWarnings("unused")
    int sectionVersionNumber = sectionHeader.readInt8(5);
    section.sectionNumber = (sectionHeader.readInt8(6) >> 1) & 0x1F;
    section.lastSectionNumber = sectionHeader.readInt8(7);
    //Util.debug("section tableID 0x" + Integer.toHexString(tableID) + ", subject ID " + subjectID + ", length " + sectionLength);
    sectionLength -= 5; // tableIDExt etc. come after the length field and thus count in the length, but are already read
    if (tableID != expectedTableID)
      throw new Exception("Got wrong table ID");
    //sectionHeader.dumpContent();
    if (sectionLength <= 0)
      throw new RuntimeException("Section is empty"); // Currently all (parsePAT(), parsePMT() and parseNIT()) expect data

    if (count > 1021) // per spec. 1024 - 3 (3 bytes for tableID and sectionLength itself).
      throw new RuntimeException("Section length too big: only 1021 allowed per spec, but got " + sectionLength);
    assert (sectionLength > myBuffer.length - 10); // 10 is just a security margin
    count = -11; // -11 = no data yet (EAGAIN)
    while (count == -11 && System.currentTimeMillis() - startRead < kTimeout)
      count = readSectionNative(fd, myBuffer, sectionLength);
    if (count == -11) // no data yet = timeout
      throw new RuntimeException("Can't read section: timeout. No data within " + kTimeout / 1000 + "s.");
    if (count != sectionLength)
      throw new RuntimeException("Could not read full section: expected " + sectionLength + " bytes, got " + count);
    sectionLength -= 4; // CRC32 (error checksum), at end, is included in sectionLength, but the DVB card already checked it
    section.data = new BufferPart(myBuffer, 0, sectionLength);
    //sectionData.dumpContent();
    //Util.debug("section number " + sectionNumber + ", last section number " + lastSectionNumber);

    return section;
  }

  public void parsePAT(BufferPart buffer, int subjectID, Transponder dsTransponder)
    throws Exception
  {
    dsTransponder.transponderID = subjectID;
    xml("<transport-stream id=" + dsTransponder.transponderID + " >");
    xmlIndent();
    for (BufferPart serviceB : buffer.split(4))
    {
      int serviceID = serviceB.readInt16(); // called program_number in spec, identifies a channel
      int pmtPID = serviceB.readInt13from16();
      if (serviceID == 0) // special case per spec
        dsTransponder.nitPID = pmtPID;
      else
      {
        DVBChannel c = new DVBChannel();
        c.serviceID = serviceID;
        c.pmtPID = pmtPID;
        c.pids.add(pmtPID);
        dsTransponder.channels.add(c);
        xml("<service id=" + c.serviceID + " pmt-pid=" + c.pmtPID + " />");
      }
    }
    xmlOutdent();
    xml("</transport-stream>");
  }

  static public final int StreamVideoMPEG1 = 0x01; // ISO 11172-2
  static public final int StreamVideoMPEG2 = 0x02; // ISO 13818-2, ITU H.262
  static public final int StreamVideoH264 = 0x1B; // H.264, MPEG4 Part 10 Video, AVC, ISO 14496, ITU H.264
  static public final int StreamAudioMPEG1Layer2 = 0x03; // MPEG1 Layer2 Audio, ISO 11172-3
  static public final int StreamAudioMPEG2Layer2 = 0x04; // MPEG2 Layer2? Audio, ISO 13818-3
  static public final int StreamAudioAAC = 0x0F; // ADTS, usually AAC
  static public final int StreamAudioLATM = 0x11; // LATM transport, MPEG4, ISO 14496-3 Amendment 1
  static public final int StreamAudioAC3 = 0x6A; // AC3
  static public final int StreamAudioATSC = 0x81; // ATSC A/53B [2] Annex B
  static public final int StreamMHEG = 0x07; // MHEG, Interactive TV
  static public final int StreamDSMCC_6A = 0x0A; // DSM-CC, whatever that may be. ISO 13818-6 type A
  static public final int StreamDSMCC_6B = 0x0B; // dito ISO 13818-6 type B
  static public final int StreamDSMCC_6C = 0x0C; // dito ISO13818-6 type C
  static public final int StreamDSMCC_6D = 0x0D; // dito ISO 13818-6 type D
  static public final int StreamDSMCC_1A = 0x08; // dito ISO 13818-1 Annex A
  static public final int StreamH222_1 = 0x09; // ITU-T H.222.1, AV multiplex for ATM???

  public void parsePMT(BufferPart buffer, int subjectID, DVBChannel dsChannel)
    throws Exception
  {
    int serviceID = subjectID; // called program_number in spec, identifies a channel
    xml("<program serivceID=" + serviceID + " >");
    xmlIndent();
    try {
    int programClockReferencePID = buffer.readInt13from16(); // 0x1FFF (int -1?) means no PCR 
    if (programClockReferencePID > 0)
      dsChannel.pids.add(programClockReferencePID);

    int descrLength = buffer.readInt12from16();
    for (Descriptor d2 : listDescriptions(buffer.subBuffer(descrLength)))
      parseChannelDescription(d2.tag, d2.data, dsChannel); // TODO

    if (dsChannel.dvbServiceName.length() > 0)
      xml("<service-name>" + dsChannel.dvbServiceName + "</service-name>");
    if (dsChannel.dvbProviderName.length() > 0)
      xml("<provider-name>" + dsChannel.dvbProviderName + "</provider-name>");

    // now information about ES = Elementary Streams
    while (buffer.currentPos() + 5 <= buffer.endPos())
    {
      int type = buffer.readInt8();
      int pid = buffer.readInt13from16();
      int esInfoLength = buffer.readInt12from16();
      if (false && esInfoLength != 0)
        xml("<!--elementary stream info descriptor length " + esInfoLength + " bytes -->");
      BufferPart esInfoB = buffer.subBuffer(esInfoLength);
      switch (type)
      {
        // Video
        case StreamVideoMPEG1:
          xml("<video pid=" + pid + " contenttype=\"video/mpeg1\" />");
          dsChannel.pids.add(pid);
          break;
        case StreamVideoMPEG2:
          xml("<video pid=" + pid + " contenttype=\"video/mpeg2\" />");
          dsChannel.pids.add(pid);
          break;
        case StreamVideoH264:
          xml("<video pid=" + pid + " contenttype=\"video/h264\" />");
          dsChannel.pids.add(pid);
          break;

        // Audio
        case StreamAudioMPEG1Layer2:
          parseAudio(pid, type, "mpeg1/layer2", esInfoB, dsChannel);
          break;
        case StreamAudioMPEG2Layer2:
          parseAudio(pid, type, "mpeg2/layer2", esInfoB, dsChannel);
          break;
        case StreamAudioAAC:
          parseAudio(pid, type, "aac", esInfoB, dsChannel);
          break;
        case StreamAudioLATM:
          parseAudio(pid, type, "mpeg4/latm", esInfoB, dsChannel);
          break;
        case StreamAudioAC3: 
          parseAudio(pid, type, "ac3", esInfoB, dsChannel);
          break;
        case StreamAudioATSC:
          parseAudio(pid, type, "atsc", esInfoB, dsChannel);
          break;

        case 0x06: // private (stream type), ES info contains list of descriptors
          boolean haveTeletext = false;
          for (Descriptor d : listDescriptions(esInfoB))
          {
            switch (d.tag)
            {
            case DescriptorAC3:
              xml("<!-- in private section: -->");
              parseAudio(pid, type, "ac3", new BufferPart(esInfoB), dsChannel);
              // the language is a *sibling* to the AC3 descriptor, although it's a property of it (should be content)
              break;
            case DescriptorTeletext:
              int teletextPid = pid;
              haveTeletext = true;
              String langCode = d.data.readString(3); // ISO 639-2, 3-character
              byte byte3 = d.data.readInt8();
              int teletextType = byte3 >> 3; // higher 5 bits.
              String teletextTypeName = null;
              switch (teletextType)
              {
              case 1: teletextTypeName = "initial-page"; break;
              case 2: teletextTypeName = "subtitle"; break;
              case 3: teletextTypeName = "additional-information"; break;
              case 4: teletextTypeName = "programme-schedule"; break;
              case 5: teletextTypeName = "subtitle-for-hearing-imparied"; break;
              }
              int magazineNumber = byte3 & 8; // lower 3 bits. EN 300 706.
              int pageNumber = d.data.readInt8(); // EN 300 706.
              xml("<teletext pid=" + teletextPid + (teletextTypeName != null ? " type=" + teletextType : "") +
                      " magazineNumber=" + magazineNumber + " pageNumber=" + pageNumber + " >");
              xmlIndent();
              parseLanguage(langCode);
              xmlOutdent();
              xml("</teletext>");
              break;
            case DescriptorSubtitles:  // This, by itself, is no statement about the format. It may be DVB or teletext subtitles.
              int subtitlesPid = pid;
              /* All these descriptors are describing one stream.
               * If another (earlier) descriptor says this stream contains
               * teletext, then obviously this is a teletext subtitle.
               * Otherwise, assume it's a DVB subtitle. */
              xml("<subtitles pid=" + subtitlesPid +
                      " contenttype=\"" + (haveTeletext ? "teletext" : "dvb") + "\" >");
              xmlIndent();
              if ( ! haveTeletext) // I don't want teletext, but DVB subtitles
                dsChannel.pids.add(pid);
              langCode = d.data.readString(3); // ISO 639-2, 3-character
              parseLanguage(langCode);
              xml(d.data.subBuffer(3, d.data.length() - 3).contentAsHexString());
              //for (Descriptor d3 : listDescriptions(d.buf))
              //  parseChannelDescription(d3.tag, d3.buf, dsChannel);
              xmlOutdent();
              xml("</subtitles>");
              break;
            }
          }
          break;

        case StreamMHEG:
          xml("<mheg pid=" + pid + ">");
          break;

        case StreamDSMCC_1A:
        case StreamDSMCC_6A:
        case StreamDSMCC_6B:
        case StreamDSMCC_6C:
        case StreamDSMCC_6D:
          xml("<dsm-cc pid=" + pid + " type=0x" + Integer.toHexString(type) + " >");
          break;

        case StreamH222_1:
          xml("<h222-1 pid=" + pid + ">");
          break;
        
        default:
          xml("<unknown-elementary-stream type=0x" + Integer.toHexString(type) + " pid=" + pid + " >");
          xml("    " + esInfoB.contentAsHexString());
          xml("</unknown-elementary-stream>");
      }
    }
    } finally {
      xmlOutdent();
      xml("</program>");
    }
  }

  public void parseSDT(BufferPart buffer, int subjectID, Transponder dsTransponder)
    throws Exception
  {
    dsTransponder.originalNetworkID = buffer.readInt16();
    buffer.readInt8(); // reserved
    while (buffer.currentPos() + 5 <= buffer.endPos())
    {
      int serviceID = buffer.readInt16();

      // find channel with this service ID in dsTransponder
      DVBChannel dsChannel = null;
      for (DVBChannel cur : dsTransponder.channels)
        if (cur.serviceID == serviceID)
          dsChannel = cur;
      if (dsChannel == null)
        //c = dsTransponder.newChannel(); // TODO Good idea? Won't have PIDs etc.
        dsChannel = new DVBChannel(); // dummy TODO

      int byte3 = buffer.readInt8();
      boolean eitAvailable = (byte3 >> 1 & 1) > 0; // EIT (the program schedule) is available on this TS
      boolean eitPresentFollowingAvailable = (byte3 & 1) > 0;
      int byte4 = buffer.readInt8(buffer.currentPos());
      int descrLength = buffer.readInt12from16();
      dsChannel.encrypted = (byte4 >> 4 & 1) > 0;
      int runningNum = byte4 >> 5 & 0x7;
      String running = "";
      switch (runningNum)
      {
      case 0: running = "undefined"; break;
      case 1: running = "not running"; break;
      case 2: running = "starts in a few seconds"; break;
      case 3: running = "pausing"; break;
      case 4: running = "running"; break;
      case 5: running = "off air"; break;
      default: running = "unknown"; break; // 6 and 7 reserved for future use
      }

      xml("<service id=" + serviceID + " scrambled=\"" + (dsChannel.encrypted ? "true" : "false") + "\" running=\"" + running + "\" >");
      xmlIndent();

      for (Descriptor d : listDescriptions(buffer.subBuffer(descrLength)))
        parseSDTChannelDescription(d.tag, d.data, dsChannel);

      xmlOutdent();
      xml("</service>");
    }
  }

  public Vector<Transponder> parseNIT(BufferPart buffer, int subjectID, Transponder dsTransponder)
    throws Exception
  {
    dsTransponder.networkID = subjectID;
    xml("<network id=" + dsTransponder.networkID + " >");
    xmlIndent();
    int descrLength = buffer.readInt12from16();
    for (Descriptor d : listDescriptions(buffer.subBuffer(descrLength)))
      parseTransponderDescription(d.tag, d.data, dsTransponder);
    buffer.readInt16(); // TODO ?
    Vector<Transponder> result = Util.newVector();
    while (buffer.currentPos() + 6 <= buffer.endPos())
    {
      Transponder otherTransponder = new Transponder();
      otherTransponder.transponderID = buffer.readInt16();
      otherTransponder.originalNetworkID = buffer.readInt16();
      xml("<transport-stream id=" + otherTransponder.transponderID + " originalNetworkID=" + otherTransponder.originalNetworkID + " />");
      xmlIndent();
      descrLength = buffer.readInt12from16();
      for (Descriptor d : listDescriptions(buffer.subBuffer(descrLength)))
        parseTransponderDescription(d.tag, d.data, otherTransponder);
      xmlOutdent();
      xml("</transport-stream>");

      /* Head stations (sat dishes and receivers on roof of apartment houses)
       * map DVB-S to DVB-C, but the stream still contains DVB-S NITs
       * which are useless for us, so don't add these. */
      // TODO This check assumes that a card can tune only one type, which matches dsTransponder.tuning.
      // And it doesn't work (all pass)
      //if ( ! myCard.canRecord(dsTransponder.tuning))
      //  continue;
      Class<? extends DVBTransponder> lookingFor = dsTransponder.tuning.getClass();
      if (lookingFor.equals(DVBS2Transponder.class))
        lookingFor = DVBSTransponder.class;
      if ( ! lookingFor.isInstance(otherTransponder.tuning))
        continue;

      result.add(otherTransponder);
    }
    xmlOutdent();
    xml("</network>");
    return result;
  }

  protected void parseVideo(int pid, int type, String typeName, BufferPart esInfoB, DVBChannel dsChannel)
  {
    dsChannel.pids.add(pid);
    xml("<video pid=" + pid + " contenttype=\"video/" + typeName + "\" />");
  }

  protected void parseAudio(int pid, int type, String typeName, BufferPart esInfoB, DVBChannel dsChannel)
  {
    dsChannel.pids.add(pid);
    xml("<audio pid=" + pid + " contenttype=\"audio/" + typeName + "\" >");
    xmlIndent();
    for (Descriptor d : listDescriptions(esInfoB))
      parseAudioStreamDescription(d.tag, d.data, dsChannel);
    xmlOutdent();
    xml("</audio>");
  }

  // ?, stream
  static public final int DescriptorDataStreamAlignment = 0x06;
  static public final int DescriptorMPEG2VideoProfileAndLevel = 0x02;
  static public final int DescriptorMPEG4VideoProfileAndLevel = 0x1B;
  static public final int DescriptorMPEG4AudioProfileAndLevel = 0x1C;
  static public final int DescriptorSLStream = 0x1E;
  static public final int DescriptorFlexMux = 0x1F;
  static public final int DescriptorExternalESID = 0x20;
  static public final int DescriptorCA = 0x09;
  // SDT
  static public final int DescriptorCASystemIDs = 0x53;
  static public final int DescriptorService = 0x48;
  static public final int DescriptorDataBroadcast = 0x64;
  // ?
  static public final int DescriptorSystemClock = 0x0B; // not clock itself, just how precise it is etc.
  // NIT
  static public final int DescriptorNetworkName = 0x40;
  static public final int DescriptorServiceList = 0x41;
  static public final int DescriptorDVBSFrequencies = 0x43;
  static public final int DescriptorDVBCFrequencies = 0x44;
  static public final int DescriptorDVBTFrequencies = 0x5A;
  static public final int DescriptorAlterantiveFrequenciesForSameMultiplex = 0x62;
  // PMT, audio
  static public final int DescriptorLanguage = 0x0A;
  static public final int DescriptorAC3 = 0x6A;
  static public final int DescriptorStreamIdentifer = 0x52;
  // PMT, private
  static public final int DescriptorTeletext = 0x56;
  static public final int DescriptorSubtitles = 0x59; // No statement about format. May be DVB or teletext subtitles.


  protected String parseLanguage(String threeletter)
  {
    String twolettercode = convertLangCode3to2(threeletter);
    xml("<language lang=\"" + twolettercode + "\" iso639-3=\"" + threeletter + "\" />");
    return twolettercode;
  }

  protected String parseLanguageDescription(int descrTag, BufferPart descrB)
  {
    assert(descrTag == DescriptorLanguage);
    String last = "none";
    for (BufferPart langB : descrB.split(4))
    {
      last = parseLanguage(langB.readString(3));
      int audioType = langB.readInt8();
      String audioTypeName = null;
      switch (audioType)
      {
      case 0x01: // Clean effects - just sound, "no language" (quote spec)
        // audioTypeName = "effectsOnly"; -- stations send this for normal audio channels :-(
        break;
      case 0x02: // Hearing impaired
        audioTypeName = "hearingImparied";
        break;
      case 0x03: // visually impaired (commentary)
        audioTypeName = "visuallyImparied";
        break;
      }
      if (audioTypeName != null)
      xml("<audio-type type=\"" + audioTypeName + "\" />");
    }
    return last;
  }

  // Spec: ISO 13818-1, Section 2.6, Page 39ff
  protected void parseTransponderDescription(int descrTag, BufferPart descrB, Transponder dsTransponder)
  {
    try {
      switch (descrTag)
      {
      case DescriptorSystemClock: // System clock description (not clock itself, just how precise it is etc.)
        break;

      case DescriptorNetworkName:
        dsTransponder.networkName = convertString(descrB.readString(descrB.length()));
        xml("<network-name name=\"" + dsTransponder.networkName + "\" />");
        break;

      case DescriptorServiceList: // TODO why would I use that and not the PAT?
        for (BufferPart serviceB : descrB.split(3))
        {
          int serviceID = serviceB.readInt16();
          int type = serviceB.readInt8();
          xml("<service id=" + serviceID + " /><service-type type=0x" + Integer.toHexString(type) + " /></service>");
        }
        break;

      case DescriptorDataStreamAlignment: // data stream alignment
        int data = descrB.readInt8();
        xml("<data-stream-alignment data=" + data + " />");
        break;

      case DescriptorDVBSFrequencies:
        parseDVBSTuningDescriptor(descrTag, descrB, dsTransponder);
        break;

      case DescriptorDVBCFrequencies:
        parseDVBCTuningDescriptor(descrTag, descrB, dsTransponder);
        break;
      case DescriptorDVBTFrequencies:
        parseDVBTTuningDescriptor(descrTag, descrB, dsTransponder);
        break;
      case DescriptorAlterantiveFrequenciesForSameMultiplex:
        descrB.readInt8(); // TODO: ?
        for (BufferPart freqB : descrB.split(4))
        {
          int frequency = 10 * freqB.readInt32BDC();
          xml("<alternative-frequency freq=" + frequency + " />");          
        }
        break;

      default:
        unknownDescriptor(descrTag, descrB);
      }
    } catch (Exception e) {
      Util.error("Descriptor content malformed", e);
    }
  }

  // Spec: ISO 13818-1, Section 2.6, Page 39ff
  protected void parseChannelDescription(int descrTag, BufferPart descrB, DVBChannel dsChannel)
  {
    try {
      switch (descrTag)
      {
      case DescriptorMPEG2VideoProfileAndLevel:
        descrB.readInt8();
        // Bit field, defined by profile_and_level_indication fields in ISO 13818-2
        @SuppressWarnings("unused")
        byte mpeg2VideoProfileAndLevel = descrB.readInt8();
        descrB.readInt8();
        break;
      case DescriptorMPEG4VideoProfileAndLevel:
        // Does not apply to MPEG4 streams encapsulated in SL-packets and in FlexMux
        // Bit field, defined by profile_and_level_indication field in the Visual Object Sequence Header in MPEG4 video
        @SuppressWarnings("unused")
        byte mpeg4VideoProfileAndLevel = descrB.readInt8();
        break;
      case DescriptorMPEG4AudioProfileAndLevel:
        // Does not apply to MPEG4 streams encapsulated in SL-packets and in FlexMux
        // Bit field, defined by ISO 13818-1 (not 14496-3), Table 2-62
        @SuppressWarnings("unused")
        byte mpeg4AudioProfileAndLevel = descrB.readInt8();
        break;

      case DescriptorSLStream:
        int esID = descrB.readInt16();
        xml("<sl-stream es-id=" + esID + " />");
        break;
      case DescriptorFlexMux:
        // Used when the whole program is again wrapped in FlexMux
        xml("<flexmux>");
        for (BufferPart esIDB : descrB.split(3))
        {
          esID = esIDB.readInt16();
          int flexMuxChannelNum = descrB.readInt8();
          xml("<flexmux-stream es-id=" + esID + " flexmux-channel-number=" + flexMuxChannelNum + " />");
        }
        xml("</flexmux>");
        break;
      case DescriptorExternalESID:
        esID = descrB.readInt16();
        xml("<other-es-stream es-id=" + esID + " />");
        break;

      case DescriptorLanguage:
        String langCode = parseLanguageDescription(descrTag, descrB);
        dsChannel.languages.add(langCode);
        break;

      case DescriptorCA:
        parseCADescriptor(descrTag, descrB, dsChannel);
        break;

      default:
        unknownDescriptor(descrTag, descrB);
      }
    } catch (Exception e) {
      Util.error("Descriptor content malformed", e);
    }
  }

  protected void parseSDTChannelDescription(int descrTag, BufferPart descrB, DVBChannel dsChannel)
  {
    try {
      switch (descrTag)
      {
      case DescriptorService:
        byte serviceType = descrB.readInt8();
        String serviceTypeName;
        if (serviceType == 1)
        {
          serviceTypeName = "TV";
          dsChannel.contentType = ChannelContentType.TV;
        }
        else if (serviceType == 2)
        {
          serviceTypeName = "Radio";
          dsChannel.contentType = ChannelContentType.Radio;
        }
        else if (serviceType == 3)
        {
          serviceTypeName = "TeletextOnly";
          dsChannel.contentType = ChannelContentType.Other;
        }
        else if (serviceType == 0x10)
        {
          serviceTypeName = "MHP";
          dsChannel.contentType = ChannelContentType.Other;
        }
        else if (serviceType == 0x11)
        {
          serviceTypeName = "HDTV-MPEG2";
          dsChannel.contentType = ChannelContentType.TV;
          dsChannel.isHDTV = true;
        }
        else if (serviceType == 0x19) // HDTV with "advanced codec"
        {
          serviceTypeName = "HDTV";
          dsChannel.contentType = ChannelContentType.TV;
          dsChannel.isHDTV = true;
        }
        else if (serviceType == 0x0C)
        {
          serviceTypeName = "Data";
          dsChannel.contentType = ChannelContentType.Data;
        }
        else // many other types specced, but probably uncommon
        {
          serviceTypeName = "unknown";
          dsChannel.contentType = ChannelContentType.Other;
        }

        int length = descrB.readInt8();
        String providerName = convertString(descrB.readString(length));
        length = descrB.readInt8();
        String serviceName = convertString(descrB.readString(length));

        dsChannel.dvbServiceName = serviceName;
        dsChannel.dvbProviderName = providerName;
        if (dsChannel.myStationLongName == null)
          dsChannel.myStationLongName = serviceName;
        if (dsChannel.myStationShortName == null)
          dsChannel.myStationShortName = serviceName;

        xml("<service-type typeName=\"" + serviceTypeName + "\" type=0x" + Integer.toHexString(serviceType) + " />");
        xml("<service-name>" + serviceName + "</service-name>");
        xml("<provider-name>" + providerName + "</provider-name>");
        break;

      case DescriptorCA:
        parseCADescriptor(descrTag, descrB, dsChannel);
        break;

      case DescriptorCASystemIDs:
        for (BufferPart caSysB : descrB.split(2))
        {
          int caSystemID = caSysB.readInt16();
          xml("<ca-system id=0x" + Integer.toHexString(caSystemID) + " />");
        }
        break;

      case DescriptorDataBroadcast:
        int dataBroadcastID = descrB.readInt16(); // defined by TR 101 162
        int componentTag = descrB.readInt8();
        length = descrB.readInt8();
        String selector = descrB.subBuffer(length).contentAsHexString();
        parseLanguage(descrB.readString(3));
        length = descrB.readInt8();
        String dataDescription = convertString(descrB.readString(length));
        xml("<data-broadcast type-id=" + dataBroadcastID +
                " selector=\"" + selector + "\"" + 
                (componentTag != 0 ? " component-id=" + componentTag : "") +
                " >" + dataDescription + "</data-broadcast>");
        break;

      default:
        unknownDescriptor(descrTag, descrB);
      }
    } catch (Exception e) {
      Util.error("Descriptor content malformed", e);
    }
  }

  protected void parseCADescriptor(int descrTag, BufferPart descrB, DVBChannel dsChannel)
  {
    try {
      assert (descrTag == DescriptorCA);
      int caSystem = descrB.readInt16();
      int pid = descrB.readInt13from16();
      String caSystemName;
      // @see <http://en.wikipedia.org/wiki/Conditional_access_system>
      switch (caSystem & 0xFF00)
      {
        case 0x0100:
          caSystemName = "SECA";
          break;
          // TODO ECM ID, several
        case 0x0500:
          caSystemName = "Viaccess";
          break;
          // TODO ECM ID, several
        case 0x0600:
          caSystemName = "Irdeto";
          break;
        case 0x1700:
          caSystemName = "Nagravision Aladin / BetaCrypt";
          // Originally BetaCrypt, then made to wrap Nagravision for compatibility (dbox2)
          // Used by Premiere (Germany)
          break;
        case 0x1800:
          caSystemName = "Nagravision";
          break;
        case 0x0B00:
          caSystemName = "Conax";
          // Created by Telenor
          break;
        case 0x0900:
          caSystemName = "NDS VideoGuard";
          break;
        case 0x0D00:
          caSystemName = "CryptoWorks";
          break;
        default:
          caSystemName = "unknown";
      }
      dsChannel.encryptSystems.add(caSystem);
      xml("<ca-system system-name=\"" + caSystemName + "\" system-id=0x" +
              Integer.toHexString(caSystem) + " ecm-pid=" + pid + " />");
    } catch (Exception e) { Util.error("CA descriptor content malformed", e); }
  }

  protected void parseAudioStreamDescription(int descrTag, BufferPart descrB, DVBChannel dsChannel)
  {
    try {
      switch (descrTag)
      {
      case DescriptorLanguage:
        String langCode = parseLanguageDescription(descrTag, descrB);
        dsChannel.languages.add(langCode);
        break;
      case DescriptorStreamIdentifer:
        int id = descrB.readInt8();
        xml("<stream-identifer id=" + id + " />");
        break;
      case DescriptorAC3:
        xml("<ac3>" + descrB.contentAsHexString() + "</ac3>");
        break;
      case DescriptorCA:
        parseCADescriptor(descrTag, descrB, dsChannel);
        break;

      default:
        unknownDescriptor(descrTag, descrB);
      }
    } catch (Exception e) {
      Util.error("Descriptor content malformed", e);
    }
  }

  protected void parseDVBSTuningDescriptor(int descrTag, BufferPart descrB, Transponder dsTransponder)
  {
    if (dsTransponder.tuning != null)
    {
      Util.debug("Got tuning data for my own transponder");
      return;
    }
    try {
      int frequency = 10 * descrB.readInt32BDC(); // Field in 10 KHz. Freq in KHz for DVB-S
      float orbitalPosition = BufferPart.convInt32BDC(0, 0, descrB.readInt8(), descrB.readInt8()) * 0.1f; // in degrees
      byte byte6 = descrB.readInt8();
      boolean west = ((byte6 & 0x80) >> 7) == 0; // bit 1. true = west, false = east 
      int polNum = (byte6 & 0x60) >> 5; // bits 2+3
      int rolloffNum = byte6 >> 3 & 3; // bit 4+5
      boolean dvbS2 = (byte6 >> 2 & 1) == 1; // bit 6
      int modNum = byte6 & 0x3; // bits 7+8
      byte byte10;
      int symbolrate = 10 * BufferPart.convInt32BDC(descrB.readInt8(), descrB.readInt8(), descrB.readInt8(),
              (byte10 = descrB.readInt8()) & 0xF0);
      String fecInner = mapFec(byte10 & 0x07); // lower 4 bits (upper 4 are part of symbolrate)

      DVBSTransponder c = dvbS2 ? new DVBS2Transponder() : new DVBSTransponder();
      c.frequency = frequency;
      c.symbolrate = symbolrate;
      c.fec = fecInner;
      switch (polNum)
      {
        case 0: c.polarity = 'h'; break;
        case 1: c.polarity = 'v'; break;
        case 2: c.polarity = 'l'; break;
        case 3: c.polarity = 'r'; break;
        default: throw new RuntimeException("Unknown value for polarity");
      }
      if      (modNum == 1) c.modulation = "QPSK";
      else if (modNum == 2) c.modulation = "8PSK";
      //else if (modNum == 3) c.modulation = "QAM16"; // huch?
      else c.modulation = "AUTO";
      if (dvbS2)
      {
        DVBS2Transponder c2 = (DVBS2Transponder) c;
        if      (rolloffNum == 0) c2.rolloff = "35";
        else if (rolloffNum == 1) c2.rolloff = "25";
        else if (rolloffNum == 2) c2.rolloff = "20";
        else c2.rolloff = "AUTO";
        c2.pilot = 'a';
      }

      dsTransponder.tuning = c;

      xml("<tuning delsys=\"" + (dvbS2 ? "DVB-S2" : "DVB-S") + "\" freq=" + c.frequency + " symbol-rate=" + c.symbolrate + 
              " pol=\"" + c.polarity + "\" fec-inner=\"" + c.fec + 
              "\" orbitalPosition=\"" + orbitalPosition + "\" orbitalDirection=\"" + (west ? "west" : "east") + "\" />");
    } catch (Exception e) {
      Util.error("Satellite tuning information in unexpected format", e);
    }
  }

  protected void parseDVBTTuningDescriptor(int descrTag, BufferPart descrB, Transponder dsTransponder)
  {
    if (dsTransponder.tuning != null)
    {
      Util.debug("Got tuning data for my own transponder");
      return;
    }
    try {
      DVBTTransponder c = new DVBTTransponder();
      c.frequency = 10 * descrB.readInt32BDC(); // Field in 10 Hz. Freq in Hz For DVB-T
      byte byte3 = descrB.readInt8();
      int bandwidthNum = byte3 >> 5; // upper 3 bits
      switch (bandwidthNum)
      {
        case 0: c.bandwidth = '8'; break;
        case 1: c.bandwidth = '7'; break;
        case 2: c.bandwidth = '6'; break;
        case 3: c.bandwidth = '5'; break;
        default: throw new RuntimeException("Unknown value for bandwidth");
      }
      boolean isHP = (byte3 >> 4 & 1) == 1; // is high priority stream
      boolean mpeFEC = (byte3 >> 3 & 1) == 1; // MPE-FEC (EN 301 192)
      byte byte4 = descrB.readInt8();
      int constellationNum = byte4 >> 6; // upper 2 bits
      switch (constellationNum)
      {
        case 0: c.modulation = "QPSK"; break;
        case 1: c.modulation = "QAM16"; break;
        case 2: c.modulation = "QAM64"; break;
        default: throw new RuntimeException("Unknown value for consellation/modulation");
      }
      int hierarchyNum = byte4 >> 3 & 7; // 3 bits 6-4
      switch (hierarchyNum)
      {
        case 0: c.hierarchy = 'N'; break;
        case 1: c.hierarchy = '1'; break;
        case 2: c.hierarchy = '2'; break;
        case 3: c.hierarchy = '4'; break;
        default: throw new RuntimeException("hierarchy in-depth interleaver not supported");
      }
      int hpCodePageNum = byte4 & 7; // lowest 3 bits
      c.hp_code_rate = mapDVBTFec(hpCodePageNum);
      byte byte5 = descrB.readInt8();
      int lpCodePageNum = byte5 >> 5; // upper 3 bits
      c.lp_code_rate = c.hierarchy == 'N' ? "NONE" : mapDVBTFec(lpCodePageNum);
      int guardIntervalNum = byte5 >> 3 & 3; // 2 bits 5-4
      switch (guardIntervalNum)
      {
        case 0: c.guard_interval = "1/32"; break;
        case 1: c.guard_interval = "1/16"; break;
        case 2: c.guard_interval = "1/8"; break;
        case 3: c.guard_interval = "1/4"; break;
        // it's only 2 bits, so no default
      }
      int transmissionModeNum = byte5 >> 1 & 3; // 2 bits 3-2
      switch (transmissionModeNum)
      {
        case 0: c.transmission_mode = '2'; break;
        case 1: c.transmission_mode = '8'; break;
        case 2: c.transmission_mode = 'A'; break; // 4K mode, but not supported by Linux DVB, so set to AUTO
        default: throw new RuntimeException("Unknown value for transmission mode");
      }
      boolean otherFrequenciesInUse = (byte5 >> 1 & 1) == 1; // lowest bit. Whatever this is.

      dsTransponder.tuning = c;

      xml("<tuning delsys=\"dvb-t\" freq=" + c.frequency + " bandwidth=" + c.bandwidth + 
              " constellation=\"" + c.modulation + "\" hierarchy=\"" + c.hierarchy + 
              "\" high-priority-code-rate=\"" + c.hp_code_rate + "\" low-priority-code-rate=\"" + c.lp_code_rate +
              "\" guard-interval=\"" + c.guard_interval + "\" transmission-mode=\"" + c.transmission_mode +
              "\" />");
    } catch (Exception e) {
      Util.error("Terrestrial DVB-T tuning information in unexpected format", e);
    }
  }
  // DVB-T
  protected String mapDVBTFec(int fec)
  {
    switch (fec)
    {
    case 0: return "1/2";
    case 1: return "2/3";
    case 2: return "3/4";
    case 3: return "5/6";
    case 4: return "7/8";
    default: throw new RuntimeException("Unknown value for FEC");
    }
  }

  protected void parseDVBCTuningDescriptor(int descrTag, BufferPart descrB, Transponder dsTransponder)
  {
    if (dsTransponder.tuning != null)
    {
      Util.debug("Got tuning data for my own transponder");
      return;
    }
    try {
      DVBCTransponder c = new DVBCTransponder();
      c.frequency = 100 * descrB.readInt32BDC(); // Field in 100 Hz. Freq in Hz for DVB-C.
      descrB.readInt8(); // reserved
      int fecNum = descrB.readInt8() & 0x0F; // lower 4 bits, upper 4 bits reserved.
      switch (fecNum)
      {
      // 0 "not defined", so default -> throw
      case 1: c.fec = "NONE";
      case 2: c.fec = "AUTO"; // RS(208/188). No idea what's that supposed to mean, so set to AUTO
      default: c.fec = "AUTO";
      //default: throw new RuntimeException("Unknown value " + fecNum + " for FEC");
      }
      int modulationNum = descrB.readInt8();
      switch (modulationNum)
      {
      // 0 "not defined", so default -> throw
      case 1: c.modulation = "QAM16"; break;
      case 2: c.modulation = "QAM32"; break;
      case 3: c.modulation = "QAM64"; break;
      case 4: c.modulation = "QAM128"; break;
      case 5: c.modulation = "QAM256"; break;
      default: c.modulation = "AUTO";
      //default: throw new RuntimeException("Unknown value for modulation");
      }
      byte byte10;
      c.symbolrate = 10 * BufferPart.convInt32BDC(descrB.readInt8(), descrB.readInt8(), descrB.readInt8(),
              (byte10 = descrB.readInt8()) & 0xF0);
      c.fec = mapFec(byte10 & 0x07); // lower 4 bits (upper 4 are part of symbolrate)

      dsTransponder.tuning = c;

      xml("<tuning delsys=\"dvb-c\" freq=" + c.frequency + " symbol-rate=" + c.symbolrate + 
              " modulation=\"" + c.modulation + "\" fec-inner=\"" + c.fec + "\" />");
    } catch (Exception e) {
      Util.error("Satellite tuning information in unexpected format", e);
    }
  }
  // DVB-S FEC inner, DVB-C FEC outer
  protected String mapFec(int fec)
  {
    switch (fec)
    {
    // 0 "not defined", so default
    case 1: return "1/2";
    case 2: return "2/3";
    case 3: return "3/4";
    case 4: return "5/6";
    case 5: return "7/8";
    case 6: return "8/9";
    case 7: return "3/5";
    case 8: return "4/5";
    case 9: return "9/10";
    case 0xFF: return "NONE";
    default: throw new RuntimeException("Unknown value for FEC");
    }
  }

  protected void unknownDescriptor(int descrTag, BufferPart descrB)
  {
    xml("<unknown-descriptor tag=0x" + Integer.toHexString(descrTag) + " >");
    xml("    " + descrB.contentAsHexString());
    xml("</unknown-descriptor>");
  }

  public class Descriptor
  {
    protected Descriptor(int aTag, BufferPart someData)
    {
      tag = aTag;
      data = someData;
    }
    public int tag;
    public BufferPart data; // does not contain descriptor header (tag and length field), just the data
  }

  protected Vector<Descriptor> listDescriptions(BufferPart buffer)
  {
    Vector<Descriptor> result = Util.newVector();
    try {
      while (buffer.currentPos() + 3 <= buffer.endPos())
      {
        int descrTag = buffer.readInt8();
        int descrLength = buffer.readInt8(); // (this length is really just 8 bits)
        if (descrLength < 0)
          throw new RuntimeException("descriptor tag " + descrTag + ": descriptor length malformed (negative value) ");
        BufferPart descrB = buffer.subBuffer(descrLength);
        result.add(new Descriptor(descrTag, descrB));
      }
    } catch (Exception e) {
      Util.error("Descriptor header malformed", e);
    }
    return result;
  }

  /**
   * Converts ISO 639-2/3 (3 characters) to ISO 639-1 (2 characters)
   * @param c   3-char ISO 639-2/3 language code, upper or lower case, e.g. "ger" (German)
   * @return 2-char ISO 639-1 language code, lower case, e.g. "de" (German)
   *   or "unknown"
   */
  protected String convertLangCode3to2(String c)
  {
    c = c.toLowerCase(Locale.ENGLISH);

    // Very common
    if (c.equals("eng")) // English
      return "en";
    if (c.equals("deu") || c.equals("ger")) // German
      return "de";
    if (c.equals("fre") || c.equals("fra")) // French
      return "fr";
    if (c.equals("spa")) // Spanish
      return "es";

    // Not in Java
    if (c.equals("cze") || c.equals("ces")) // Czech
      return "cs";
    if (c.equals("dut")) // Dutch
      return "nl";
    if (c.equals("slo")) // Slovak
      return "sk";

    // Ask Java
    for (Locale locale : Locale.getAvailableLocales()) // TODO slow
      if (locale.getISO3Language().toLowerCase(Locale.ENGLISH).equals(c)) // TODO getISO3 returns upper or lower?
        return locale.getLanguage();
    return "unknown";
  }
  /**
   * Rant: *Of course*, these guys had to:
   * - create their own charset.
   * - create a new way to select another charset.
   * As if we didn't have enough charsets yet.
   *
   * Throw in some control characters for "bold", for fun, too.
   * If 9Live couldn't do "*WIN NOW*-!-!-!-!-!-!-" incl. bold,
   * there'd be something missing.
   * Control characters are 0x80-0x9F, plus more for 16bit,
   * and 0x0-0x20 for charset selection etc..
   *
   * Oh, yeah, and they invented a way to put two strings in one,
   * the short and long name are the same string, and the short
   * version starts at 0xFF86 and ends at 0xFF87,
   * with multiple occurrences. So, I have to strip that, too. 
   *
   * DVB SI uses 8-byte strings. Default is latin.
   * However, the first byte(s) can select another charset.
   * @param bytes as in the DVB stream, 8 byte per character
   * @return Unicode string
   */
  public String convertString(String bytes)
  {
    // remove control characters
    //bytes.replaceAll("[\\x80-\\x9F]", "");
    //bytes.replaceAll("[\\xFF86\\xFF87]", "");
    String bytes2 = "";
    for (int i = 0; i < bytes.length(); i++)
    {
      int c = bytes.charAt(i);
      if (c == 0xFF86 || c == 0xFF87 ||
         (c >= 0x80 && c <= 0x9F) ||
         c < 0x20)
        continue;
      bytes2 += bytes.charAt(i);
    }
    bytes = bytes2;

    /*
    // Honor charset specced
    byte byte0 = (byte) bytes.charAt(0);
    if (byte0 < 0x20)
    {
      byte byte1 = (byte) bytes.charAt(1);
      byte byte2 = (byte) bytes.charAt(2);
      if (byte0 == 0x10 && byte1 == 0 && byte2 <= 15) // ISO-8859-n, where n = byte2
      {
        Charset cs = Charset.forName("ISO-8859-" + Integer.toString(byte2));
        Util.debug("String in " + cs.displayName());
        ByteBuffer buffer = ByteBuffer.allocate(bytes.length());
        for (int i = 3; i < bytes.length(); i++)
          buffer.put((byte) bytes.charAt(i));
        return cs.decode(buffer).toString();
      }
      else if (byte0 <= 0xB) // ISO-8859-n, where n = byte0 + 4
      {
        Charset cs = Charset.forName("ISO-8859-" + Integer.toString(byte0 + 4));
        Util.debug("String in " + cs.displayName());
        ByteBuffer buffer = ByteBuffer.allocate(bytes.length());
        for (int i = 1; i < bytes.length(); i++)
          buffer.put((byte) bytes.charAt(i));
        return cs.decode(buffer).toString();
        return bytes;
      }
      
      Util.debug("Warning: String in other charset");
    }
    */
    return bytes;
  }



  ////////////////////////////////////////////////////////////////
  // App
  ////////////////////////////////////////////////////////////////

  protected void xmlIndent()
  {
    if (myOutputXML)
      myXMLIndention += "  ";
  }
  protected void xmlOutdent()
  {
    if (myOutputXML)
      myXMLIndention = myXMLIndention.substring(0, myXMLIndention.length() - 2);
  }
  protected void xml(String xml)
  {
    if (myOutputXML)
      myXML += myXMLIndention + xml + "\n";
    if (myOutputXMLImmediately)
      Util.debug(myXMLIndention + xml);
  }
  public void enableXML(boolean enable)
  {
    myOutputXML = enable;
  }
  public String resultXML()
  {
    return myXML;
  }
  /**
   * Translate - dummy for now
   * @param untranslated
   * @return translated string
   */
  protected String tr(String untranslated)
  {
    return untranslated;
  }

  // Data
  protected byte[] myBuffer = new byte[1100]; // 1024 bytes per spec, plus security margin
  protected String myXML = "";
  protected String myXMLIndention = "";
  protected boolean myOutputXML = false;
  protected boolean myOutputXMLImmediately = false;
}
