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

/**
 * Created on Jun 14, 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.
 */

/**
 * Classifies the ping-times and then maps them against the a
 * grid of upload and download rates.
 *
 * Create a two dimensional map of upload and download rates. Map onto this
 * space ping times.
 *
 * The mesh size will be smaller near zero, and larger higher up.
 *
 * 0 - 100 kByte/sec   - 10 kBytes mesh size.
 * 100 - 500 kBytes/sec - 50 kBytes mesh size.
 * 500 - 5000 kBytes/sec - 100 kBytes mesh size.
 * Anything above 5 MBytes/sec is one region.
 *
 */
public class PingSpaceMapper
{

    //ToDo: use the SpeedManagerPingMapper interface and move this up a level.

    GridRegion[][] gridRegion; //here upIndex,downIndex

    int lastDownloadBitsPerSec;
    int lastUploadBitsPerSec;

    int goodPingInMilliSec;
    int badPingInMilliSec;

    int totalPointsInMap = 0;

    private static final int maxMeshIndex=70;

    /**
     * Create a grid and define good and bad ping times.
     * @param _goodPingInMilliSec -
     * @param _badPingInMilliSec -
     */
    public PingSpaceMapper(int _goodPingInMilliSec, int _badPingInMilliSec){

        createNewGrid();

        goodPingInMilliSec = _goodPingInMilliSec;
        badPingInMilliSec = _badPingInMilliSec;
    }


    private void createNewGrid() {
        //create the mesh. We will have 70 by 70 grid. Even though we only use 63.
        gridRegion = null;
        gridRegion = new GridRegion[maxMeshIndex][maxMeshIndex];
        for(int upIndex=0;upIndex<maxMeshIndex;upIndex++){
            for(int downIndex=0;downIndex<maxMeshIndex;downIndex++){
                gridRegion[upIndex][downIndex] = new GridRegion();
            }
        }
    }


    /**
     * We have a hard coded mesh.
     * 0-9999 = 0, 10000-
     *
     *
     * @param bitsPerSec -
     * @return - mesh index.
     */
    private int convertBitsPerSec2meshIndex(int bitsPerSec){

        if( bitsPerSec<0){
            return 0;
        }

        int bytesPerSec = bitsPerSec/1024;

        if( bytesPerSec<100){
            return bytesPerSec/10;
        }else if(bytesPerSec<500){
            return 10 + ((bytesPerSec-100)/50);
        }else if(bytesPerSec<5000){
            return 10 + 8 + ((bytesPerSec-500)/100);
        }

        //return max mesh index.
        return 10 + 8 + 45;
    }//convertBitsPerSec2meshIndex

    /**
     * The reverse of bit/sec -> mesh index calculation.
     * @param meshIndex - value between 0 and 70
     * @return lowest BitsPerSecond that meets that criteria.
     */
    private int convertMeshIndex2bitsPerSec(int meshIndex){

        int bytesPerSec=0;
        if(meshIndex<=0){
            return 0;
        }

        if(meshIndex<=10){
            bytesPerSec = meshIndex*10;
        }else if(meshIndex<=18){
            bytesPerSec = 100 + (meshIndex-10) * 50;
        }else{
            bytesPerSec = 500 + (meshIndex-18) * 100;
        }

        return bytesPerSec*1024;
    }//convertMeshIndex2bitsPerSec

    public void setCurrentTransferRates(int downloadBitPerSec, int uploadBitsPerSec){
        lastDownloadBitsPerSec = downloadBitPerSec;
        lastUploadBitsPerSec = uploadBitsPerSec;
    }

    public void addMetricToMap(int metric){

        int downIndex = convertBitsPerSec2meshIndex(lastDownloadBitsPerSec);
        int upIndex = convertBitsPerSec2meshIndex(lastUploadBitsPerSec);

        totalPointsInMap++;

        if( metric<goodPingInMilliSec ){
            gridRegion[upIndex][downIndex].incrementMetricCount( GridRegion.INDEX_PING_GOOD );
        }else if( metric<badPingInMilliSec ){
            gridRegion[upIndex][downIndex].incrementMetricCount( GridRegion.INDEX_PING_NEUTRAL );
        }else{
            gridRegion[upIndex][downIndex].incrementMetricCount( GridRegion.INDEX_PING_BAD );
        }

    }//addMetricToMap

    /**
     * Start accumlating data from scratch.
     */
    public void reset(){
        totalPointsInMap=0;
        createNewGrid();
    }

    public static final int RESULT_UPLOAD_INDEX = 0;
    public static final int RESULT_DOWNLOAD_INDEX = 1;

    private Result getHighestMeshIndexWithGoodPing(){
       Result[] retVal = calculate();
       return retVal[GOOD_PING_INDEX];
    }


    private Result getHighestMeshIndexWithAnyPing(){
        Result[] retVal = calculate();
        return retVal[ANY_PING_INDEX];
    }

    /**
     * Try to determine if a chocking ping occured during this test.
     * @param isDownloadTest - set true if this is a download_search_test. set false if upload search test.
     * @return - true if it appears a chocking ping occured.
     */
    public boolean hadChockingPing(boolean isDownloadTest){

        Result[] res = calculate();

        int goodPingIndex;
        int highPingIndex;

        if( isDownloadTest ){
            goodPingIndex = res[GOOD_PING_INDEX].getDownloadIndex();
            highPingIndex = res[ANY_PING_INDEX].getDownloadIndex();
        }else{
            goodPingIndex = res[GOOD_PING_INDEX].getUploadIndex();
            highPingIndex = res[ANY_PING_INDEX].getUploadIndex();
        }

        return (highPingIndex>goodPingIndex);
    }


    static final int GOOD_PING_INDEX = 0;
    static final int ANY_PING_INDEX = 1;

    /**
     * Look at the Map and find the highest index for each catagory.
     * @return Result[2], where index 0 is goodPing, index 1 is anyPing
     */
    private Result[] calculate(){

        Result[] retVal = new Result[2];
        retVal[GOOD_PING_INDEX] = new Result();
        retVal[ANY_PING_INDEX] = new Result();

        for(int upIndex=0;upIndex<maxMeshIndex;upIndex++){
            for(int downIndex=0;downIndex<maxMeshIndex;downIndex++)
            {
                //Register this grid point if it has more good then bad pings.
                float rating = gridRegion[upIndex][downIndex].getRating();
                if( rating>0.0f ){
                    retVal[GOOD_PING_INDEX].checkAndUpdate(upIndex,downIndex);
                }

                //Register this grid point if it has any ping values.
                int count = gridRegion[upIndex][downIndex].getTotal();
                if( count>0 ){
                    retVal[ANY_PING_INDEX].checkAndUpdate(upIndex,downIndex);
                }

            }//for
        }//for

        return retVal;
    }

    /**
     * Make a guess at the upload capacity based on metric data.
     * @return -
     */
    public int guessUploadLimit(){

        Result result = getHighestMeshIndexWithGoodPing();
        int upMeshIndex = result.getUploadIndex();
        return convertMeshIndex2bitsPerSec( upMeshIndex );
    }

    /**
     * Make a guess at the download capacity based on metric data.
     * @return -
     */
    public int guessDownloadLimit(){

        Result result = getHighestMeshIndexWithGoodPing();
        int downMeshIndex = result.getDownloadIndex();
        return convertMeshIndex2bitsPerSec( downMeshIndex );
    }

    /**
     * Class to return a result.
     */
    static class Result{
        int highestUploadIndex=0;
        int highestDownloadIndex=0;

        /**
         * If the input index is higher then stored, update it with the new value.
         * @param uploadIndex -
         * @param downloadIndex -
         */
        public void checkAndUpdate(int uploadIndex, int downloadIndex){

            if( uploadIndex>highestUploadIndex ){
                highestUploadIndex=uploadIndex;
            }

            if( downloadIndex>highestDownloadIndex ){
                highestDownloadIndex=downloadIndex;
            }
        }

        public int getUploadIndex(){
            return highestUploadIndex;
        }

        public int getDownloadIndex(){
            return highestDownloadIndex;
        }
    }//class Result

    /**
     * A region on the grid for accumulating counts.
     */
    static class GridRegion{

        public static final int INDEX_PING_GOOD = 0;
        public static final int INDEX_PING_NEUTRAL = 1;
        public static final int INDEX_PING_BAD = 2;

        int pingCount[] = new int[3];
        int uploadBound[] = new int[2];
        int downloadBound[] = new int[2];

        public void incrementMetricCount( int pingIndex){
            if(pingIndex>=0 && pingIndex<=3){
                ++pingCount[pingIndex];
            }
        }//incrementMetricCount

        public float getRating(){

            int total = getTotal();

            if( total==0 ){
                return 0.0f;
            }

            float score = (pingCount[INDEX_PING_GOOD]
                    + 0.3f * pingCount[INDEX_PING_NEUTRAL]
                    - pingCount[INDEX_PING_BAD]);

            return ( score / (float) total );
        }//getRating

        public int getTotal(){

            return  pingCount[INDEX_PING_GOOD]
                   +pingCount[INDEX_PING_NEUTRAL]
                   +pingCount[INDEX_PING_BAD];
        }//getTotal

    }//class

}
