/*
 *  PHEX - The pure-java Gnutella-servent
 *  Copyright (C) 2001 - 2004 Gregor Koukkoullis ( phex <at> kouk <dot> de )
 *
 *  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
 */
package phex.msg;

import java.io.IOException;

import phex.common.IntObj;
import phex.host.*;
import phex.host.HostManager;
import phex.statistic.*;
import phex.utils.*;

/**
 * <p>A pong response.</p>
 *
 * <p>This encapsulates a Gnutella message that informs this servent of the
 * vital statistics of a Gnutella host. Pongs should  only ever be received
 * in response to pings.</p>
 *
 * <p>This implementation handles GGEP extension blocks.</p>
 */
public class MsgPong extends Message
{
    /**
     * Vendor code GGEP extension in GUESS format
     */
    private static final byte[] GGEP_VENDOR_CODE = new byte[5];
    static
    {
        // add vendor code 'PHEX'
        GGEP_VENDOR_CODE[ 0 ] = (byte) 0x50;
        GGEP_VENDOR_CODE[ 1 ] = (byte) 0x48;
        GGEP_VENDOR_CODE[ 2 ] = (byte) 0x45;
        GGEP_VENDOR_CODE[ 3 ] = (byte) 0x58;
        GGEP_VENDOR_CODE[4] = IOUtil.serializeGUESSVersionFormat(
            VersionUtils.getMajorVersionNumber(),
            VersionUtils.getMinorVersionNumber() );
    }
    
    /**
     * <p>The un-parsed body of the message.</p>
     */
    private byte[] body;

    private MsgHeader header;

    private int port;
    private byte[] ip;
    private long fileCount;
    private long fileSizeInKB;
    private int avgDailyUptime;


    public MsgPong( MsgHeader aHeader, byte[] payload )
    {
        header = aHeader;
        header.setPayloadType( MsgHeader.PONG_PAYLOAD );

        body = payload;
        header.setDataLen( body.length );
        
        avgDailyUptime = -1;

        // parse the body
        parseBody();
    }

    /**
     * <p>Create a new MsgInitResponse.</p>
     *
     * <p>The header will be modified so that its function property becomes
     * MsgHeader.sInitResponse. The header argument is owned by this object.</p>
     *
     * @param header  the MsgHeader to associate with the new message
     */
    // TODO2 provide more constructors for different kind of Pongs we send
    // currently all Pongs get GGEP extensions. This is not necessary.
    public MsgPong( MsgHeader header, byte[] ip, int port, int fileCount,
        int fileSizeInKB )
    {
        this.header = header;
        header.setPayloadType( MsgHeader.PONG_PAYLOAD );

        if( ip.length != 4 )
        {
            throw new IllegalArgumentException(
                "Can't accept ip that is not 4 bytes in length: " +
                ip.length );
        }
        this.ip = ip;
        this.port = port;
        this.fileCount = fileCount;
        
        StatisticsManager statMgr = StatisticsManager.getInstance();
        StatisticProvider uptimeProvider = statMgr.getStatisticProvider(
                StatisticsManager.DAILY_UPTIME_PROVIDER );
        avgDailyUptime = ((IntObj)uptimeProvider.getValue()).intValue();

        boolean isUltrapeer = HostManager.getInstance().isUltrapeer();
        if ( isUltrapeer )
        {
            this.fileSizeInKB = createUltrapeerMarking( fileSizeInKB );
        }
        else
        {
            this.fileSizeInKB = fileSizeInKB;
        }

        buildBody();
        header.setDataLen( body.length );
    }

    public MsgHeader getHeader()
    {
        return header;
    }

    /**
     * Get the port that the remote host will listen on.
     *
     * @return  the current port number as a short
     */
    public int getPort()
    {
        return port;
    }

    /**
     * <p>Get the four byte image of the ip address of the Gnutella servent.</p>
     *
     * <p><em>Important:</em> Do not modify the return value. It is owned by
     * this object.</p>
     *
     * @return the four byte image of this servent's ip address
     */
    public byte[] getIP()
    {
        return ip;
    }

    /**
     * Get the number of files served from this servent.
     *
     * @return  a zero or positive integer giving the number of files served
     */
    public long getFileCount()
    {
        return fileCount;
    }

    /**
     * Get the number of bytes served by this servent.
     *
     * @return  the number of bytes served
     */
    public long getFileSizeInKB()
    {
        return fileSizeInKB;
    }
    
    /**
     * @return
     */
    public int getDailyUptime()
    {
        return avgDailyUptime;
    }

    public int getSize()
    {
        return header.getSize() + header.getDataLen();
    }

    public void writeMessage( GnutellaOutputStream outStream )
        throws IOException
    {
        header.writeHeader( outStream );
        outStream.write( body, 0, body.length );
    }

    public String getDebugString()
    {
        return "Pong[ IP=" + IPUtils.ip2string( ip ) +
            ", Port=" + port +
            ", FileCount=" + fileCount +
            ", FileSize=" + fileSizeInKB +
            ", AvgUptime=" + avgDailyUptime +
            ", HEX=[" + HexConverter.toHexString( body ) +
            "] ]";
    }

    private void buildBody()
    {
        // first create GGEP block...
        GGEPBlock ggepBlock = new GGEPBlock();
        
        // add daily avg. uptime.
        if ( avgDailyUptime > 0 )
        {
            ggepBlock.addExtension( GGEPBlock.AVARAGE_DAILY_UPTIME, avgDailyUptime );
        }
        
        HostManager hostMgr = HostManager.getInstance();
        if ( hostMgr.isUltrapeer() )
        {
            // add UP GGEP extension.
            byte[] upExtension = new byte[3];
            upExtension[0] = IOUtil.serializeGUESSVersionFormat(
                VersionUtils.getUltrapeerMajorVersionNumber(),
                VersionUtils.getUltrapeerMinorVersionNumber() ); 
                
            NetworkHostsContainer networkHostsContainer = hostMgr.getNetworkHostsContainer();
            upExtension[1] = (byte) networkHostsContainer.getOpenLeafSlotsCount();
            upExtension[2] = (byte) networkHostsContainer.getOpenUltrapeerSlotsCount();
            
            ggepBlock.addExtension( GGEPBlock.ULTRAPEER_ID, upExtension );
        }
        
        // add vendor info
        ggepBlock.addExtension( GGEPBlock.VENDOR_CODE_ID, GGEP_VENDOR_CODE );
        
        byte[] ggepExtension;
        int extensionLength;
        try
        {
            ggepExtension = ggepBlock.getBytes();
            extensionLength = ggepExtension.length;
        }
        catch ( IOException exp )
        {
            Logger.logError(exp);
            extensionLength = 0;
            ggepExtension = null;
        }
        
        body = new byte[ 14 + extensionLength ];

        IOUtil.serializeShortLE( (short)port, body, 0 );

        System.arraycopy( ip, 0, body, 2, 4 );

        IOUtil.serializeIntLE( (int)fileCount, body, 6);
        IOUtil.serializeIntLE( (int)fileSizeInKB, body, 10);
        
        if ( ggepExtension != null )
        {
            System.arraycopy( ggepExtension, 0, body, 14, extensionLength );
        }   
    }

    private void parseBody()
    {
        port = IOUtil.unsignedShort2Int(IOUtil.deserializeShortLE( body, 0 ));

        ip = new byte[4];
        ip[0] = body[2];
        ip[1] = body[3];
        ip[2] = body[4];
        ip[3] = body[5];

        fileCount = IOUtil.unsignedInt2Long( IOUtil.deserializeIntLE( body, 6 ) );
        fileSizeInKB = IOUtil.unsignedInt2Long( IOUtil.deserializeIntLE( body, 10 ) );
        
        // parse possible GGEP data
        if ( body.length <= 14 )
        {
            return;
        }
        
        parseGGEPBlocks();
    }
    
    private void parseGGEPBlocks()
    {
        GGEPBlock[] ggepBlocks;
        try
        {
            ggepBlocks = GGEPBlock.parseGGEPBlocks( body, 14 );
        }
        catch ( InvalidGGEPBlockException exp )
        {// ignore and continue parsing...
            Logger.logMessage( Logger.FINE, Logger.NETWORK, exp );
            return;
        }
        
        if ( GGEPBlock.isExtensionHeaderInBlocks( ggepBlocks, GGEPBlock.AVARAGE_DAILY_UPTIME ) )
        {
            byte[] data = GGEPBlock.getExtensionDataInBlocks( ggepBlocks, GGEPBlock.AVARAGE_DAILY_UPTIME );
            if ( data != null )
            {
                try
                {
                    avgDailyUptime = IOUtil.deserializeIntLE( data, 0, data.length );
                }
                catch ( NumberFormatException exp )
                {
                    Logger.logWarning( Logger.NETWORK, exp, "Invalid average uptime GGEP extension data: " +
                        HexConverter.toHexString( data ) );
                    avgDailyUptime = -1;
                }
            }
        }
    }

    /**
     * Returns true if this pong is marking a ultrapeer. This is the case when
     * fileSizeInKB is a power of two but at least 8.
     * @return true if this pong is marking a ultrapeer.
     */
    public boolean isUltrapeerMarked()
    {
        if ( fileSizeInKB < 8 )
        {
            return false;
        }
        return ( fileSizeInKB & (fileSizeInKB - 1 ) ) == 0;
    }

    /**
     * Sets the ultrapeer kbytes field for ultrapeers.
     * This is done by returning the nearest power of two of the kbytes field.
     * A kbytes value of 1536 would return 1024.
     *                   1535              512.
     */
    private static int createUltrapeerMarking( int kbytes )
    {
        if ( kbytes < 12 )
        {
            return 8;
        }
        // first get the bit count of the value and substract 1...
        int bitCount = IOUtil.determineBitCount( kbytes );
        // calculate the power of two...
        int power = (int)Math.pow( 2, bitCount );
        // now determine the border value of the exponent...
        int minBorder = power - (power / 4);
        if ( kbytes < minBorder )
        {
            power = (int)Math.pow( 2, bitCount-1 );
        }
        return power;
    }

}