/*   **********************************************************************  **
 **   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.controller.thread;

import net.sourceforge.rssowl.controller.GUI;
import net.sourceforge.rssowl.controller.NewsTabFolder;
import net.sourceforge.rssowl.controller.statusline.LoadJob;
import net.sourceforge.rssowl.controller.statusline.StatusLine;
import net.sourceforge.rssowl.dao.NewsfeedFactory;
import net.sourceforge.rssowl.dao.NewsfeedFactoryException;
import net.sourceforge.rssowl.model.Category;
import net.sourceforge.rssowl.model.Channel;
import net.sourceforge.rssowl.model.Favorite;
import net.sourceforge.rssowl.util.GlobalSettings;
import net.sourceforge.rssowl.util.search.SearchDefinition;
import net.sourceforge.rssowl.util.shop.FileShop;
import net.sourceforge.rssowl.util.shop.StringShop;
import net.sourceforge.rssowl.util.shop.WidgetShop;

import org.eclipse.swt.custom.CTabItem;

import java.util.Iterator;
import java.util.TreeSet;
import java.util.Vector;

/**
 * Load multiple RSSChannels from a TreeSet holding the favorites titles.
 * Display the aggregated RSSChannels in one tab.
 * 
 * @author <a href="mailto:bpasero@rssowl.org">Benjamin Pasero </a>
 * @version 1.2.3
 */
public class AggregationLoader {

  /**
   * Wait state in ms for the outer feed loader to check the inner feed loader
   * Threads if they have already finished
   */
  private static final int WAIT_CYCLE = 300;

  /** If a search is performed, set to TRUE */
  private boolean performSearch;

  /** Search Definition */
  private SearchDefinition searchDefinition;

  /** The category that is aggregated */
  Category aggregatedCategory;

  /** Aggregated category channel */
  Channel aggregatedRSSChannel;

  /** List of favorites to aggregate */
  TreeSet favorites;

  /** Set this flag to true, if the newstitles have to be unique */
  boolean generateUniqueTitles;

  /** Collection of all inner feed load Threads */
  Vector innerFeedLoadThreads;

  /** Set this flag to true, if the Multi loader performs a reload */
  boolean isReload;

  /** Thread to load the aggregated category */
  ExtendedThread outerNewsFeedLoader;

  /** List of RSS channels */
  Vector rssChannels;

  /** The MainController */
  GUI rssOwlGui;

  /** Count the number of inner feed loader Threads */
  int runningThreadCounter;

  /** Style of the operation (load, reload, search) */
  int style;

  /** Title of the aggregated category */
  String title;

  /**
   * Instantiate a new AggregationLoader
   * 
   * @param favorites Vector with the URLs
   * @param aggregatedCategory The category that is aggregated
   * @param rssOwlGui The RSSOwl Maincontroller
   * @param title Title of the aggregated Channel
   */
  public AggregationLoader(TreeSet favorites, Category aggregatedCategory, GUI rssOwlGui, String title) {
    this(favorites, aggregatedCategory, rssOwlGui, title, SearchDefinition.NO_SEARCH);
  }

  /**
   * Instantiate a new AggregationLoader
   * 
   * @param favorites Vector with the URLs
   * @param aggregatedCategory The category that is aggregated
   * @param rssOwlGui The RSSOwl Maincontroller
   * @param title Title of the aggregated Channel
   * @param searchDefinition Pattern and Scope of the Search.
   */
  public AggregationLoader(TreeSet favorites, Category aggregatedCategory, GUI rssOwlGui, String title, SearchDefinition searchDefinition) {
    this.favorites = favorites;
    this.aggregatedCategory = aggregatedCategory;
    this.rssOwlGui = rssOwlGui;
    this.title = title;
    this.searchDefinition = searchDefinition;
    performSearch = StringShop.isset(searchDefinition.getPattern());
    rssChannels = new Vector();
    generateUniqueTitles = true;
    isReload = false;
    innerFeedLoadThreads = new Vector();
    runningThreadCounter = 0;
  }

  /**
   * Load all favorites in a Thread
   * 
   * @param displayNewsFeed TRUE if newsfeed should be displayed
   */
  public void loadFavorites(boolean displayNewsFeed) {

    /**
     * Set to FALSE if the multi loader is performing a reload of the given
     * favorites without displaying them. Setting it to FALSE will avoid
     * creating unique titles in the channel, because the items are not
     * displayed in an aggregated tab.
     */
    generateUniqueTitles = displayNewsFeed;

    /** Create the thread to load the favorites */
    createOuterFeedLoader(displayNewsFeed);

    style = StatusLine.LOAD;

    /** Init the status line */
    if (!displayNewsFeed)
      style = StatusLine.RELOAD;
    else if (performSearch)
      style = StatusLine.SEARCH;
    else
      style = StatusLine.LOAD;

    /** Setup informations on status line */
    rssOwlGui.getRSSOwlStatusLine().addEntireLoad(favorites.size());

    /** Start thread */
    outerNewsFeedLoader.setName("Outer Multi Feed Load Thread");
    outerNewsFeedLoader.start();
  }

  /**
   * Set to TRUE if all feeds of the aggregated category should get uncached
   * before loaded.
   * 
   * @param isReload If Reload, set to TRUE
   */
  public void setReload(boolean isReload) {
    this.isReload = isReload;
  }

  /**
   * Create the favorites multi load thread
   * 
   * @param displayNewsFeed TRUE if newsfeed should be displayed
   */
  private void createOuterFeedLoader(final boolean displayNewsFeed) {

    /** Outer Thread: Create threads in loop to load each feed */
    outerNewsFeedLoader = new ExtendedThread() {
      public void run() {

        /** Foreach favorite */
        Iterator favIt = favorites.iterator();
        while (favIt.hasNext() && !isStopped() && GUI.isAlive()) {

          /** Do not start a new Thread if MAX_THREAD_COUNT is reached */
          if (runningThreadCounter == GlobalSettings.maxConnectionCount) {
            try {
              sleep(WAIT_CYCLE);
            } catch (InterruptedException e) {
              return;
            }
          }

          /** RSSOwl is ready to begin a new inner feed loader */
          else {
            String url = Category.getLinkForTitle((String) favIt.next());

            /** Create new thread to load the feed */
            Thread innerNewsFeedLoader = createInnerFeedLoader(url);

            /** Add this Thread to the collection of inner feed load Threads */
            innerFeedLoadThreads.add(innerNewsFeedLoader);

            /** Begin loading of the feed */
            innerNewsFeedLoader.start();

            /** Count this Thread */
            runningThreadCounter++;
          }
        }

        /** Wait for the inner newsfeed loaders to finish loading */
        while (!isLoadingDone() && !isStopped()) {
          try {
            sleep(WAIT_CYCLE);
          } catch (InterruptedException e) {
            return;
          }
        }

        /** Done loading, display channels */
        if (!isStopped() && GUI.isAlive()) {
          GUI.display.syncExec(new Runnable() {
            public void run() {
              if (GUI.isAlive() && (displayNewsFeed || rssOwlGui.getRSSOwlNewsTabFolder().isAggregationOpened(title))) {

                /** Aggregate Channels */
                if (!isStopped())
                  createAggregatedRSSChannel();

                /** Display Aggregation */
                if (!isStopped())
                  displayRSSChannels();
              }
            }
          });
        }
      }
    };
    outerNewsFeedLoader.setDaemon(true);
  }

  /** Aggregate the rsschannels to one channel */
  void createAggregatedRSSChannel() {
    aggregatedRSSChannel = new Channel(title, aggregatedCategory, rssChannels, favorites, generateUniqueTitles);
  }

  /**
   * Create an warning channel to display an erroneous RSS feed
   * 
   * @param url The url of the feed
   * @param e The factory exception that occured
   */
  void createErrorChannel(String url, NewsfeedFactoryException e) {

    /** RSS Channel */
    rssChannels.add(Channel.createErrorChannel(url, e));

    /** Set warning flag to this channel's favorite */
    if (Category.getFavPool().containsKey(url)) {
      ((Favorite) Category.getFavPool().get(url)).updateErrorState(true);
    }
  }

  /**
   * Create the inner feed loader that is called from the outer feed loader for
   * each URL to load.
   * 
   * @param url The url of the feed to load
   * @return Thread The inner feed loader
   */
  ExtendedThread createInnerFeedLoader(final String url) {

    /** Inner Thread: Load a newsfeed */
    ExtendedThread innerNewsFeedLoader = new ExtendedThread() {
      Channel currentRSSChannel;

      public void run() {

        /** Return in case URL is not available */
        if (!StringShop.isset(url))
          return;

        /** Create a new Job */
        final LoadJob loadJob = new LoadJob(url, Category.getTitleForLink(url), true, style, this);
        loadJob.setAggregationThread(outerNewsFeedLoader);

        /** Flag indicating wether to update the cache */
        boolean updateCache = false;

        /** Uncache feed if reload is performed */
        if (isReload)
          rssOwlGui.getFeedCacheManager().unCacheNewsfeed(url, false);

        /** Update the status line if user did not skipped this thread */
        if (GUI.isAlive() && !isStopped()) {
          GUI.display.syncExec(new Runnable() {
            public void run() {
              rssOwlGui.getRSSOwlStatusLine().insertJob(loadJob);
            }
          });
        }

        /** The favorite could have been deleted meanwhile */
        if (url != null) {
          currentRSSChannel = null;

          /** Load the Channel from the live cache */
          if (rssOwlGui.getFeedCacheManager().isNewsfeedCached(url, false) && !FileShop.exists(url)) {
            currentRSSChannel = rssOwlGui.getFeedCacheManager().getCachedNewsfeed(url);

            /** Set unread / read state in newsitems */
            if (!isStopped() && currentRSSChannel != null)
              currentRSSChannel.updateReadStatusOnNews();
          }

          /**
           * Load the Channel from the local cache in case the user is working
           * offline
           */
          else if (GlobalSettings.workOffline && rssOwlGui.getFeedCacheManager().isNewsfeedCached(url, true) && !FileShop.exists(url)) {
            currentRSSChannel = rssOwlGui.getFeedCacheManager().getCachedNewsfeed(url);

            /** Set unread / read state in newsitems */
            if (!isStopped() && currentRSSChannel != null)
              currentRSSChannel.updateReadStatusOnNews();

            /** Store local cache into live cache */
            rssOwlGui.getFeedCacheManager().cacheNewsfeed(url, currentRSSChannel);
          }

          /** The user is working offline but the online feed is not cached */
          else if (GlobalSettings.workOffline && !FileShop.exists(url))
            createErrorChannel(url, new NewsfeedFactoryException(url, null, null, NewsfeedFactoryException.ERROR_WORKING_OFFLINE));

          /** Feed was not cached and user did not skipped this thread */
          else if (!isStopped()) {

            /** Try to load the channel from the URL */
            try {
              currentRSSChannel = new NewsfeedFactory((Favorite) Category.getFavPool().get(url)).getRSSChannel();
              updateCache = true;
            }

            /**
             * There was an error. Display it in a Channel News if user did not
             * skipped this thread
             */
            catch (NewsfeedFactoryException e) {
              if (!isStopped())
                createErrorChannel(url, e);
            }
          }

          /** Finish the job if the user has not skipped it */
          if (GUI.isAlive() && !isStopped()) {
            GUI.display.syncExec(new Runnable() {
              public void run() {
                rssOwlGui.getRSSOwlStatusLine().finishJob(loadJob);
              }
            });
          }

          /** Newsfeed loaded successfully */
          if (currentRSSChannel != null) {

            /** Add to list of channels if user did not skipped this thread */
            if (!isStopped())
              rssChannels.add(currentRSSChannel);

            /**
             * Update the Favorite's TreeItem (if it is a favorite) if user did
             * not skipped this thread
             */
            if (Category.getFavPool().containsKey(url) && !isStopped()) {
              Favorite rssOwlFavorite = (Favorite) Category.getFavPool().get(url);
              rssOwlFavorite.updateErrorState(false);
              rssOwlFavorite.updateReadStatus(currentRSSChannel.getUnreadNewsCount());
              rssOwlFavorite.syncMetaData(currentRSSChannel);
            }

            /** Save this channel in memory if user did not skipped this thread */
            if (!isStopped() && updateCache)
              rssOwlGui.getFeedCacheManager().cacheNewsfeed(url, currentRSSChannel);

            /** Update opened Tab with current loaded feed is necessary */
            if (!isStopped() && GUI.isAlive()) {
              GUI.display.syncExec(new Runnable() {
                public void run() {
                  CTabItem tabItem = rssOwlGui.getRSSOwlNewsTabFolder().getFeedTabItem(url);
                  if (WidgetShop.isset(tabItem))
                    rssOwlGui.getRSSOwlNewsTabFolder().displayNewsfeed(currentRSSChannel, url, SearchDefinition.NO_SEARCH, true, NewsTabFolder.DISPLAY_MODE_NO_FOCUS);
                }
              });
            }
          }
        }

        /** Decrease Thread Counter */
        runningThreadCounter--;
      }
    };

    /** Name identifier */
    innerNewsFeedLoader.setName("Inner Feed Loader Thread");

    /** JVM should exit if this thread is still waiting for the URL connection */
    innerNewsFeedLoader.setDaemon(true);

    return innerNewsFeedLoader;
  }

  /**
   * Display the rsschannels in one tab
   */
  void displayRSSChannels() {

    /** Handle extrem situation: RSSOwl closed */
    if (!GUI.isAlive())
      return;

    /** Cache the aggregated category */
    rssOwlGui.getFeedCacheManager().cacheNewsfeed(title, aggregatedRSSChannel);

    /** Display the aggregated category in a tab and update cache */
    rssOwlGui.displayNewsfeed(aggregatedRSSChannel, title, searchDefinition, true, NewsTabFolder.DISPLAY_MODE_FOCUS);
  }

  /**
   * Checks if the loading of this aggregation is done or not
   * 
   * @return boolean TRUE if the loading is done
   */
  boolean isLoadingDone() {

    /** Foreach inner feed load Thread */
    for (int a = 0; a < innerFeedLoadThreads.size(); a++) {
      ExtendedThread innerFeedLoadThread = (ExtendedThread) innerFeedLoadThreads.get(a);

      /** In case this thread was not interrupted and is still running */
      if (!innerFeedLoadThread.isStopped() && innerFeedLoadThread.isAlive())
        return false;
    }

    /** Loading is done */
    return true;
  }
}