/*   **********************************************************************  **
 **   Copyright notice                                                       **
 **                                                                          **
 **   (c) 2003-2006 RSSOwl Development Team                                  **
 **   http://www.rssowl.org/                                                 **
 **                                                                          **
 **   All rights reserved                                                    **
 **                                                                          **
 **   This program and the accompanying materials are made available under   **
 **   the terms of the Eclipse Public License 1.0 which accompanies this     **
 **   distribution, and is available at:                                     **
 **   http://www.rssowl.org/legal/epl-v10.html                               **
 **                                                                          **
 **   A copy is found in the file epl-v10.html and important notices to the  **
 **   license from the team is found in the textfile LICENSE.txt distributed **
 **   in this package.                                                       **
 **                                                                          **
 **   This copyright notice MUST APPEAR in all copies of the file!           **
 **                                                                          **
 **   Contributors:                                                          **
 **     RSSOwl - initial API and implementation (bpasero@rssowl.org)         **
 **                                                                          **
 **  **********************************************************************  */

package net.sourceforge.rssowl.util.archive;

import net.sourceforge.rssowl.controller.GUI;
import net.sourceforge.rssowl.dao.NewsfeedFactoryException;
import net.sourceforge.rssowl.dao.feedparser.FeedParser;
import net.sourceforge.rssowl.model.Channel;
import net.sourceforge.rssowl.util.GlobalSettings;
import net.sourceforge.rssowl.util.shop.XMLShop;

import org.jdom.Document;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;

/**
 * Cache manager for RSSOwl. Any loaded newsfeed is stored in the cache folder
 * and indexed in the newsFeedCache Hashtable. That index is serialized into a
 * file and loaded on startup of RSSOwl. Two Hashtables manage the caching. The
 * live cache contains Channel objects withe the URL as key, whereas the local
 * cache contains File objects which point to the cached file where the newsfeed
 * is stored.
 * 
 * @author <a href="mailto:bpasero@rssowl.org">Benjamin Pasero </a>
 * @version 1.2.3
 */
public class FeedCacheManager {

  /** File to serialize the cache index into */
  private static final String CACHE_INDEX = "index.obj";

  /** Max time to live for unmodified cache files (2 days) */
  private static final long FILE_TIME_TO_LIVE = 2 * 24 * 3600 * 1000;

  /** Live cache will store RSSChannels */
  private Hashtable newsFeedLiveCache;

  /** Local cache will store File reference to cached files */
  private Hashtable newsFeedLocalCache;

  /**
   * Instantiate a new FeedCacheManager
   */
  public FeedCacheManager() {
    newsFeedLiveCache = new Hashtable();

    /** Load the local cache */
    deserializeCache();

    /** Clean-Up the local cache */
    cleanUpCache();
  }

  /**
   * Cache a newsfeed
   * 
   * @param url Unique identifier
   * @param rssChannel The newsfeed to cache
   */
  public void cacheNewsfeed(String url, Channel rssChannel) {

    /** Channel might be NULL due to connection timeout */
    if (rssChannel == null)
      return;

    /** Free the Document for Garbage Collection in Channel */
    Document document = rssChannel.getDocument();
    rssChannel.setDocument(null);

    /** First delete old cache entry */
    if (isNewsfeedCached(url))
      unCacheNewsfeed(url, true);

    /** Cache Channel in live cache */
    newsFeedLiveCache.put(url, rssChannel);

    /** Do not write newsfeed into file, return */
    if (!GlobalSettings.localCacheFeeds)
      return;

    /** Save reference to the cache file into local cache */
    try {

      /** Channel might be an aggregated category which has no document set */
      if (document != null) {
        File cachedFeed = saveDocument(document);
        newsFeedLocalCache.put(url, cachedFeed);
      }
    } catch (IOException e) {
      GUI.logger.log("cacheNewsfeed", e);
    }
  }

  /**
   * Get a cached newsfeed from the feed cache
   * 
   * @param url Unique identifier
   * @return Channel The cached newsfeed
   */
  public Channel getCachedNewsfeed(String url) {

    /** Check if feed is cached */
    if (!isNewsfeedCached(url))
      return null;

    /** First try to load the feed from the live cache */
    if (newsFeedLiveCache.containsKey(url))
      return (Channel) newsFeedLiveCache.get(url);

    /** Init SAX builder */
    SAXBuilder builder = new SAXBuilder("org.apache.xerces.parsers.SAXParser");
    XMLShop.setDefaultEntityResolver(builder);

    /** Second try: Load feed from local cache */
    try {
      File cachedFeed = (File) newsFeedLocalCache.get(url);
      Document document = builder.build(cachedFeed);

      /** Parse the document */
      FeedParser parser = new FeedParser(document, url);
      parser.parse();
      return parser.getChannel();
    }

    /** In any case of an error return NULL */
    catch (IOException e) {
      return null;
    } catch (JDOMException e) {
      return null;
    } catch (NewsfeedFactoryException e) {
      return null;
    } catch (IllegalArgumentException e) {
      return null;
    }
  }

  /**
   * Check if a newsfeed is cached by a unique identifier in either the live
   * cache (memory) or local (file)
   * 
   * @param url Unique identifier
   * @return boolean TRUE if newsfeed is cached
   */
  public boolean isNewsfeedCached(String url) {
    return isNewsfeedCached(url, true);
  }

  /**
   * Check if a newsfeed is cached by a unique identifier in either the live
   * cache (memory) and local (file) is allowLocalCache is set to TRUE
   * 
   * @param url Unique identifier
   * @param allowLocalCache If TRUE also look in local file cache
   * @return boolean TRUE if newsfeed is cached
   */
  public boolean isNewsfeedCached(String url, boolean allowLocalCache) {
    if (url == null)
      return false;
    return (newsFeedLiveCache.containsKey(url) || (allowLocalCache && newsFeedLocalCache.containsKey(url)));
  }

  /**
   * Serialize the index on application exit and cleanup old chache items
   */
  public void shutdown() {

    /** In case user has not switched caching off */
    if (GlobalSettings.localCacheFeeds)
      serializeCache();

    /** Cache no longer used, delete it */
    else
      deleteCache();
  }

  /**
   * Remove a newsfeed from the cache and delete its cache file
   * 
   * @param url Unique identifier
   * @param deleteLocalCache If TRUE, also uncache local file
   */
  public void unCacheNewsfeed(String url, boolean deleteLocalCache) {

    /** Remove from live cache */
    newsFeedLiveCache.remove(url);

    /** Delete from local cache if required */
    if (deleteLocalCache && newsFeedLocalCache.containsKey(url)) {
      File file = (File) newsFeedLocalCache.get(url);
      file.delete();
      newsFeedLocalCache.remove(url);
    }
  }

  /**
   * Remove those files from the archive which were not modified over a long
   * period
   */
  private void cleanUpCache() {
    Enumeration keys = newsFeedLocalCache.keys();
    ArrayList indexedFiles = new ArrayList();
    ArrayList removeKeys = new ArrayList();

    /** Foreach feed cache */
    while (keys.hasMoreElements()) {
      String url = (String) keys.nextElement();
      File cachedFile = (File) newsFeedLocalCache.get(url);
      indexedFiles.add(cachedFile.getName());

      /** In case the file does not exist delete it from the index */
      if (!cachedFile.exists())
        removeKeys.add(url);

      /** Un-Cache the file if it was not modified for some time */
      if ((System.currentTimeMillis() - cachedFile.lastModified()) > FILE_TIME_TO_LIVE) {
        unCacheNewsfeed(url, false);
        removeKeys.add(url);
        cachedFile.delete();
      }
    }

    /** Delete "lost" indices, which are not referenced to existing files */
    for (int a = 0; a < removeKeys.size(); a++)
      newsFeedLocalCache.remove(removeKeys.get(a));

    /** Delete "lost" files, which are no longer on the index. */
    File cachedFiles[] = new File(GlobalSettings.CACHE_DIR).listFiles();
    for (int a = 0; a < cachedFiles.length; a++) {
      String fileName = cachedFiles[a].getName();
      boolean isXmlFile = fileName.indexOf("xml") >= 0;
      if (!indexedFiles.contains(cachedFiles[a].getName()) && isXmlFile)
        cachedFiles[a].delete();
    }
  }

  /**
   * Delete the cache as it is no longer used in RSSOwl
   */
  private void deleteCache() {
    File cachedFiles[] = new File(GlobalSettings.CACHE_DIR).listFiles();
    for (int a = 0; a < cachedFiles.length; a++) {
      String fileName = cachedFiles[a].getName();
      boolean isCacheFile = fileName.indexOf("xml") >= 0 || fileName.indexOf("obj") >= 0;
      if (isCacheFile)
        cachedFiles[a].delete();
    }
  }

  /**
   * Load the local cache index from local file
   */
  private void deserializeCache() {

    /** In case the local Cache is not used */
    if (!GlobalSettings.localCacheFeeds) {
      newsFeedLocalCache = new Hashtable();
      return;
    }

    /** Deserialize from index.obj */
    try {
      BufferedInputStream fileInStream = new BufferedInputStream(new FileInputStream(GlobalSettings.CACHE_DIR + GlobalSettings.PATH_SEPARATOR + CACHE_INDEX));
      ObjectInputStream objInStream = new ObjectInputStream(fileInStream);
      newsFeedLocalCache = (Hashtable) objInStream.readObject();
      objInStream.close();
    }

    /** In the case of an error create empty Hashtable */
    catch (IOException e) {
      newsFeedLocalCache = new Hashtable();
    } catch (ClassNotFoundException e) {
      newsFeedLocalCache = new Hashtable();
    }
  }

  /**
   * Save the newsfeed's XML document into a unique file in the cache dir and
   * return the reference to the new file
   * 
   * @param document The newsfeed's document to save
   * @return File The new cache file that was created
   * @throws IOException If an error occurs
   */
  private File saveDocument(Document document) throws IOException {

    /** Create a unique file */
    File file = File.createTempFile("cache", ".xml", new File(GlobalSettings.CACHE_DIR));

    /** Write document */
    XMLShop.writeXML(document, file);
    return file;
  }

  /**
   * Save the chache index into local file
   */
  private void serializeCache() {
    try {
      BufferedOutputStream fileOutStream = new BufferedOutputStream(new FileOutputStream(GlobalSettings.CACHE_DIR + GlobalSettings.PATH_SEPARATOR + CACHE_INDEX));
      ObjectOutputStream objOutStream = new ObjectOutputStream(fileOutStream);
      objOutStream.writeObject(newsFeedLocalCache);
      objOutStream.close();
    } catch (IOException e) {
      GUI.logger.log("serializeCache", e);
    }
  }
}