package org.bucksch.zeipis.recorder;
import org.bucksch.zeipis.all.*;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

/**
 * EIT (program schedule)
 *
 * I'm trying to (roughly) output XMLTV
 */
public class DVBEIT extends DVBScan
{
  public DVBEIT(DVBCard card)
    throws CantUseTVCard
  {
    super(card);
    myOutputXMLImmediately = true;
  }

  public void tuneAndReadEIT(DVBChannel channel)
    throws Misconfiguration, CantUseTVCard
  {
    DVBTuner tuner = myCard.tuner();
    tuner.tuneTo(channel);
    try {
      readEITTuned();
    } finally {
      tuner.done();
    }
  }

  static public final int PIDEIT = 0x12;
  static public final int TableEITCurrent = 0x4E; // "present/following": currently running and chronologically following
  static public final int TableEITCurrentOtherTS = 0x4F;
  static public final int TableEITSchedule = 0x50; // to 0x5F // future programmes
  static public final int TableEITScheduleOtherTS = 0x60; // to 0x6F

  public void readEITTuned()
    throws CantUseTVCard
  {
    int fd = openSectionFilterNative(myCard.demuxDeviceFilename(), PIDEIT, TableEITSchedule, -1);
    Util.debug("Searching for EIT");
    try {
      int maxLoops = 10;
      boolean haveFullEIT = false;
      boolean startedEIT = false;
      while ( ! haveFullEIT && maxLoops-- > 0)
      {
        try {
          Section section = readSection(fd, TableEITSchedule);
          parseEIT(section.data, section.subjectID);

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

  protected static DateFormat xmltvdateformat = new SimpleDateFormat("yyyyMMddHHmmss z");

  public void parseEIT(BufferPart buffer, int subjectID)
  {
    int serviceID = subjectID;
    int tsID = buffer.readInt16();
    int originalNetworkID = buffer.readInt16();
    String channelID = "dvb-N" + originalNetworkID + "-T" + tsID + "-S" + serviceID; // TODO map to proper channel IDs
    int segmentLastSectionNumber = buffer.readInt8();
    int lastTableID = buffer.readInt8();
    //buffer.readInt16(); // TODO ?
    while (buffer.currentPos() + 12 <= buffer.endPos())
    {
      int eventID = buffer.readInt16();
      Calendar starttime = convertMJD(buffer.readInt16()); // 16 LSB of (modified Juian) date
      starttime.set(Calendar.HOUR_OF_DAY, buffer.readInt8());
      starttime.set(Calendar.MINUTE, buffer.readInt8());
      starttime.set(Calendar.SECOND, buffer.readInt8());
      Calendar endtime = (Calendar) starttime.clone();
      endtime.add(Calendar.HOUR_OF_DAY, buffer.readInt8());
      endtime.add(Calendar.MINUTE, buffer.readInt8());
      endtime.add(Calendar.SECOND, buffer.readInt8());
      byte byte11 = buffer.readInt8(buffer.currentPos()); // upper byte of descrLength
      int runningStatus = byte11 >> 5; // upper 3 bits
      String runningStatusName = null;
      switch (runningStatus)
      {
      case 1: runningStatusName = "not running"; break;
      case 2: runningStatusName = "starts in a few seconds"; break;
      case 3: runningStatusName = "pausing"; break;
      case 4: runningStatusName = "running"; break;
      case 5: runningStatusName = "service off-air"; break;
      }
      boolean fta = (byte11 >> 4 & 1) == 0; // bit 4. Free to air = free TV = not scambled = no CA needed
      int descrLength = buffer.readInt12from16();
      
      xml("<programme channel=\"" + channelID + "\""
              + " networkID=" + originalNetworkID
              + " transponderID=" + tsID
              + " serviceID=" + serviceID
              + " start=\"" + xmltvdateformat.format(starttime.getTime()) + "\""
              + " stop=\"" + xmltvdateformat.format(endtime.getTime()) + "\""
              + " >");
      xmlIndent();
      if ( ! fta)
        xml("<scramled/>");
      for (Descriptor d : listDescriptions(buffer.subBuffer(descrLength)))
        parseEITDescriptors(d.tag, d.data);
      xmlOutdent();
      xml("</programme>");
    }
  }

  static public final int DescriptorEITShortEvent = 0x4D;
  static public final int DescriptorEITExtendedEvent = 0x4E;
  static public final int DescriptorEITGenre = 0x54; // called content_descriptor in spec
  static public final int DescriptorEITPDC = 0x69;
  static public final int DescriptorEITContentType = 0x50; // called component_descriptor in spec

  protected void parseEITDescriptors(int descrTag, BufferPart descrB)
  {
    try {
      switch (descrTag)
      {
      case DescriptorEITShortEvent:
        parseEITShortEventDescriptor(descrTag, descrB);
        break;
      case DescriptorEITExtendedEvent:
        parseEITShortEventDescriptor(descrTag, descrB);
        break;
      case DescriptorEITGenre:
        parseEITGenreDescriptor(descrTag, descrB);
        break;
      case DescriptorEITPDC:
        parseEITPDCDescriptor(descrTag, descrB);
        break;
      default:
        unknownDescriptor(descrTag, descrB);
      }
    } catch (Exception e) {
      Util.error("EIT descriptor content malformed", e);
    }
  }

  protected void parseEITShortEventDescriptor(int descrTag, BufferPart descrB)
  {
    try {
      assert (descrTag == DescriptorEITShortEvent);
      //xml("<programme>");
      //xmlIndent();
      parseLanguage(descrB.readString(3));
      int length = descrB.readInt8();
      xml("<!-- length " + length + " -->");       
      xml("<title>" + convertString(descrB.readString(length)) + "</title>"); 
      length = descrB.readInt8();
      xml("<!-- length " + length + " -->");       
      if (length > 0)
        xml("<desc>" + convertString(descrB.readString(length)) + "</desc>"); 
      //xmlOutdent();
      //xml("</programme>");
    } catch (Exception e) { Util.error("Event descriptor content malformed", e); }
  }

  protected void parseEITExtendedEventDescriptor(int descrTag, BufferPart descrB)
  {
    try {
      assert (descrTag == DescriptorEITExtendedEvent);
      int byte0 = descrB.readInt8();
      int sequenceNumber = byte0 >> 4; // upper 4 bits
      int lastSequenceNumer = byte0 & 0x31; // lower 4 bits
      String langCode = parseLanguage(descrB.readString(3));
      int length = descrB.readInt8();
      BufferPart itemsB = descrB.subBuffer(length);
      while (itemsB.currentPos() + 2 <= itemsB.endPos())
      {
        length = itemsB.readInt8();
        String itemDescr = convertString(itemsB.readString(length));
        length = itemsB.readInt8();
        String itemText = convertString(itemsB.readString(length));
        xml("<desc>" + itemDescr + "</desc>");
        xml("<text>" + itemText + "</text>");
      }
      length = itemsB.readInt8();
      String text = convertString(itemsB.readString(length));
      xml("<event-text>" + text + "</event-text>");
      // TODO what is the difference between item description, item text, and text???

    } catch (Exception e) { Util.error("Extended event descriptor content malformed", e); }
  }

  /**
   * PDC = Programme Delivery Control
   * Successor of VPS. Allows video recorders to record on time,
   * even if the programme is delayed etc..
   * This descriptor contains the PIL = Programme Identification Label,
   * an ID for the programme. It consists of the starttime of first publishing
   * of the programme.
   */
  protected void parseEITPDCDescriptor(int descrTag, BufferPart descrB)
  {
    try {
      assert (descrTag == DescriptorEITPDC);
      int byte0 = descrB.readInt8();
      int byte1 = descrB.readInt8();
      int byte2 = descrB.readInt8();
      // 4 bits reserved, 5 bits day, 4 bits month, 5 bits hour, 6 bits minute
      int day = (byte0 & 0xF) << 1 + byte1 >> 7;
      int month = byte1 >> 3 & 0xF;
      int hour = byte1 & 7 + byte2 >> 6;
      int minute = byte2 & 0x3F;
      xml("<pdc>" + descrB.contentAsHexString() + "</pdc>");
      //xml("<pdc pil=\"" + month + "-" + day + "T" + hour + ":" + minute + "\" />");
    } catch (Exception e) { Util.error("PDC descriptor content malformed", e); }
  }

  protected void parseEITContentTypeDescriptor(int descrTag, BufferPart descrB)
  {
    try {
      assert (descrTag == DescriptorEITContentType);
      int streamType = descrB.readInt8() & 0xF;
      int technicalType = descrB.readInt8();
      if (streamType == 1 || streamType == 5) // video MPEG2 and H.264
      {
        boolean h264 = streamType == 5;

        int videoTypeNum = (technicalType - 1) & 0x0F;
        // 2 bits aspect ratio, 1 bit 25Hz / 30 Hz, 1 bit SD / HD
        String aspectRatioName;
        switch (videoTypeNum >> 2)
        {
        case 0: aspectRatioName = "4:3"; break;
        case 1: aspectRatioName = "16:9-with-pan-vectors"; break; // euphemism for letterbox / black bars?
        case 2: aspectRatioName = "16:9"; break;
        case 3: aspectRatioName = "wider-than-16:9"; break;
        default: throw new RuntimeException("Not reached");
        }
        int hz = 25;
        if ((videoTypeNum >> 1 & 1) == 1)
          hz = 30;
        boolean hd = (videoTypeNum & 1) == 1;

        xml("<video contenttype=\"video/" + (h264 ? "h264" : "mpeg2")
                + (hd ? " hd=\"true\"" : "")
                + " aspect-ratio=\"" + aspectRatioName + "\""
                + " refresh-rate=" + hz + " />");
      }
      else if (streamType == 1) // audio
      {
        String channelsName = null;
        switch (technicalType)
        {
        case 1: channelsName = "mono"; break;
        case 2: channelsName = "mono-dual"; break;
        case 3: channelsName = "mono-stereo"; break;
        case 5: channelsName = "analog-surround"; break;
        }

        xml("<audio contenttype=\"audio/mpeg1/layer2\""
                + (channelsName != null ? " channels=\"" + channelsName + "\"" : "") + " />");
      }
      else if (streamType == 4) // AC3
      {
        xml("<audio contenttype=\"audio/ac3\" />");
      }
      else if (streamType == 6) // AAC
      {
        xml("<audio contenttype=\"audio/aac\" />");
      }
      else if (streamType == 3) // subtitles
      {
        String typeName = null;
        if (technicalType == 1)
          typeName = "subtitles/teletext";
        if ((technicalType & 0xF0) == 1)
          typeName = "subtitles/dvb";
        xml("<subtitles " + (typeName != null ? " contenttype=\"" + typeName + "\"" : "") + " />");
      }
    } catch (Exception e) { Util.error("Content type descriptor content malformed", e); }
  }

  // called content_descriptor in spec
  protected void parseEITGenreDescriptor(int descrTag, BufferPart descrBArray)
  {
    try {
      assert (descrTag == DescriptorEITGenre);
      for (BufferPart descrB : descrBArray.split(2))
      {
        byte byte0 = descrB.readInt8();
        //byte broadcasterDefined = descrB.readInt8();
        int showtype = byte0 >> 4; // higher 4 bits
        int genre = byte0 & 0x0F; // lower 4 bits
        String showtypeName = null;
        String genreName = null;
        if (showtype == 1) // movie
        {
          showtypeName = "movie";
          if (genre == 0) // movie/drama (general)
            ;
          else if (genre == 1) // detective / thriller
            genreName = tr("Thriller/Crime");
          else if (genre == 2) // adventure / western / war
            genreName = tr("Adventure");
          else if (genre == 3) // science fiction / fantasy / horror
            genreName = tr("SciFi/Fantasy/Horror");
          else if (genre == 4) // comedy
            genreName = tr("Comedy");
          else if (genre == 5) // soap / melodrama / folkloric
            genreName = tr("Soap");
          else if (genre == 6) // Roamance
            genreName = tr("Romance");
          else if (genre == 7) // serious / classical / religious / historical
            genreName = tr("Serious");
          else if (genre == 8) // Adult
            genreName = tr("Adult");
        }
        else if (showtype == 2) // news and documentary
        {
          //showtypeName = "news"; // TODO include documentary?
          if (genre == 0) // general
            ;
          else if (genre == 1) // news / wheater report
          {
            showtypeName = "news";
            genreName = tr("News");
          }
          else if (genre == 2) // magazine
            genreName = tr("News magazine");
          else if (genre == 3) // Documentary
            genreName = tr("Documentary");
          else if (genre == 4) // discussion / interview / debate
            genreName = tr("Discussion");
        }
        else if (showtype == 3) // show / game show
        {
          showtypeName = "show";
          if (genre == 0) // general
            ;
          else if (genre == 1) // game show / quiz / contest
            genreName = tr("Gameshow");
          else if (genre == 2) // Variety
            genreName = tr("Variety");
          else if (genre == 3) // talk show
            genreName = tr("Talk show");
        }
        else if (showtype == 4) // sports
        {
          showtypeName = "sports";
          if (genre == 0) // general
            ;
          else if (genre == 1) // special events: Olympic games, Soccer World Cup etc.
            genreName = tr("Special sport event");
          else if (genre == 2) // sports magazine
            genreName = tr("Sports magazine");
          else if (genre == 3) // Football / soccer
            genreName = tr("Football/Soccer");
          else if (genre == 4) // Tennis / squash
            genreName = tr("Tennis");
          // 5 = Team sport is useless
          else if (genre == 6) // Athletics
            genreName = tr("Athletics");
          else if (genre == 7) // Motor sport
            genreName = tr("Motor sport");
          else if (genre == 8) // Water sport
            genreName = tr("Water sport");
          else if (genre == 9) // Winter sport
            genreName = tr("Winter sport");
        }
        else if (showtype == 5) // children
        {
          showtypeName = "children";
          if (genre == 0) // general
            ;
          else if (genre == 1) // 0-6 years
            genreName = tr("Children 0-6 years");
          else if (genre == 2) // 6-14 years
            genreName = tr("Children 6-14 years");
          else if (genre == 3) // 10-16 years
            genreName = tr("Children 10-16 years");
          else if (genre == 4) // educational / school / informational
            genreName = tr("School");
          else if (genre == 5) // Cartoons / puppets
            genreName = tr("Cartoons");
        }
        else if (showtype == 6) // Music
        {
          showtypeName = "music";
          if (genre == 0) // general
            ;
          else if (genre == 1)
            genreName = tr("Pop music");
          else if (genre == 2)
            genreName = tr("Classical music");
          else if (genre == 3)
            genreName = tr("Folk music");
          else if (genre == 4)
            genreName = tr("Jazz");
          else if (genre == 5)
            genreName = tr("Opera/Musical");
          else if (genre == 6)
            genreName = tr("Ballet");
        }
        else if (showtype == 7) // Arts
        {
          genreName = tr("Arts/Culture");
        }
        else if (showtype == 8) // Social, politics, economics
        {
          genreName = tr("Social/Political/Economic");
        }
        else if (showtype == 9) // education, science, facts
        {
          genreName = tr("Education");
        }
        else if (showtype == 10) // hobbies
        {
          if (genre == 0) // general
            ;
          else if (genre == 1)
            genreName = tr("Travel");
          else if (genre == 2)
            genreName = tr("Handicraft hobby");
          else if (genre == 3)
            genreName = tr("Cars/motor hobby");
          else if (genre == 4)
            genreName = tr("Fitness/Health");
          else if (genre == 5)
            genreName = tr("Cooking");
          else if (genre == 6)
            genreName = tr("Advertisement");
          else if (genre == 7)
            genreName = tr("Garden");
        }
        else if (showtype == 11) // characteristics, not category
        {
          if (genre == 0) // original language
            xml("<lang original=\"true\" />"); // TODO abuse of <lang>
          else if (genre == 1) // black and white
            xml("<video><colour>no</colour></video>");
          else if (genre == 2) // unpublished
            xml("<new/><premiere/>"); // TODO which one of these? Correct meaning of "unpublished"?
          else if (genre == 2) // live
            xml("<live/>");
          continue; // It's not a showtype + genre 
        }
        if (showtypeName != null)
          xml("<showtype>" + showtypeName + "</showtype>");
        if (genreName != null)
          xml("<category>" + genreName + "</category>");
      }
    } catch (Exception e) { Util.error("Genre descriptor content malformed", e); }
  }

  /**
   * "Middle Julian Date", some freaky 16bit date format for dates between year 1900-2100
   * Spec: EN 300 468 Annex C
   */
  public Calendar convertMJD(int mjd)
  {
    // They are sick
    int year2 = truncateToInt((mjd - 15078.2) / 365.25);
    int month = truncateToInt((mjd - 14956.1 - truncateToInt(year2 * 365.25)) / 30.6001);
    int day = mjd - 14956 - truncateToInt(year2 * 365.25) - truncateToInt(month * 30.6001);
    int k = month == 14 || month == 15 ? 1 : 0; // nonsense (per spec): if month can be 15 here, it can end up as 13 
    year2 += k;
    month -= 1 + k * 12;
    int year = 1900 + year2; // 2009: year2 = 109

    /*
    int w = truncateToInt(mjd / 7 - 2144.64);
    int wy = truncateToInt(w * 28 / 1461 - 0.0079);
    int weekOfYear = w - truncateToInt(wy * 1461 / 28 + 0.41);
    int dayOfWeek = ((mjd + 2) % 7) + 1;
    */

    Calendar result = GregorianCalendar.getInstance();
    result.set(Calendar.YEAR, year);
    result.set(Calendar.MONTH, month);
    result.set(Calendar.DAY_OF_MONTH, day);
    return result;
  }
  // cuts everything after the decimal point, e.g. truncateToInt(Math.PI) = 3.
  static protected int truncateToInt(double d)
  {
    return (int) d;
  }

  static public void main(String[] commandlineArgs)
  {
    try {
      new CoreData();
      if (commandlineArgs.length < 2)
        throw new Exception("Need 2 commandline params. First param: card id. Second param: channelid or 'all' (for all known channels) or 'tuned' (for currently tuned channel, kept open by other app)");
      int cardid = Integer.parseInt(commandlineArgs[0]);
      DVBCard card = (DVBCard) CoreData.me.myCards.get(cardid);
      if ( ! (card instanceof DVBCard))
        throw new Exception("card id " + cardid + " is not a DVBCard");
      DVBEIT eit = new DVBEIT(card);
      eit.myOutputXML = true;
      if (commandlineArgs[1].equals("all"))
      {
        for (TVChannel ch : CoreData.me.myChannels.values())
          if (ch instanceof DVBChannel && card.canRecord(ch))
            eit.tuneAndReadEIT((DVBChannel) ch);
      }
      else if (commandlineArgs[1].equals("tuned"))
      {
        eit.readEITTuned();
      }
      else
      {
        int channelid = Integer.parseInt(commandlineArgs[1]);
        DVBChannel ch = (DVBChannel) CoreData.me.myChannels.get(channelid);
        eit.tuneAndReadEIT(ch);
      }
      Util.debug(eit.resultXML());
      System.exit(0);
    } catch (Exception e) {
      Util.error("Error", e);
      System.exit(1);
    }
  }
}
