package com.aelitis.azureus.core.speedmanager.impl.v2;

import com.aelitis.azureus.core.speedmanager.SpeedManagerPingSource;
import com.aelitis.azureus.core.util.average.Average;

import java.util.Map;
import java.util.HashMap;

import org.gudy.azureus2.core3.util.SystemTime;

/**
 * Created on May 31, 2007
 * Created by Alan Snyder
 * Copyright (C) 2007 Aelitis, All Rights Reserved.
 * <p/>
 * 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.
 * <p/>
 * AELITIS, SAS au capital de 63.529,40 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 */


/**
 * This class manage cycling though the PingSources. It keep track of PingSource stats and
 * applies rules on if/when to cycle though a ping-source.
 *
 * #1) If the slowest ping-source is 10x the the best for a 1 min average. kick it.
 * #2) If a ping-source is slower then two combined sources (2x) for a 5 min average. then kick it.
 * #3) Every 30 minutes kick the slowest ping source and request a new one. Just to keep things fresh.
 *
 * Also maintain logic do determine if a new source is better then the previous one. (To determine
 * if these rules lead to good data.)
 *
 */
public class PingSourceManager
{
    //
    private final Map pingAverages = new HashMap(); //<Source,PingSourceStats>
    private long lastPingRemoval=0;
    private static final long TIME_BETWEEN_BAD_PING_REMOVALS = 2 * 60 * 1000; // two minutes.
    private static final long TIME_BETWEEN_SLOW_PING_REMOVALS = 5 * 60 * 1000;// fifteen minutes.
    private static final long TIME_BETWEEN_FORCED_CYCLE_REMOVALS = 30 * 60 * 1000;// thirty minutes.


    /**
     * Determine if we should drop any ping sources.
     * Sort them, if one significantly higher then the other two. then drop it.
     * @param sources - SpeedManagerPingSource[] inputs
     */
    public void checkPingSources(SpeedManagerPingSource[] sources){

        //if the long term average of one source is 10 the lowest and twice a large as the
        //two lowest then drop the highest at the moment. Also, don't force sources to
        //drop to frequently.

        //no sources.
        if( sources==null ){
            return;
        }

        //if we have only two sources then don't do this test.
        if( sources.length<3 ){
            return;
        }

        //Test for a very bad ping source. i.e. slowest source is 10x slower then the fastest,
        if( checkForBadPing(sources) ){
            return;
        }

        //Test for slower then average source. i.e. slowest source is 3x media.
        if( checkForSlowSource(sources) ){
            return;
        }

        //Even if everything is going well then force a change every 30 minutes.
        forcePingSourceChange(sources);

    }//checkPingSources


    /**
     * If one ping source is twice the fastest then replace it. Otherwise reset the timer.
     * @param sources -
     * @return - true is a souce has been changed.
     */
    private boolean forcePingSourceChange(SpeedManagerPingSource[] sources){

        //We only apply this rule if nothing has been removed in the past 30 minutes.
        long currTime = SystemTime.getCurrentTime();
        if( currTime<lastPingRemoval+ TIME_BETWEEN_FORCED_CYCLE_REMOVALS){
            return false;
        }

        if(sources.length<3){
            return false;
        }

        //just find the slowest ping-source and remove it.
        SpeedManagerPingSource slowestSource = null;
        double slowestPing = 0.0;
        double fastestPing = 10000.0;

        int len = sources.length;
        for(int i=0; i<len; i++){
            PingSourceStats pss = (PingSourceStats) pingAverages.get(sources[i]);
            Average ave = pss.getHistory();
            double pingTime = ave.getAverage();

            //find slowest
            if( pingTime>slowestPing ){
                slowestPing = pingTime;
                slowestSource=sources[i];
            }

            //find sped of fastest.
            if( pingTime<fastestPing ){
                fastestPing = pingTime;
            }

        }//for

        //regardless of result, resetTimer the timer.
        resetTimer();
        //only replace the slowest if it is twice the fastest.
        if( slowestPing > 2*fastestPing ){
            if(slowestSource!=null){
                slowestSource.destroy();
                return true;
            }
        }

        return false;
    }//forcePingSourceChange

    /**
     * A slow source is something that is 2x the slower then the two fastest.
     * @param sources -
     * @return - true is a source has been removed.
     */
    private boolean checkForSlowSource(SpeedManagerPingSource[] sources){

        //We only apply this rule if nothing has been removed in the past 15 minutes.
        long currTime = SystemTime.getCurrentTime();
        if( currTime<lastPingRemoval+ TIME_BETWEEN_SLOW_PING_REMOVALS){
            return false;
        }

        SpeedManagerPingSource slowestSource = null;
        if( sources.length<3 ){
            return false;
        }

        double fastA = 10000.0;
        double fastB = 10000.0;
        double slowest = 0.0;
        int len = sources.length;
        for(int i=0; i<len; i++){
            PingSourceStats pss = (PingSourceStats) pingAverages.get(sources[i]);
            Average ave = pss.getHistory();
            double pingTime = ave.getAverage();

            //determine fastest or second fastest.
            if(pingTime<fastA){
                fastB=fastA;
                fastA=pingTime;
            }else if(pingTime<fastB){
                fastB=pingTime;
            }

            //determine slowest.
            if(pingTime>slowest){
                slowest = pingTime;
                slowestSource = sources[i];
                resetTimer();
            }
        }//for

        double sumFastest = fastA+fastB;

        boolean removedSource = false;
        if( sumFastest*2 < slowest ){
            //destroy this source. It is a bit too slow.
            if(slowestSource!=null){
                slowestSource.destroy();
                SpeedManagerLogger.log("dropping ping source: "+slowestSource.getAddress()+" for being 2x slower then two fastest.");
                removedSource = true;
                resetTimer();
            }
        }//if

        return removedSource;
    }//checkForSlowSource

    /**
     * If the slowest ping in 10x the fastest then remove it.
     * @param sources -
     * @return - true is a source has been removed.
     */
    private boolean checkForBadPing(SpeedManagerPingSource[] sources) {

        //if we just recently removed a ping source then wait.
        long currTime = SystemTime.getCurrentTime();
        if( currTime<lastPingRemoval+ TIME_BETWEEN_BAD_PING_REMOVALS){
            return false;
        }

        double highestLongTermPing=0.0;
        SpeedManagerPingSource highestSource=null;
        double lowestLongTermPing=10000.0;

        int len = sources.length;
        for(int i=0; i<len; i++){
            PingSourceStats pss = (PingSourceStats) pingAverages.get(sources[i]);
            Average a = pss.getLongTermAve();
            double avePingTime = a.getAverage();

            //is this a new highest value?
            if( avePingTime>highestLongTermPing ){
                highestLongTermPing = avePingTime;
                highestSource = sources[i];
            }

            //is this a new lowest value?
            if( avePingTime<lowestLongTermPing ){
                lowestLongTermPing = avePingTime;
            }
        }//for

        boolean removedSource = false;
        //if the highest value is 8x the lowest then find another source.
        if( lowestLongTermPing*8 < highestLongTermPing ){
            //remove the slow source we will get a new one to replace it.
            if( highestSource!=null ){
                SpeedManagerLogger.log("dropping ping source: "+highestSource.getAddress()+" for being 8x greater then min source.");
                highestSource.destroy();
                removedSource = true;
                resetTimer();
            }
        }//if

        return removedSource;
    }


    public void pingSourceFound(SpeedManagerPingSource source, boolean is_replacement){
        PingSourceStats pss = new PingSourceStats(source);
        pingAverages.put(source,pss);
    }

    public void pingSourceFailed(SpeedManagerPingSource source) {
        if( pingAverages.remove(source)==null){
            SpeedManagerLogger.log("didn't find source: "+source.getAddress().getHostName());
        }
    }

    public void addPingTime(SpeedManagerPingSource source){

        if(source==null){
            return;
        }

        PingSourceStats pss = (PingSourceStats) pingAverages.get(source);

        if(pss==null){
            pingSourceFound(source,false);
            pss = (PingSourceStats) pingAverages.get(source);
            SpeedManagerLogger.trace("added new source from addPingTime.");
        }

        int pingTime = source.getPingTime();
        if(pingTime>0){
            pss.addPingTime( source.getPingTime() );
        }

    }//addPingTime

    /**
     * After a ping-source has been removed, need to resetTimer the timer.
     */
    private void resetTimer(){
        lastPingRemoval = SystemTime.getCurrentTime();
    }

}
