/*
 * TV-Browser
 * Copyright (C) 04-2003 Martin Oberhauser (darras@users.sourceforge.net)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * CVS information:
 *  $RCSfile$
 *   $Source$
 *     $Date: 2014-06-11 19:49:51 +0200 (Mi, 11 Jun 2014) $
 *   $Author: ds10 $
 * $Revision: 8121 $
 */
package tvbrowser.core;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.logging.Level;
import java.util.logging.Logger;

import tvbrowser.core.data.OnDemandDayProgramFile;
import tvbrowser.ui.mainframe.MainFrame;
import tvdataservice.MutableChannelDayProgram;
import tvdataservice.MutableProgram;
import util.misc.SoftReferenceCache;
import devplugin.Channel;
import devplugin.ChannelDayProgram;
import devplugin.Date;
import devplugin.Program;
import devplugin.ProgramFieldType;

/**
 *
 *
 * @author Til Schneider, www.murfman.de
 */
public class TvDataBase {

  /** The logger for this class. */
  private static final Logger mLog = Logger
      .getLogger(TvDataBase.class.getName());

  private static final String INVENTORY_FILE = "tv-data-inventory.dat";

  /** The singleton. */
  private static TvDataBase mSingleton;

  /** The TV data cache. */
  private SoftReferenceCache<String, OnDemandDayProgramFile> mTvDataHash;

  private ArrayList<TvDataBaseListener> mListenerList;

  /** Contains date objects for each date for which we have a TV listing */
  private HashSet<Date> mAvailableDateSet;

  private Hashtable<String, UpdateData> mNewDayProgramsAfterUpdate;

  private TvDataInventory mTvDataInventory;
  private boolean mPendingPluginInformationAboutChangedData;
  private Hashtable<ChannelDayKey, UpdateData> mSendToTvDataListener;
  
  private MutableChannelDayProgram mCurrentAddedDayProgram;
  
  public static final int DEFAULT_DATA_LIFESPAN = 1;

  private TvDataBase() {
    mPendingPluginInformationAboutChangedData = false;
    mTvDataHash = new SoftReferenceCache<String, OnDemandDayProgramFile>();
    mListenerList = new ArrayList<TvDataBaseListener>();
    mAvailableDateSet = new HashSet<Date>();
    mNewDayProgramsAfterUpdate = new Hashtable<String, UpdateData>();
    mSendToTvDataListener = new Hashtable<ChannelDayKey, UpdateData>();
    updateAvailableDateSet();

/* Uncommend testwise.
 *
 * Now update of data base is started direct from TvDataUpdater to
 * make sure the data base is updated before all other TvDataUpdateListener.
 *
 *   TvDataUpdater.getInstance().addTvDataUpdateListener(
 *       new TvDataUpdateListener() {
 *         public void tvDataUpdateStarted() {}
 *
 *         public void tvDataUpdateFinished() {
 *           updateAvailableDateSet();
 *         }
 *       });
 */

    // Load inventory
    mTvDataInventory = new TvDataInventory();
    File file = new File(Settings.getUserSettingsDirName(), INVENTORY_FILE);
    if (file.exists()) {
      try {
        mTvDataInventory.readData(file);
      } catch (Exception exc) {
        mLog.log(Level.WARNING, "Loading TV data inventory failed", exc);
      }
    }
  }

  public static TvDataBase getInstance() {
    if (mSingleton == null) {
      mSingleton = new TvDataBase();
    }

    return mSingleton;
  }

  /**
   * Updates the TV data base.
   *
   * @since 2.2.2/2.5.1
   */
  protected void updateTvDataBase() {
    updateAvailableDateSet();
  }

  public void checkTvDataInventory(int lifespan) {
    // Get the channel of the subscribed channels
    Channel[] channelArr = ChannelList.getSubscribedChannels();
    String[] channelIdArr = new String[channelArr.length];
    for (int i = 0; i < channelArr.length; i++) {
      channelIdArr[i] = getChannelKey(channelArr[i]);
    }
    
    Date cutoff = Date.getCurrentDate().addDays(-lifespan);

    // Inventory prüfen
    boolean somethingChanged = false;

    File tvDataDir = new File(Settings.propTVDataDirectory.getString());
    File[] tvDataArr = tvDataDir.listFiles();
    if (tvDataArr == null) {
      return;
    }
    //store all files in a map for faster lookup
    HashMap<String,File> tvDataFiles=new HashMap<String,File>();
    for (File file : tvDataArr) {
		  tvDataFiles.put(file.getName(), file);
	  }

    // Check whether day programs were removed
    String[] knownProgArr = mTvDataInventory.getKnownDayPrograms();
    Date expireDate = Date.getCurrentDate().addDays(-1);
    for (String key : knownProgArr) {
      Date date = getDateFromFileName(key);
      // Check whether this file is still present AND not expired
      // (The key is equal to the file name)
      if (!tvDataFiles.containsKey(key)
          || (date != null && date.compareTo(expireDate) < 0)) {
        // This day program was deleted -> Inform the listeners

        // Get the channel and date
        Channel channel = getChannelFromFileName(key, channelArr, channelIdArr);
        if ((channel != null) && (date != null)) {
          if(date.compareTo(cutoff) >= 0) {
            mLog.info("Day program was deleted by third party: " + date + " on "
                + channel.getName());
          }
          
          ChannelDayProgram dummyProg = new MutableChannelDayProgram(date,
              channel);
          //validateProgramState(dummyProg, null);
          fireDayProgramTouched(dummyProg, null);
          fireDayProgramDeleted(dummyProg);
          
          mTvDataInventory.setUnknown(date, channel);
        }
        else {
          // we do not know the channel of this file -> remove from list of
          // remembered files
          mTvDataInventory.setUnknown(key);
        }
      }
    }

    // Check whether day programs were added or replaced
    for (File tvDataFile : tvDataArr) {
      String fileName = tvDataFile.getName();

      // Get the channel and date
      Channel channel = getChannelFromFileName(fileName, channelArr,
          channelIdArr);
      Date date = getDateFromFileName(fileName);
      if ((channel != null) && (date != null)) {
        // Get the version
        int version = (int) tvDataFile.length();

        // Check whether this day program is known
        int knownStatus = mTvDataInventory.getKnownStatus(date, channel,
            version);

        if ((knownStatus == TvDataInventory.UNKNOWN)
            || (knownStatus == TvDataInventory.OTHER_VERSION)) {
          // do not try to read very old unknown files
          if (isValidDate(date)) {
            if (!somethingChanged) {
              // This is the first changed day program -> fire update start
              TvDataUpdater.getInstance().fireTvDataUpdateStarted(Date.getCurrentDate().addDays(28));
            }

            // Inform the listeners
            mLog.info("Day program was changed by third party: " + date + " on "
                + channel.getName());
            //ChannelDayProgram newDayProg = getDayProgram(date, channel, false);
            OnDemandDayProgramFile newDayProg = getCacheEntry(date, channel, true, false);
            
            if (newDayProg != null && newDayProg.getDayProgram() != null) {
              handleKnownStatus(knownStatus, newDayProg, version);
            }

            somethingChanged = true;
          }
        }
      }
    }

    // fire update finished
    if (somethingChanged) {
      if(!MainFrame.isStarting()) {
        TvDataUpdater.getInstance().fireTvDataUpdateFinished();
      }
      else {
        mPendingPluginInformationAboutChangedData = true;
      }
    }
  }

  /**
   * Handles the pending plugin information if it is to do
   * after TV-Browser start is finished.
   * <p>
   * @since 2.7.2
   */
  public void handleTvBrowserStartFinished() {
    if(mPendingPluginInformationAboutChangedData) {
      mPendingPluginInformationAboutChangedData = false;

      Collection<UpdateData> dayPrograms = mNewDayProgramsAfterUpdate.values();

      for(UpdateData dayProgram : dayPrograms) {        
        if(dayProgram instanceof ChannelDayProgram) {
          fireDayProgramTouched(dayProgram.getRemoved(),dayProgram.getAdded().getDayProgram());
          fireDayProgramAdded(dayProgram.getAdded().getDayProgram());
        }
      }

      mNewDayProgramsAfterUpdate.clear();

      TvDataUpdater.getInstance().fireTvDataUpdateFinished();
    }
  }

  public void close(boolean log) {
    if (log) {
      mLog.info("Closing TV data base");
    }
    try {
      File file = new File(Settings.getUserSettingsDirName(), INVENTORY_FILE);
      mTvDataInventory.writeData(file);
    } catch (Exception exc) {
      if (log) {
        mLog.log(Level.WARNING, "Closing database failed", exc);
      }
    }
  }

  public void addTvDataListener(TvDataBaseListener listener) {
    synchronized (mListenerList) {
      mListenerList.add(listener);
    }
  }

  public void removeTvDataListener(TvDataBaseListener listener) {
    synchronized (mListenerList) {
      mListenerList.remove(listener);
    }
  }

  public ChannelDayProgram getDayProgram(Date date, Channel channel) {
    return getDayProgram(date, channel, false);
  }

  public void setDayProgramWasChangedByPlugin(Date date, Channel channel) {
    if(mCurrentAddedDayProgram != null && mCurrentAddedDayProgram.getDate().equals(date) && mCurrentAddedDayProgram.getChannel().equals(channel)) {
      mCurrentAddedDayProgram.setWasChangedByPlugin();
    }
  }

  private ChannelDayProgram getDayProgram(Date date, Channel channel, boolean update) {
    OnDemandDayProgramFile progFile = getCacheEntry(date, channel, true, update);

    if (progFile != null) {
      return progFile.getDayProgram();
    } else {
      return null;
    }
  }

  /**
   * Checks all TV-Data for missing length.
   *
   * @param days The number of days to recalculate
   * @param progressMonitor display status of the recalculation
   */
 /* public synchronized void reCalculateTvData(int days, ProgressMonitor progressMonitor) {
    Channel[] channels = ChannelList.getSubscribedChannels();

    progressMonitor.setMaximum(channels.length + 1);

    int value = 0;
    Date currentDate = Date.getCurrentDate();
    for (Channel channel : channels) {
      progressMonitor.setValue(value++);
      for (int i = -1; i < days; i++) {
        correctDayProgramFile(currentDate.addDays(i), channel);
      }
    }
    mNewDayProgramsAfterUpdate.clear();
  }*/
  
  public synchronized void reCalculateTvData(Channel channel, Date date) {
    correctDayProgramFile(date, channel);
  }

  public synchronized void setDayProgram(MutableChannelDayProgram prog) {
    Date date = prog.getDate();
    Channel channel = prog.getChannel();
    String key = getDayProgramKey(date, channel);

    if(prog.getProgramCount() > 0) {
      prog.setLastProgramHadEndOnUpdate(prog.getProgramAt(prog.getProgramCount() - 1).getLength() > 0);
    }
    
    OnDemandDayProgramFile oldProgFile = getCacheEntry(date, channel, true, false);
    
    // Create a backup (Rename the old file if it exists)
    File file = getDayProgramFile(date, channel);
    File backupFile = null;
    
    if (file.exists()) {
      backupFile = new File(file.getAbsolutePath() + ".backup");
      if (!file.renameTo(backupFile)) {
        // Renaming failed -> There will be no backup
        backupFile = null;
      }
    }
    
    // Invalidate the old program file from the cache
    if (oldProgFile != null) {
      oldProgFile.setValid(false);

      // Remove the old entry from the cache (if it exists)
      removeCacheEntry(key);
    }

    // Create a new program file
    OnDemandDayProgramFile newProgFile = new OnDemandDayProgramFile(file, prog);

    // Put the new program file in the cache
    addCacheEntry(key, newProgFile);

    // Inform the listeners about adding the new program
    // NOTE: This must happen before saving to give the listeners the chance to
    // change the data and have those changes saved to disk.
    //fireDayProgramAdded(prog);

    // Save the new program
    try {
      // Save the day program
      newProgFile.saveDayProgram();
      
      
      // Delete the backup
      if (backupFile != null) {
        backupFile.delete();
      }

      // save the value for informing the listeners later
      if(oldProgFile != null) {
        mNewDayProgramsAfterUpdate.put(key, new UpdateData(newProgFile, oldProgFile.getDayProgram()));
      } else {
        mNewDayProgramsAfterUpdate.put(key, new UpdateData(newProgFile, null));
      }

      // Set the new program to 'known'
      int version = (int) file.length();
      mTvDataInventory.setKnown(date, channel, version);
    } catch (IOException exc) {
      // Remove the new program from the cache
      removeCacheEntry(key);

      // Remove the program from the later update list
      mNewDayProgramsAfterUpdate.remove(key);

      // Inform the listeners about removing the new program
      //validateProgramState(prog,null);
      fireDayProgramTouched(prog, null);
      fireDayProgramDeleted(prog);

      // Try to restore the backup
      boolean restoredBackup = false;
      if (backupFile != null) {
        if (backupFile.renameTo(file)) {
          restoredBackup = true;
        } else {
          backupFile.delete();
        }
      }

      // Log the error
      String msg = "Saving program for " + channel + " from " + date
          + " failed.";
      if (restoredBackup) {
        msg += " The old version was restored.";
      }
      mLog.log(Level.WARNING, msg, exc);
    }
  }

  private OnDemandDayProgramFile getCacheEntry(Date date,
      Channel channel, boolean loadFromDisk, boolean update) {
    if(!loadFromDisk) {
      String key = getDayProgramKey(date, channel);

      //Try to get the program from the cache
      OnDemandDayProgramFile progFile =  mTvDataHash.get(key);

      if(progFile != null) {
        return progFile;
      }
    }

    return getCacheEntryBlocking(date,channel,loadFromDisk,update);
  }


  private synchronized OnDemandDayProgramFile getCacheEntryBlocking(Date date,
      Channel channel, boolean loadFromDisk, boolean update) {
    String key = getDayProgramKey(date, channel);

    // Try to get the program from the cache
    OnDemandDayProgramFile progFile =  mTvDataHash.get(key);

    // Try to load the program from disk
    if (loadFromDisk && (progFile == null || (update && progFile.isTimeLimitationData()))) {
      progFile = loadDayProgram(date, channel, update);
      if (progFile != null && !update) {
        addCacheEntry(key, progFile);
      }
    }

    return progFile;
  }

  private synchronized void addCacheEntry(String key,
      OnDemandDayProgramFile progFile) {
    mTvDataHash.put(key, progFile);
  }

  private synchronized void removeCacheEntry(String key) {
    mTvDataHash.remove(key);
  }

  protected static String getDayProgramKey(Date date, Channel channel) {
    // check the input
    if (date == null) {
      throw new NullPointerException("date is null");
    }

    return new StringBuilder(getChannelKey(channel)).append('.').append(
        date.getDateString()).toString();
  }

  private static String getChannelKey(Channel channel) {
    // check the input
    if (channel == null) {
      throw new NullPointerException("channel is null");
    }

    return new StringBuilder(channel.getBaseCountry()).append('_').append(
        channel.getId()).append('_').append(channel.getDataServicePackageName()).toString();
  }

  public boolean isDayProgramAvailable(Date date, Channel channel) {
    File file = getDayProgramFile(date, channel);
    return file.exists();
  }

  /**
   * Deletes expired tvdata files older then lifespan days.
   *
   * @param lifespan
   *          The number of days to delete from the past
   * @param informPlugins If the plugins should be informed about the deleting.
   */
  public void deleteExpiredFiles(int lifespan, boolean informPlugins) {
    if (lifespan < 0) {
      return; // manually
    }
    
    Channel[] channelArr = ChannelList.getSubscribedChannels();
    
    // remove the dates from the "available" set
    Date date = Date.getCurrentDate().addDays(-lifespan);
    while (dataAvailable(date)) {
      mAvailableDateSet.remove(date);
      date = date.addDays(-1);
    }

    // search files
    final Date d = Date.getCurrentDate().addDays(-lifespan);

    FilenameFilter filter = new FilenameFilter() {
      public boolean accept(File dir, String name) {
        int p = name.lastIndexOf('.');
        String s = name.substring(p + 1, name.length());
        for (int i = 0; i < s.length(); i++) {
          if (! Character.isDigit(s.charAt(i))) {
            return false;
          }
        }
        int val;
        try {
          val = Integer.parseInt(s);
        } catch (NumberFormatException e) {
          return false;
        }
        int year = val / 10000;
        int r = val % 10000;
        int month = r / 100;
        int day = r % 100;
        Date curDate = new Date(year, month, day);
        return curDate.getValue() < d.getValue();
      }
    };

    // Get the subscribed channels
    String[] channelIdArr = new String[channelArr.length];
    for (int i = 0; i < channelArr.length; i++) {
      channelIdArr[i] = getChannelKey(channelArr[i]);
    }

    deleteFiles(informPlugins, filter, channelArr, channelIdArr);
    updateAvailableDateSet();
  }

  /**
   * @param informPlugins
   * @param filter
   * @param channelArr
   * @param channelIdArr
   * @return whether a data finished callback was triggered during file deletion
   */
  private boolean deleteFiles(boolean informPlugins, FilenameFilter filter,
      Channel[] channelArr, String[] channelIdArr) {
    File[] fileList = new File(Settings.propTVDataDirectory.getString())
        .listFiles(filter);
    boolean somethingDeleted = false;

    if (fileList != null && fileList.length > 0) {
      somethingDeleted = true;
      for (File deleteFile : fileList) {
        Channel ch = getChannelFromFileName(deleteFile.getName(), channelArr, channelIdArr);
        Date date = getDateFromFileName(deleteFile.getName());

        if(ch != null && date != null) {
          if(informPlugins) {
            ChannelDayProgram dayProgram = getDayProgram(date, ch);
            if (dayProgram != null) {
              //validateProgramState(dayProgram, null);
              fireDayProgramTouched(dayProgram, null);
              fireDayProgramDeleted(dayProgram);
            }
          }

          removeCacheEntry(getDayProgramKey(date,ch));
        }

        deleteFile.delete();
      }
    }

    if(informPlugins && somethingDeleted) {
      TvDataUpdater.getInstance().fireTvDataUpdateFinished();
      return true;
    }
    return false;
  }

  private synchronized void correctDayProgramFile(Date date,
      Channel channel) {
    boolean verbose = Settings.propVerboseLogging.getBoolean();
    
    if(verbose) {
      mLog.info(new java.util.Date(System.currentTimeMillis()) + ": CorrectDayProgramFile " + date + " " + channel);
    }
    
    File file = getDayProgramFile(date, channel);
    String key = getDayProgramKey(date, channel);
    if (!file.exists()) {
      return;
    }

    try {
      // Check whether this day program is known
      int version = (int) file.length();
      int knownStatus = mTvDataInventory.getKnownStatus(date, channel, version);

      UpdateData updateData = mNewDayProgramsAfterUpdate.remove(key);
      
      OnDemandDayProgramFile checkProg = null;
      
      if(updateData != null) {
        checkProg = updateData.getAdded();//getCacheEntry(date, channel, false, false);
      }
      else {
        checkProg = getCacheEntry(date, channel, true, true);//getCacheEntry(date, channel, false, false);
      }
      
      mCurrentAddedDayProgram = checkProg.getDayProgram();

      if(verbose) {
        mLog.info(new java.util.Date(System.currentTimeMillis()) + ": START calculate missing length");
      }
      
      boolean somethingChanged = calculateMissingLengths(mCurrentAddedDayProgram);
      
      if(verbose) {
        mLog.info(new java.util.Date(System.currentTimeMillis()) + ": END calculate missing length");
        mLog.info(new java.util.Date(System.currentTimeMillis()) + ": FIRE DAY PROGRAM ADDED");
      }
      
      // fire day program added to give plugins a chance to change programs
      fireDayProgramAdded(mCurrentAddedDayProgram);
      /*if((oldProg = mNewDayProgramsAfterUpdate.remove(key)) != null) {
        // Inform the listeners about deleting the old program
        if (oldProg instanceof ChannelDayProgram) {
          fireDayProgramDeleted((ChannelDayProgram)oldProg);
        }

        // Inform the listeners about adding the new program
        fireDayProgramAdded(checkProg);
      }
      else if(somethingChanged){
        fireDayProgramAdded(checkProg);
      }*/
      
      if(verbose) {
        mLog.info(new java.util.Date(System.currentTimeMillis()) + ": SOMETHING CHANGED: " + somethingChanged);
      }
      
      if (mCurrentAddedDayProgram.getAndResetChangedByPluginState() || somethingChanged) {
        // Some missing lengths could now be calculated
        // -> Try to save the changes

        // We use a temporary file. If saving succeeds we rename it
        File tempFile = new File(file.getAbsolutePath() + ".changed");
        try {
          // Try to save the changed program
          OnDemandDayProgramFile newProgFile = new OnDemandDayProgramFile(
              tempFile, mCurrentAddedDayProgram);
          newProgFile.saveDayProgram();

          // Saving the changed version succeed -> Delete the original
          file.delete();

          // Use the changed version now
          tempFile.renameTo(file);

          // If the old version was known -> Set the new version to known too
          if (knownStatus == TvDataInventory.KNOWN) {
            version = (int) file.length();
            mTvDataInventory.setKnown(date, channel, version);
          }
        } catch (Exception exc) {
          // Saving the changes failed
          // -> remove the temp file and keep the old one
          tempFile.delete();
        }

        // We have to load the file again to get the new data positions
        OnDemandDayProgramFile progFile = new OnDemandDayProgramFile(file, date, channel);
        progFile.loadDayProgram(false);

        // Invalidate the old program file from the cache
        if (checkProg != null) {
          checkProg.setValid(false);

          // Remove the old entry from the cache (if it exists)
          removeCacheEntry(key);
        }
        
        updateData = new UpdateData(progFile, updateData != null ? updateData.getRemoved() : null);
        
        // Put the new program file in the real cache
        addCacheEntry(key, progFile);
      } else if(checkProg != null) {
        checkProg.calculateTimeLimits();
      }

      // Inform the listeners about adding the new program
      if(updateData != null || somethingChanged) {
        ChannelDayKey dayKey = new ChannelDayKey(channel, date);
//        OnDemandDayProgramFile dayProgramFile = getCacheEntry(date, channel, true, false);
                
//        if(oldProg instanceof ChannelDayProgram) {
          mSendToTvDataListener.put(dayKey, updateData);
  /*      }
        else {
          mSendToTvDataListener.put(dayKey, "dummy");
        }*/
      }
    } catch (Exception exc) {
      mLog.log(Level.WARNING, "Loading program for " + channel + " from "
          + date + " failed. The file will be deleted...", exc);

      file.delete();
    }
    
    mCurrentAddedDayProgram = null;
  }


  private synchronized OnDemandDayProgramFile loadDayProgram(Date date,
      Channel channel, boolean update) {
    File file = getDayProgramFile(date, channel);
    if (!file.exists()) {
      return null;
    }

    try {
      // Load the program file
      OnDemandDayProgramFile progFile = new OnDemandDayProgramFile(file, date,
          channel);
      progFile.loadDayProgram(update);

      return progFile;
    } catch (Exception exc) {
      mLog.log(Level.WARNING, "Loading program for " + channel + " from "
          + date + " failed. The file will be deleted...", exc);

      file.delete();

      return null;
    }
  }

  private File getDayProgramFile(Date date, Channel channel) {
    String fileName = getDayProgramKey(date, channel);

    String tvDataDir = Settings.propTVDataDirectory.getString();
    return new File(tvDataDir, fileName);
  }

  private Channel getChannelFromFileName(String fileName, Channel[] channelArr,
      String[] channelIdArr) {
    for (int i = 0; i < channelIdArr.length; i++) {
      if (fileName.startsWith(channelIdArr[i])) {
        return channelArr[i];
      }
    }

    return null;
  }

  private Date getDateFromFileName(String fileName) {
    int dotIdx = fileName.lastIndexOf('.');
    if (dotIdx == -1) {
      return null;
    }

    String valueAsString = fileName.substring(dotIdx + 1);
    for (int i=0; i < valueAsString.length(); i++) {
      if (!Character.isDigit(valueAsString.charAt(i))) {
        return null;
      }
    }
    try {
      long value = Long.parseLong(valueAsString);
      return Date.createDateFromValue(value);
    } catch (NumberFormatException exc) {
      return null;
    }
  }

  /**
   * Returns true, if TV data is available on disk for the given date.
   *
   * @param date
   *          The date to check.
   * @return if the data is available.
   */
  public boolean dataAvailable(Date date) {
    if (date == null) {
      return false;
    }
    return mAvailableDateSet.contains(date);
  }

  private void updateAvailableDateSet() {

    String tvDataDirStr = Settings.propTVDataDirectory.getString();
    File tvDataDir = new File(tvDataDirStr);
    if (!tvDataDir.exists()) {
      return;
    }

    FilenameFilter filter = new FilenameFilter() {
      public boolean accept(File dir, String name) {
        if (name.length() < 8) {
          return false;
        }
        String dateStr = name.substring(name.length() - 8);
        for (int i = 0; i< dateStr.length(); i++) {
          if (! Character.isDigit(dateStr.charAt(i))) {
            return false;
          }
        }
        try {
          Integer.parseInt(dateStr.substring(0, 4)); // year
          Integer.parseInt(dateStr.substring(4, 6)); // month
          Integer.parseInt(dateStr.substring(6, 8)); // day
        } catch (NumberFormatException e) {
          return false;
        }
        return true;
      }
    };

    String[] fileNameList = tvDataDir.list(filter);
    if (fileNameList != null) {
      for (String fileName : fileNameList) {
        if (fileName.length() > 8) {
          String dateStr = fileName.substring(fileName.length() - 8);
          try {
            int year = Integer.parseInt(dateStr.substring(0, 4));
            int month = Integer.parseInt(dateStr.substring(4, 6));
            int day = Integer.parseInt(dateStr.substring(6, 8));
            mAvailableDateSet.add(new devplugin.Date(year, month, day));
          } catch (NumberFormatException e) {}
        }
      }
    }

  }

  private void fireDayProgramAdded(MutableChannelDayProgram prog) {
    synchronized (mListenerList) {
      for (int i = 0; i < mListenerList.size(); i++) {
        TvDataBaseListener lst = mListenerList.get(i);
        lst.dayProgramAdded(prog);
      }
    }
  }

  private void fireDayProgramTouched(ChannelDayProgram removedDayProgram, ChannelDayProgram addedDayProgram) {
    synchronized (mListenerList) {
      for (int i = 0; i < mListenerList.size(); i++) {
        TvDataBaseListener lst = mListenerList.get(i);
        lst.dayProgramTouched(removedDayProgram,addedDayProgram);
      }
    }
  }

  private void fireDayProgramAdded(ChannelDayProgram prog) {
    synchronized (mListenerList) {
      for (int i = 0; i < mListenerList.size(); i++) {
        TvDataBaseListener lst = mListenerList.get(i);
        lst.dayProgramAdded(prog);
      }
    }
  }

  private void fireDayProgramDeleted(ChannelDayProgram prog) {
    synchronized (mListenerList) {
      for (int i = 0; i < mListenerList.size(); i++) {
        TvDataBaseListener lst = mListenerList.get(i);
        lst.dayProgramDeleted(prog);
      }
    }
  }

  private void handleKnownStatus(int knownStatus, OnDemandDayProgramFile newDayProgFile,
      int version) {
    if (knownStatus != TvDataInventory.KNOWN) {
      MutableChannelDayProgram newDayProg = newDayProgFile.getDayProgram();
      
      Date date = newDayProg.getDate();
      Channel channel = newDayProg.getChannel();

      if (knownStatus == TvDataInventory.OTHER_VERSION) {
        // The day program was replaced -> fire a deleted event
        // (And later an added event)

        // Since we don't have the old day program we use a dummy program
        ChannelDayProgram dayProg = new MutableChannelDayProgram(date, channel);
        //validateProgramState(dayProg, null);
        fireDayProgramTouched(dayProg, null);
        fireDayProgramDeleted(dayProg);
      }

      // Set the day program to 'known'
      mTvDataInventory.setKnown(date, channel, version);

      // The day program is new -> fire an added event
      // save the value for informing the listeners later
      if (isValidDate(date)) {
        mNewDayProgramsAfterUpdate.put(getDayProgramKey(date, channel),
            new UpdateData(newDayProgFile, null));
      }
    }
  }

  /**
   * a day program is only valid, if it is from yesterday or newer
   *
   * @param date
   * @return
   */
  private boolean isValidDate(final Date date) {
    return (Date.getCurrentDate().getNumberOfDaysSince(date) <= 1);
  }

  /**
   * Checks whether all programs have a length. If not, the length will be
   * calculated.
   *
   * @param channelProg
   *          The day program to calculate the lengths for.
   * @return <code>true</code> when at least one length was missing.
   */
  private boolean calculateMissingLengths(ChannelDayProgram channelProg) {
    boolean somethingChanged = false;
    // Go through all programs and correct them
    // (This is fast, if no correction is needed)
    for (int progIdx = 0; progIdx < channelProg.getProgramCount(); progIdx++) {
      Program program = channelProg.getProgramAt(progIdx);
      if (!(program instanceof MutableProgram)) {
        continue;
      }

      MutableProgram prog = (MutableProgram) program;

      if (prog.getLength() <= 0) {
        // Try to get the next program
        Program nextProgram = null;
        int addCount = 1;
        
        // If next program and current program have the same start time, find next program that has another start time.
        do {
          if ((progIdx + addCount) < channelProg.getProgramCount()) {
            // Try to get it from this ChannelDayProgram
            nextProgram = channelProg.getProgramAt(progIdx + addCount++);
          } else {
            // This is the last program -> Try to get the first program of the
            // next ChannelDayProgram
            nextProgram = getFirstNextDayProgram(channelProg);
            break;
          }
        }while(nextProgram != null && nextProgram.getStartTime() == prog.getStartTime());

        somethingChanged = calculateLength(prog, nextProgram) || somethingChanged;
      } else if (progIdx + 1 == channelProg.getProgramCount() && !channelProg.getLastProgramHadEndOnUpdate()) {
        // This is the last program that has a length but it could be wrong.
        somethingChanged = calculateLength(prog,
            getFirstNextDayProgram(channelProg)) || somethingChanged;
      }
    }

    return somethingChanged;
  }

  private Program getFirstNextDayProgram(ChannelDayProgram channelProg) {
    Date nextDate = channelProg.getDate().addDays(1);
    Channel channel = channelProg.getChannel();
    TvDataBase db = TvDataBase.getInstance();
    ChannelDayProgram nextDayProg = db.getDayProgram(nextDate, channel, true);

    if ((nextDayProg != null) && (nextDayProg.getProgramCount() > 0)) {
      return (nextDayProg.getProgramAt(0));
    }

    return null;
  }

  private boolean calculateLength(MutableProgram first, Program second) {
    // Calculate the Length
    if (second != null) {
      int startTime = first.getHours() * 60 + first.getMinutes();
      int endTime = second.getHours() * 60 + second.getMinutes();
      if (endTime < startTime) {
        // The program ends the next day
        endTime += 24 * 60;
      }

      if ((startTime + first.getLength()) != endTime) {
        int length = endTime - startTime;
        
        if(first.hasFieldValue(ProgramFieldType.NET_PLAYING_TIME_TYPE) && first.getIntField(ProgramFieldType.NET_PLAYING_TIME_TYPE) > length) {
          length = first.getIntField(ProgramFieldType.NET_PLAYING_TIME_TYPE);
        }
        
        // Only allow a maximum length of 15 hours
        if (length < 15 * 60) {
          first.setLength(length);
          return (true);
        }
      }
    }
    return false;
  }

  public void unsubscribeChannels(final Channel[] channels) {
    boolean callback = false;
    for (Channel channel : channels) {
      // only trigger the plugin update after the last channel
      if (unsubscribeChannel(channel, channel == channels[channels.length - 1])) {
        callback = true;
      }
    }
    // run the callback manually
    if (!callback) {
      TvDataUpdater.getInstance().fireTvDataUpdateFinished();
    }
  }

  /**
   * delete all program files of a channel after unsubscribing it
   *
   * @param channel
   * @return whether a data update finished callback was triggered or not
   */
  private boolean unsubscribeChannel(final Channel channel, final boolean informPlugins) {
    if (channel == null) {
      return false;
    }
    final Channel[] channelArr = new Channel[] { channel };
    final String[] channelIdArr = new String[] { getChannelKey(channel) };

    // get all files of this channel, independent of the date (but they still
    // must have a date)
    FilenameFilter filter = new FilenameFilter() {

      @Override
      public boolean accept(File dir, String name) {
        Channel ch = getChannelFromFileName(name, channelArr, channelIdArr);
        Date date = getDateFromFileName(name);

        return ch != null && date != null;
      }
    };

    // delete all channel files
    return deleteFiles(informPlugins, filter, channelArr, channelIdArr);
  }

  /**
   * the maximum date any part of TV-Browser needs to be able to handle
   * @return today plus 4 weeks
   */
  public Date getMaxSupportedDate() {
    return Date.getCurrentDate().addDays(28);
  }
  
  /*private void validateProgramState(ChannelDayProgram oldChannelDayProg, ChannelDayProgram newChannelDayProg) {
    if(oldChannelDayProg != null) {
      Iterator<Program> programs = oldChannelDayProg.getPrograms();
      
      while(programs.hasNext()) {
        Program oldProg = programs.next();
        Program newProg = null;
        
        if(newChannelDayProg != null) {
          newProg = newChannelDayProg.getProgram(oldProg.getID());
        }
        
        if(!((MutableProgram)oldProg).checkStateAgainst(newProg)) {
          ((MutableProgram)oldProg).checkStateAgainst(oldProg);
        }
      }
    }
  }*/
  
  void sendNewProgramsToTvDataListener() {
    boolean verbose = Settings.propVerboseLogging.getBoolean();
    Enumeration<ChannelDayKey> keys = mSendToTvDataListener.keys();
    
    while(keys.hasMoreElements()) {
      ChannelDayKey key = keys.nextElement();
      
      if(verbose) {
        mLog.info(new java.util.Date(System.currentTimeMillis()) + ": SEND TO PLUGINS " + key);
      }
      
      UpdateData updateData = mSendToTvDataListener.remove(key);
      //Object oldProg = mSendToTvDataListener.remove(key);
    
    //  OnDemandDayProgramFile dayProgramFile = updateData.getAdded();//getCacheEntry(key.getDate(), key.getChannel(), true, false);
    
    //  ChannelDayProgram dayProgram = dayProgramFile.getDayProgram();
      
      if(updateData.getRemoved() != null) {
        //validateProgramState((ChannelDayProgram)oldProg,dayProgram);
        fireDayProgramTouched(updateData.getRemoved(),updateData.getAdded().getDayProgram()/*dayProgram*/);
        fireDayProgramDeleted(updateData.getRemoved());
      }
      else {
        fireDayProgramTouched(null,updateData.getAdded().getDayProgram()/*dayProgram*/);
      }
  
      fireDayProgramAdded((ChannelDayProgram)updateData.getAdded().getDayProgram()/*dayProgram*/);
    }
  }
  
  private class ChannelDayKey {
    private Channel mChannel;
    private Date mDate;
    
    public ChannelDayKey(Channel channel, Date date) {
      mChannel = channel;
      mDate = date;
    }
    
    public Channel getChannel() {
      return mChannel;
    }
    
    public Date getDate() {
      return mDate;
    }
    
    public String toString() {
      return mDate + " " + mChannel;
    }
  }
  
  private static class UpdateData {
    private OnDemandDayProgramFile mAdded;
    private ChannelDayProgram mRemoved;
    
    public UpdateData(OnDemandDayProgramFile added, ChannelDayProgram removed) {
      mAdded = added;
      mRemoved = removed;
    }
    
    public OnDemandDayProgramFile getAdded() {
      return mAdded;
    }
    
    public ChannelDayProgram getRemoved() {
      return mRemoved;
    }
  }
}
