/*
 * Copyright (C) 2005, 2006 Aelitis, All Rights Reserved.
 *
 * 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.
 *
 * AELITIS, SAS au capital de 46,603.30 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 * 
 */
package com.aelitis.azureus.plugins.startstoprules.defaultplugin;

import java.util.Iterator;
import java.util.List;

import org.gudy.azureus2.core3.config.COConfigurationListener;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.plugins.PluginConfig;
import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.DownloadStats;
import org.gudy.azureus2.plugins.logging.LoggerChannel;

/**
 * @author TuxPaper
 * @created Dec 13, 2005
 *
 */
public class DefaultRankCalculator implements Comparable {
	/** All of the First Priority rules must match */
	public static final int FIRSTPRIORITY_ALL = 0;

	/** Any of the First Priority rules must match */
	public static final int FIRSTPRIORITY_ANY = 1;

	/** 
	 * Force torrent to be "Actively Seeding/Downloading" for this many ms upon
	 * start of torrent.
	 */
	private static final int FORCE_ACTIVE_FOR = 30000;

	/** 
	 * Wait XX ms before really changing activity (DL or CDing) state when
	 * state changes via speed change
	 */
	private static final int ACTIVE_CHANGE_WAIT = 10000;

	/** Maximium ranking that a torrent can get using the SPRATIO ranking type */
	private static int SPRATIO_BASE_LIMIT = 99999;

	/** 
	 * Amount to shift over the rank of the SEEDONLY ranking type, to make room
	 * in case the user has fallback to SPRATIO set.
	 */
	private static int SEEDONLY_SHIFT = SPRATIO_BASE_LIMIT + 1;

	/**
	 * For loading config settings
	 */
	private static COConfigurationListener configListener = null;

	//
	// Seeding Rank (SR) Limits and Values

	/** Rank that complete starts at (and incomplete ends at + 1) */
	public static final int SR_COMPLETE_STARTS_AT = 1000000000; // billion

	/** Maximimum ranking for time queue mode. 1 unit is a second */
	public static final int SR_TIMED_QUEUED_ENDS_AT = 999999; // 11.57 days

	/** Ranks below this value are for torrents to be ignored (moved to bottom & queued) */
	public static final int SR_IGNORED_LESS_THAN = -1;

	/** Seeding Rank value when download is marked as not queued */
	public static final int SR_NOTQUEUED = -2;

	/** Seeding Rank value when download is marked as S:P Ratio Met for FP */
	public static final int SR_FP_SPRATIOMET = -3;

	/** Seeding Rank value when download is marked as P:1S Ratio Met */
	public static final int SR_RATIOMET = -4;

	/** Seeding Rank value when download is marked as # Seeds Met */
	public static final int SR_NUMSEEDSMET = -5;

	/** Seeding Rank value when download is marked as 0 Peers and FP */
	public static final int SR_FP0PEERS = -6;

	/** Seeding Rank value when download is marked as 0 Peers */
	public static final int SR_0PEERS = -7;

	/** Seeding Rank value when download is marked as Share Ratio Met */
	public static final int SR_SHARERATIOMET = -8;

	public static final String[] SR_NEGATIVE_DEBUG = {
		"?",
		"Not Qd",
		"FP SPRatioMet",
		"Ratio Met",
		"# CDs Met",
		"FP 0 Peers",
		"0 Peers",
		"Share Ratio Met"
	};

	private static final long STALE_REFRESH_INTERVAL = 1000 * 60;

	//
	// Static config values

	/** Ranking System to use */
	protected static int iRankType = -1;

	/** Min # of Peers needed before boosting the rank of downloads with no seeds */
	private static int minPeersToBoostNoSeeds;

	/** Min Speed needed to count a incomplete download as being actively downloading */
	private static int minSpeedForActiveDL;

	/** Min speed needed to count a complete download as being actively seeding */
	private static int minSpeedForActiveSeeding;

	// Ignore torrent if seed count is at least..
	private static int iIgnoreSeedCount;

	// Ignore even when First Priority
	private static boolean bIgnore0Peers;

	private static int iIgnoreShareRatio;

	private static int iIgnoreShareRatio_SeedStart;

	private static int iIgnoreRatioPeers;

	private static int iIgnoreRatioPeers_SeedStart;

	private static int iRankTypeSeedFallback;

	private static boolean bPreferLargerSwarms;

	private static int minQueueingShareRatio;

	// Ignore First Priority
	private static int iFirstPriorityIgnoreSPRatio;

	private static boolean bFirstPriorityIgnore0Peer;

	private static int iFirstPriorityType;

	private static int iFirstPrioritySeedingMinutes;

	private static int iFirstPriorityActiveMinutes;

	private static int iFirstPriorityIgnoreIdleHours;
	
	private static long minTimeAlive;

	private static boolean bAutoStart0Peers;

	//
	// Class variables

	protected Download dl;

	private boolean bActivelyDownloading;

	private long lDLActivelyChangedOn;

	private boolean bActivelySeeding;

	private long lCDActivelyChangedOn;

	private long staleCDSince;
	
	private long staleCDOffset;

	private long lastStaleCDRefresh;

	private boolean bIsFirstPriority;

	/** Public for tooltip to access it */
	public String sExplainFP = "";

	/** Public for tooltip to access it */
	public String sExplainSR = "";

	/** Public for tooltip to access it */
	public String sTrace = "";

	private AEMonitor downloadData_this_mon = new AEMonitor(
			"StartStopRules:downloadData");

	private final StartStopRulesDefaultPlugin rules;
	
	
	
	// state-caches for sorting
	
	int lastModifiedScrapeResultPeers = 0;
	int lastModifiedScrapeResultSeeds = 0;
	int lastModifiedShareRatio = 0;
	// modified by a listener in StartStopRulesDefaultPlugin
	boolean lastScrapeResultOk = false;

	/**
	 * Default Initializer
	 * 
	 * @param _rules
	 * @param _dl
	 */
	public DefaultRankCalculator(StartStopRulesDefaultPlugin _rules, Download _dl) {
		rules = _rules;
		dl = _dl;

		try {
			downloadData_this_mon.enter();

			if (configListener == null) {

				configListener = new COConfigurationListener() {
					public void configurationSaved() {
						reloadConfigParams(rules.plugin_config);
					}
				};

				COConfigurationManager.addListener(configListener);
				configListener.configurationSaved();
			}
		} finally {
			downloadData_this_mon.exit();
		}
	}

	/**
	 * Load config values into the static variables
	 * 
	 * @param cfg
	 */
	public static void reloadConfigParams(PluginConfig cfg) {
		final String PREFIX = "StartStopManager_";

		iRankType = cfg.getUnsafeIntParameter(PREFIX + "iRankType");

		minPeersToBoostNoSeeds = cfg.getUnsafeIntParameter(PREFIX
				+ "iMinPeersToBoostNoSeeds");
		minSpeedForActiveDL = cfg.getUnsafeIntParameter(PREFIX + "iMinSpeedForActiveDL");
		minSpeedForActiveSeeding = cfg.getUnsafeIntParameter(PREFIX
				+ "iMinSpeedForActiveSeeding");

		iRankTypeSeedFallback = cfg.getUnsafeIntParameter(PREFIX
				+ "iRankTypeSeedFallback");
		bPreferLargerSwarms = cfg.getUnsafeBooleanParameter(PREFIX
				+ "bPreferLargerSwarms");
		minTimeAlive = cfg.getUnsafeIntParameter(PREFIX + "iMinSeedingTime") * 1000;
		bAutoStart0Peers = cfg.getUnsafeBooleanParameter(PREFIX + "bAutoStart0Peers");

		// Ignore torrent if seed count is at least..
		iIgnoreSeedCount = cfg.getUnsafeIntParameter(PREFIX + "iIgnoreSeedCount");
		bIgnore0Peers = cfg.getUnsafeBooleanParameter(PREFIX + "bIgnore0Peers");
		iIgnoreShareRatio = (int) (1000 * cfg.getFloatParameter("Stop Ratio"));
		iIgnoreShareRatio_SeedStart = cfg.getUnsafeIntParameter(PREFIX
				+ "iIgnoreShareRatioSeedStart");
		iIgnoreRatioPeers = cfg.getIntParameter("Stop Peers Ratio", 0);
		iIgnoreRatioPeers_SeedStart = cfg.getUnsafeIntParameter(PREFIX
				+ "iIgnoreRatioPeersSeedStart", 0);

		minQueueingShareRatio = cfg.getUnsafeIntParameter(PREFIX
				+ "iFirstPriority_ShareRatio");
		iFirstPriorityType = cfg.getUnsafeIntParameter(PREFIX + "iFirstPriority_Type");
		iFirstPrioritySeedingMinutes = cfg.getUnsafeIntParameter(PREFIX
				+ "iFirstPriority_SeedingMinutes");
		iFirstPriorityActiveMinutes = cfg.getUnsafeIntParameter(PREFIX
				+ "iFirstPriority_DLMinutes");
		// Ignore FP
		iFirstPriorityIgnoreSPRatio = cfg.getUnsafeIntParameter(PREFIX
				+ "iFirstPriority_ignoreSPRatio");
		bFirstPriorityIgnore0Peer = cfg.getUnsafeBooleanParameter(PREFIX
				+ "bFirstPriority_ignore0Peer");
		iFirstPriorityIgnoreIdleHours = cfg.getUnsafeIntParameter(PREFIX
				+ "iFirstPriority_ignoreIdleHours");
	}

	/** Sort first by SeedingRank Descending, then by Position Ascending.
	 */
	public int compareTo(Object obj) {
		if (!(obj instanceof DefaultRankCalculator)) {
			return -1;
		}

		DefaultRankCalculator dlData = (DefaultRankCalculator) obj;

		// Test FP.  FP goes to top
		if (dlData.bIsFirstPriority && !bIsFirstPriority)
			return 1;
		if (!dlData.bIsFirstPriority && bIsFirstPriority)
			return -1;
		
		// Test Completeness.  Complete go to bottom
		boolean aIsComplete = dlData.dl.isComplete();
		boolean bIsComplete = dl.isComplete();
		if (aIsComplete && !bIsComplete)
			return -1;
		if (!aIsComplete && bIsComplete)
			return 1;

		if (iRankType == StartStopRulesDefaultPlugin.RANK_NONE) {
			return dl.getPosition() - dlData.dl.getPosition();
		}

		// Check Rank. Large to top
		int value = dlData.dl.getSeedingRank() - dl.getSeedingRank();
		if (value != 0)
			return value;

		if (iRankType != StartStopRulesDefaultPlugin.RANK_TIMED) {
			// Test Large/Small Swarm pref
			int numPeersThem = dlData.lastModifiedScrapeResultPeers;
			int numPeersUs = lastModifiedScrapeResultPeers;
			if (bPreferLargerSwarms)
				value = numPeersThem - numPeersUs;
			else
				value = numPeersUs - numPeersThem;
			if (value != 0)
				return value;

			// Test Share Ratio
			value = lastModifiedShareRatio - dlData.lastModifiedShareRatio;
			if (value != 0)
				return value;
		}

		// Test Position
		return dl.getPosition() - dlData.dl.getPosition();
	}

	public Download getDownloadObject() {
		return dl;
	}
	
	public boolean isForceActive() {
		DownloadStats stats = dl.getStats();
		return SystemTime.getCurrentTime() - stats.getTimeStarted() <= FORCE_ACTIVE_FOR;
	}

	/**
	 * Retrieves whether the torrent is "actively" downloading
	 * 
	 * @return true: actively downloading
	 */
	public boolean getActivelyDownloading() {
		boolean bIsActive = false;
		DownloadStats stats = dl.getStats();
		int state = dl.getState();

		// In order to be active,
		// - Must be downloading (and thus incomplete)
		// - Must be above speed threshold, or started less than 30s ago
		if (state != Download.ST_DOWNLOADING) {
			bIsActive = false;
		} else if (SystemTime.getCurrentTime() - stats.getTimeStarted() <= FORCE_ACTIVE_FOR) {
			bIsActive = true;
		} else {
			// activity based on DL Average
			bIsActive = (stats.getDownloadAverage() >= minSpeedForActiveDL);

			if (bActivelyDownloading != bIsActive) {
				long now = SystemTime.getCurrentTime();
				// Change
				if (lDLActivelyChangedOn == -1) {
					// Start Timer
					lDLActivelyChangedOn = now;
					bIsActive = !bIsActive;
				} else if (now - lDLActivelyChangedOn < ACTIVE_CHANGE_WAIT) {
					// Continue as old state until timer finishes
					bIsActive = !bIsActive;
				}
			} else {
				// no change, reset timer
				lDLActivelyChangedOn = -1;
			}
		}

		if (bActivelyDownloading != bIsActive) {
			bActivelyDownloading = bIsActive;
			if (rules != null) {
				rules.requestProcessCycle(null);
				if (rules.bDebugLog)
					rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION,
							"somethingChanged: ActivelyDownloading changed");
			}
		}
		return bActivelyDownloading;
	}

	/**
	 * Retrieves whether the torrent is "actively" seeding
	 * 
	 * @return true: actively seeding
	 */
	public boolean getActivelySeeding() {
		boolean bIsActive = false;
		DownloadStats stats = dl.getStats();
		int state = dl.getState();
		// Timed torrents don't use a speed threshold, since they are based on time!
		// However, First Priorities need to be checked for activity so that 
		// timed ones can start when FPs are below threshold.  Ditto for 0 Peers
		// when bAutoStart0Peers
		if (iRankType == StartStopRulesDefaultPlugin.RANK_TIMED
				&& !isFirstPriority()
				&& !(bAutoStart0Peers && rules.calcPeersNoUs(dl) == 0 && lastScrapeResultOk)) {
			bIsActive = (state == Download.ST_SEEDING);

		} else if (state != Download.ST_SEEDING
				|| (bAutoStart0Peers && rules.calcPeersNoUs(dl) == 0)) {
			// Not active if we aren't seeding
			// Not active if we are AutoStarting 0 Peers, and peer count == 0
			bIsActive = false;
			staleCDSince = -1;
		} else if (SystemTime.getCurrentTime() - stats.getTimeStarted() <= FORCE_ACTIVE_FOR) {
			bIsActive = true;
			staleCDSince = -1;
		} else {
			bIsActive = (stats.getUploadAverage() >= minSpeedForActiveSeeding);

			if (bActivelySeeding != bIsActive) {
				long now = SystemTime.getCurrentTime();
				// Change
				if (lCDActivelyChangedOn < 0) {
					// Start Timer
					lCDActivelyChangedOn = now;
					bIsActive = !bIsActive;
				} else if (now - lCDActivelyChangedOn < ACTIVE_CHANGE_WAIT) {
					// Continue as old state until timer finishes
					bIsActive = !bIsActive;
				}
				
				if (bActivelySeeding != bIsActive) {
  				if (bIsActive) {
  					staleCDSince = -1;
  					staleCDOffset = 0;
  				} else {
  					staleCDSince = System.currentTimeMillis();
  				}
				}

			} else {
				// no change, reset timer
				lCDActivelyChangedOn = -1;
			}
		}

		if (bActivelySeeding != bIsActive) {
			bActivelySeeding = bIsActive;

			if (rules != null) {
				rules.requestProcessCycle(null);
				if (rules.bDebugLog)
					rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION,
							"somethingChanged: ActivelySeeding changed");
			}
		}
		return bActivelySeeding;
	}

	/** Assign Seeding Rank based on RankType
	 * @return New Seeding Rank Value
	 */
	public int recalcSeedingRank() {
		try {
			downloadData_this_mon.enter();

			sExplainSR = "";

			int oldSR = dl.getSeedingRank();
			DownloadStats stats = dl.getStats();

			int newSR = 0;

			// make undownloaded sort to top so they can start first.
			if (!dl.isComplete()) {
				newSR = SR_COMPLETE_STARTS_AT + (10000 - dl.getPosition());
				dl.setSeedingRank(newSR);
				// make sure we capture FP being turned off when torrent does from
				// complete to incomplete
				isFirstPriority();
				if ( rules.bDebugLog ){
					sExplainSR += "  not complete. SetSR " + newSR + "\n";
				}
				return newSR;
			}

			// here we are seeding

			lastModifiedShareRatio = stats.getShareRatio();
			lastModifiedScrapeResultPeers = rules.calcPeersNoUs(dl);
			lastModifiedScrapeResultSeeds = rules.calcSeedsNoUs(dl);

			boolean bScrapeResultsOk = (lastModifiedScrapeResultPeers > 0 || lastModifiedScrapeResultSeeds > 0
					|| lastScrapeResultOk) && (lastModifiedScrapeResultPeers >= 0 && lastModifiedScrapeResultSeeds >= 0);

			if (!isFirstPriority()) {
				// Check Ignore Rules
				// never apply ignore rules to First Priority Matches
				// (we don't want leechers circumventing the 0.5 rule)

				//0 means unlimited
				if (iIgnoreShareRatio != 0 && lastModifiedShareRatio >= iIgnoreShareRatio
						&& (lastModifiedScrapeResultSeeds >= iIgnoreShareRatio_SeedStart || !bScrapeResultsOk)
						&& lastModifiedShareRatio != -1) {
					
					if (rules.bDebugLog)
						sExplainSR += "  shareratio met: shareRatio(" + lastModifiedShareRatio
								+ ") >= " + iIgnoreShareRatio + "\n";

					dl.setSeedingRank(SR_SHARERATIOMET);
					return SR_SHARERATIOMET;
				} else if (rules.bDebugLog && iIgnoreShareRatio != 0
						&& lastModifiedShareRatio >= iIgnoreShareRatio) {
					sExplainSR += "  shareratio NOT met: ";
					if (lastModifiedScrapeResultSeeds >= iIgnoreShareRatio_SeedStart) 
						sExplainSR += lastModifiedScrapeResultSeeds + " below seed threshold of "
								+ iIgnoreShareRatio_SeedStart;
					sExplainSR += "\n";
				}

				if (lastModifiedScrapeResultPeers == 0 && bScrapeResultsOk) {
					// If both bIgnore0Peers and bFirstPriorityIgnore0Peer are on,
					// we won't know which one it is at this point.
					// We have to use the normal SR_0PEERS in case it isn't FP
					if (bIgnore0Peers) {
						if (rules.bDebugLog)
							sExplainSR += "  Ignore 0 Peers criteria met\n";

						dl.setSeedingRank(SR_0PEERS);
						return SR_0PEERS;
					}

//					if (bFirstPriorityIgnore0Peer) {
//						if (rules.bDebugLog)
//							sExplainSR += "  Ignore 0 Peers criteria for FP met\n";
//
//						dl.setSeedingRank(SR_FP0PEERS);
//						return SR_FP0PEERS;
//					}
				} else if (rules.bDebugLog && lastModifiedScrapeResultPeers == 0) {
					sExplainSR += "  0 Peer Ignore rule NOT applied: Scrape invalid\n";
				}

//				if (numPeers != 0 && iFirstPriorityIgnoreSPRatio != 0
//						&& numSeeds / numPeers >= iFirstPriorityIgnoreSPRatio) {
//					if (rules.bDebugLog)
//						sExplainSR += "  Ignore rule for S:P Ratio for FP met.  Current: ("
//								+ (numSeeds / numPeers)
//								+ ") >= Threshold("
//								+ iFirstPriorityIgnoreSPRatio + ")\n";
//
//					dl.setSeedingRank(SR_FP_SPRATIOMET);
//					return SR_FP_SPRATIOMET;
//				}

				//0 means disabled
				if ((iIgnoreSeedCount != 0) && (lastModifiedScrapeResultSeeds >= iIgnoreSeedCount)) {
					if (rules.bDebugLog)
						sExplainSR += "  SeedCount Ignore rule met.  numSeeds("
								+ lastModifiedScrapeResultSeeds + " >= iIgnoreSeedCount(" + iIgnoreSeedCount + ")\n"; 

					dl.setSeedingRank(SR_NUMSEEDSMET);
					return SR_NUMSEEDSMET;
				}

				// Ignore when P:S ratio met
				// (More Peers for each Seed than specified in Config)
				//0 means never stop
				if (iIgnoreRatioPeers != 0 && lastModifiedScrapeResultSeeds != 0) {
					float ratio = (float) lastModifiedScrapeResultPeers / lastModifiedScrapeResultSeeds;
					if (ratio <= iIgnoreRatioPeers
							&& lastModifiedScrapeResultSeeds >= iIgnoreRatioPeers_SeedStart) {

						if (rules.bDebugLog)
							sExplainSR += "  P:S Ignore rule met.  ratio(" + ratio
									+ " <= threshold(" + iIgnoreRatioPeers_SeedStart + ")\n";

						dl.setSeedingRank(SR_RATIOMET);
						return SR_RATIOMET;
					}
				}
			}

			// Never do anything with rank type of none
			if (iRankType == StartStopRulesDefaultPlugin.RANK_NONE) {
				if (rules.bDebugLog)
					sExplainSR += "  Ranking Type set to none.. blanking seeding rank\n";
				
				// everythink ok!
				dl.setSeedingRank(newSR);
				return newSR;
			}

			if (iRankType == StartStopRulesDefaultPlugin.RANK_TIMED) {
				if (bIsFirstPriority) {
					newSR += SR_TIMED_QUEUED_ENDS_AT + 1;
					dl.setSeedingRank(newSR);
					return newSR;
				}

				int state = dl.getState();
				if (state == Download.ST_STOPPING || state == Download.ST_STOPPED
						|| state == Download.ST_ERROR) {
					if (rules.bDebugLog)
						sExplainSR += "  Download stopping, stopped or in error\n";
					dl.setSeedingRank(SR_NOTQUEUED);
					return SR_NOTQUEUED;
				} else if (state == Download.ST_SEEDING || state == Download.ST_READY
						|| state == Download.ST_WAITING || state == Download.ST_PREPARING) {
					// force sort to top
					long lMsElapsed = 0;
					if (state == Download.ST_SEEDING && !dl.isForceStart())
						lMsElapsed = (SystemTime.getCurrentTime() - stats
								.getTimeStartedSeeding());

					if (lMsElapsed >= minTimeAlive) {
						dl.setSeedingRank(1);
						if (oldSR > SR_TIMED_QUEUED_ENDS_AT) {
							rules.requestProcessCycle(null);
							if (rules.bDebugLog)
								rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION,
										"somethingChanged: TimeUp");
						}
					} else {
						newSR = SR_TIMED_QUEUED_ENDS_AT + 1 + (int) (lMsElapsed / 1000);
						dl.setSeedingRank(newSR);
						if (oldSR <= SR_TIMED_QUEUED_ENDS_AT) {
							rules.requestProcessCycle(null);
							if (rules.bDebugLog)
								rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION,
										"somethingChanged: strange timer change");
						}
					}
					return newSR;
				} else {
					if (oldSR <= 0) {
						newSR = SR_TIMED_QUEUED_ENDS_AT - dl.getPosition();
						dl.setSeedingRank(newSR);
						rules.requestProcessCycle(null);
						if (rules.bDebugLog)
							rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION,
									"somethingChanged: NotIgnored");
					}
					return newSR;
				}
			}

			/** 
			 * Add to SeedingRank based on Rank Type
			 */

			// SeedCount and SPRatio require Scrape Results..
			if (bScrapeResultsOk) {
				if ( iRankType == StartStopRulesDefaultPlugin.RANK_PEERCOUNT )
				{
					if(lastModifiedScrapeResultPeers > lastModifiedScrapeResultSeeds * 10)
						newSR = 100 * lastModifiedScrapeResultPeers * 10;
					else
						newSR = (int)((long)100 * lastModifiedScrapeResultPeers * lastModifiedScrapeResultPeers/(lastModifiedScrapeResultSeeds+1));
				}				
				else if ((iRankType == StartStopRulesDefaultPlugin.RANK_SEEDCOUNT)
						&& (iRankTypeSeedFallback == 0 || iRankTypeSeedFallback > lastModifiedScrapeResultSeeds))
				{
					if (lastModifiedScrapeResultSeeds < 10000)
						newSR = 10000 - lastModifiedScrapeResultSeeds;
					else
						newSR = 1;
					// shift over to make way for fallback
					newSR *= SEEDONLY_SHIFT;

				} else { // iRankType == RANK_SPRATIO or we are falling back
					if (lastModifiedScrapeResultPeers != 0) {
						if (lastModifiedScrapeResultSeeds == 0) {
							if (lastModifiedScrapeResultPeers >= minPeersToBoostNoSeeds)
								newSR += SPRATIO_BASE_LIMIT;
						} else { // numSeeds != 0 && numPeers != 0
							float x = (float) lastModifiedScrapeResultSeeds / lastModifiedScrapeResultPeers;
							newSR += SPRATIO_BASE_LIMIT / ((x + 1) * (x + 1));
						}
					}
				}
			} else {
				if (rules.bDebugLog)
					sExplainSR += "  Can't calculate SR, no scrape results\n";
			}
			
			if (staleCDOffset > 0) {
				// every 10 minutes of not being active, subtract one SR
				if (newSR > staleCDOffset) {
					newSR -= staleCDOffset;
					sExplainSR += "  subtracted " + staleCDOffset + " due to non-activeness\n";
				} else {
					staleCDOffset = 0; 
				}
			}

			if (newSR < 0)
				newSR = 1;

			if (newSR != oldSR)
				dl.setSeedingRank(newSR);
			return newSR;
		} finally {

			downloadData_this_mon.exit();
		}
	} // recalcSeedingRank

	/** Does the torrent match First Priority criteria? 
	 * @return FP State 
	 */
	public boolean isFirstPriority() {
		boolean bFP = pisFirstPriority();

		if (bIsFirstPriority != bFP) {
			bIsFirstPriority = bFP;
			rules.requestProcessCycle(null);
			if (rules.bDebugLog)
				rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION,
						"somethingChanged: FP changed");
		}
		return bIsFirstPriority;
	}

	private boolean pisFirstPriority() {
		if (rules.bDebugLog)
			sExplainFP = "FP if "
					+ (iFirstPriorityType == FIRSTPRIORITY_ALL ? "all" : "any")
					+ " criteria match:\n";

		if (!dl.isPersistent()) {
			if (rules.bDebugLog)
				sExplainFP += "Not FP: Download not persistent\n";
			return false;
		}

		int state = dl.getState();
		if (state == Download.ST_ERROR || state == Download.ST_STOPPED) {
			if (rules.bDebugLog)
				sExplainFP += "Not FP: Download is ERROR or STOPPED\n";
			return false;
		}

		// FP only applies to completed
		if (!dl.isComplete()) {
			if (rules.bDebugLog)
				sExplainFP += "Not FP: Download not complete\n";
			return false;
		}
		
		List listeners = rules.getFPListeners();
		StringBuffer fp_listener_debug = null;
		if (!listeners.isEmpty())
		{
			if (rules.bDebugLog)
				fp_listener_debug = new StringBuffer();
			for (Iterator iter = listeners.iterator(); iter.hasNext();)
			{
				StartStopRulesFPListener l = (StartStopRulesFPListener) iter.next();
				boolean result = l.isFirstPriority(dl, lastModifiedScrapeResultSeeds, lastModifiedScrapeResultPeers, fp_listener_debug);
				if (fp_listener_debug != null && fp_listener_debug.length() > 0)
				{
					char last_ch = fp_listener_debug.charAt(fp_listener_debug.length() - 1);
					if (last_ch != '\n')
						fp_listener_debug.append('\n');
					sExplainFP += fp_listener_debug;
					fp_listener_debug.setLength(0);
				}
				if (result)
				{
					return true;
				}
			}
		}


		// FP doesn't apply when S:P >= set SPratio (SPratio = 0 means ignore)
		if (lastModifiedScrapeResultPeers > 0 && lastModifiedScrapeResultSeeds > 0
				&& (lastModifiedScrapeResultSeeds / lastModifiedScrapeResultPeers) >= iFirstPriorityIgnoreSPRatio
				&& iFirstPriorityIgnoreSPRatio != 0) {
			if (rules.bDebugLog)
				sExplainFP += "Not FP: S:P >= " + iFirstPriorityIgnoreSPRatio + ":1\n";
			return false;
		}

		//not FP if no peers  //Nolar, 2105 - Gouss, 2203
		if (lastModifiedScrapeResultPeers == 0 && lastScrapeResultOk && bFirstPriorityIgnore0Peer) {
			if (rules.bDebugLog)
				sExplainFP += "Not FP: 0 peers\n";
			return false;
		}

		if (iFirstPriorityIgnoreIdleHours > 0) {
			long lastUploadSecs = dl.getStats().getSecondsSinceLastUpload();
			if (lastUploadSecs < 0) {
				lastUploadSecs = dl.getStats().getSecondsOnlySeeding();
			}
			if (lastUploadSecs > 60 * 60 * iFirstPriorityIgnoreIdleHours) {
				if (rules.bDebugLog)
					sExplainFP += "Not FP: " + lastUploadSecs + "s > "
							+ iFirstPriorityIgnoreIdleHours + "h of no upload\n";
				return false;
			}
		}

		int shareRatio = dl.getStats().getShareRatio();
		boolean bLastMatched = (shareRatio != -1)
				&& (shareRatio < minQueueingShareRatio);

		if (rules.bDebugLog)
			sExplainFP += "  shareRatio(" + shareRatio + ") < "
					+ minQueueingShareRatio + "=" + bLastMatched + "\n";

		if (!bLastMatched && iFirstPriorityType == FIRSTPRIORITY_ALL) {
			if (rules.bDebugLog)
				sExplainFP += "..Not FP.  Exit Early\n";
			return false;
		}
		if (bLastMatched && iFirstPriorityType == FIRSTPRIORITY_ANY) {
			if (rules.bDebugLog)
				sExplainFP += "..Is FP.  Exit Early\n";
			return true;
		}

		bLastMatched = (iFirstPrioritySeedingMinutes == 0);
		if (!bLastMatched) {
			long timeSeeding = dl.getStats().getSecondsOnlySeeding();
			if (timeSeeding >= 0) {
				bLastMatched = (timeSeeding < (iFirstPrioritySeedingMinutes * 60));
				if (rules.bDebugLog)
					sExplainFP += "  SeedingTime(" + timeSeeding + ") < "
							+ (iFirstPrioritySeedingMinutes * 60) + "=" + bLastMatched + "\n";
				if (!bLastMatched && iFirstPriorityType == FIRSTPRIORITY_ALL) {
					if (rules.bDebugLog)
						sExplainFP += "..Not FP.  Exit Early\n";
					return false;
				}
				if (bLastMatched && iFirstPriorityType == FIRSTPRIORITY_ANY) {
					if (rules.bDebugLog)
						sExplainFP += "..Is FP.  Exit Early\n";
					return true;
				}
			}
		} else if (rules.bDebugLog) {
			sExplainFP += "  Skipping Seeding Time check (user disabled)\n";
		}

		bLastMatched = (iFirstPriorityActiveMinutes == 0);
		if (!bLastMatched) {
			long timeActive = dl.getStats().getSecondsDownloading()
					+ dl.getStats().getSecondsOnlySeeding();
			if (timeActive >= 0) {
				bLastMatched = (timeActive < (iFirstPriorityActiveMinutes * 60));
				if (rules.bDebugLog)
					sExplainFP += "  ActiveTime(" + timeActive + ") < "
							+ (iFirstPriorityActiveMinutes * 60) + "=" + bLastMatched + "\n";
				if (!bLastMatched && iFirstPriorityType == FIRSTPRIORITY_ALL) {
					if (rules.bDebugLog)
						sExplainFP += "..Not FP.  Exit Early\n";
					return false;
				}
				if (bLastMatched && iFirstPriorityType == FIRSTPRIORITY_ANY) {
					if (rules.bDebugLog)
						sExplainFP += "..Is FP.  Exit Early\n";
					return true;
				}
			}
		} else if (rules.bDebugLog) {
			sExplainFP += "  Skipping DL Time check (user disabled)\n";
		}

		if (iFirstPriorityType == FIRSTPRIORITY_ALL) {
			if (rules.bDebugLog)
				sExplainFP += "..Is FP\n";
			return true;
		}

		if (rules.bDebugLog)
			sExplainFP += "..Not FP\n";
		return false;
	}

	/**
	 * 
	 * @return last calculated FP state
	 */
	public boolean getCachedIsFP() {
		return bIsFirstPriority;
	}

	public String toString() {
		return String.valueOf(dl.getSeedingRank());
	}

	/**
	 * Check Seeders for various changes not triggered by listeners
	 * 
	 * @return True: something changed
	 */
	public boolean changeChecker() {
		if (getActivelySeeding()) {
			int shareRatio = dl.getStats().getShareRatio();
			int numSeeds = rules.calcSeedsNoUs(dl);

			if (iIgnoreShareRatio != 0 && shareRatio >= iIgnoreShareRatio
					&& (numSeeds >= iIgnoreShareRatio_SeedStart || !lastScrapeResultOk)
					&& shareRatio != -1) {
				if (rules.bDebugLog) {
					rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION,
							"somethingChanged: shareRatio changeChecker");
				}
				return true;
			}
		}

		/* READY downloads are usually waiting for a seeding torrent to
		 stop (the seeding torrent probably is within the "Minimum Seeding
		 Time" setting)
		 
		 The rules may go through several cycles before a READY torrent is
		 processed
		 */
		if (dl.getState() == Download.ST_READY) {
			if (rules.bDebugLog)
				rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION,
						"somethingChanged: Download is ready");
			return true;
		}
		
		if (staleCDSince > 0) {
			long now = SystemTime.getCurrentTime(); 
			if (now - lastStaleCDRefresh > STALE_REFRESH_INTERVAL) {
				staleCDOffset += (now - lastStaleCDRefresh) / STALE_REFRESH_INTERVAL;
				lastStaleCDRefresh = now;
				if (rules.bDebugLog) {
					rules.log.log(dl.getTorrent(), LoggerChannel.LT_INFORMATION,
							"somethingChanged: staleCD changeChecker");
				}
				return true;
			}
		}

		return false;
	}
}
