/*
 *  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.*;
import java.net.*;

import org.apache.crimson.util.XmlChars;


import phex.common.*;
import phex.connection.*;
import phex.host.*;
import phex.query.*;
import phex.upload.*;
import phex.utils.*;

/**
 * <p>A response to a query message.</p>
 *
 * <p>These should have message IDs matching the query message it is responding
 * to.</p>
 *
 * <p>This class does implement the EQHD extention. It also support the GGEP
 * extension.
 * </p>
 */
public class MsgQueryResponse extends Message implements QueryConstants
{
    private static final boolean INCLUDE_QHD = true;

    /**
     * push flag mask for beashare metadata.
     */
    private static final byte PUSH_NEEDED_MASK=(byte)0x01;
    /**
     * busy flag mask for beashare metadata.
     */
    private static final byte SERVER_BUSY_MASK=(byte)0x04;
    /**
     * upload flag mask for beashare metadata.
     */
    private static final byte HAS_UPLOADED_MASK=(byte)0x08;
    /**
     * speed flag mask for beashare metadata.
     */
    private static final byte UPLOAD_SPEED_MASK=(byte)0x10;
    /**
     * ggep flag mask for QHD.
     */
    private static final byte GGEP_MASK=(byte)0x20;
    /**
     * chat flag mask for limewire and shareaza metadata.
     */
    private static final byte CHAT_SUPPORTED_MASK = (byte)0x01;


    private MsgHeader mHeader;

    /**
     * <p>The un-parsed body of the query response.</p>
     *
     * <p>For queries that are being forwarded, this body will include all extra
     * data. For queries built using this API, there is no way currently to add
     * extra information to this body.</p>
     */
    private byte[] body;
    private int mRemoteHostSpeed;
    private MsgResRecord[] mRecords;

    /**
     * The host address for the query response
     */
    private HostAddress hostAddress;

    private GUID remoteClientID;

    /**
     * Defines the four character vendor code of the client that offers the
     * file.
     */
    private String vendorCode;

    /**
     * <p>Defines if a push transfer is needed or not or unknown.</p>
     *
     * <p><em>fixme:</em> Why not use a boolean? Same goes for other flags.</p>
     */
    private short pushNeededFlag;

    /**
     * Defines if a server is busy currently or unknown.
     */
    private short serverBusyFlag;

    /**
     * Defines if a the server has already uploaded a file.
     */
    private short hasUploadedFlag;

    /**
     * Defines if the upload speed of a server.
     */
    private short uploadSpeedFlag;

    /**
     * Defines if the server supportes chat.
     */
    private boolean isChatSupported;

    /**
     * Defines if the server supports browse host.
     */
    private boolean isBrowseHostSupported;

    /**
     * Defines if the body of the query response is already parsed.
     */
    private boolean isParsed;

    /**
     * Build a new MsgQueryResponse with a header, a client GUID, host address,
     * port and speed, and an array of MsgResRecord instances representing hits.
     *
     * @param header  the MsgHeader to attach, which will have its function
     *                property set to MsgHeader.sQueryResponse
     * @param clientID     the GUID of the client that requested the query
     * @param hostAddress  the InetAddress of the responding servent
     * @param hostPort     the port as short of the responding servent
     * @param speed        the speed of the responding servent
     * @param records      an array of MsgResRecord objects representing hits to
     *                     the query which must be shorter than 256
     * @throws IllegalArgumentException if there are more than 255 records
     * @throws UnknownHostException if host ip address cant be resolved.
     */
    public MsgQueryResponse( MsgHeader header, GUID clientID,
        HostAddress aHostAddress, int speed, MsgResRecord records[] )
        throws UnknownHostException // if ip cant be resolved
    {
        if( records.length > 255 )
        {
            throw new IllegalArgumentException(
                "A maximum of 255 records can be associated with a single " +
                "response: " + records.length );
        }

        mHeader = header;
        mHeader.setPayloadType(MsgHeader.QUERY_HIT_PAYLOAD);

        remoteClientID = clientID;
        hostAddress = aHostAddress;
        mRemoteHostSpeed = speed;
        mRecords = records;

        // when we are behind firewall or have not accepted an incoming connection
        // like defined in the protocol
        boolean isPushNeeded;
        if ( NetworkManager.getInstance().hasConnectedIncoming() )
        {
            isPushNeeded = false;
            pushNeededFlag = QHD_FALSE_FLAG;
        }
        else
        {
            isPushNeeded = true;
            pushNeededFlag = QHD_TRUE_FLAG;
        }

        boolean isServerBusy = UploadManager.getInstance().isHostBusy();

        buildBody( isPushNeeded, isServerBusy );
        mHeader.setDataLen( body.length );
        isParsed = true;
    }

    /**
     * <p>Create a query response with its header and body.</p>
     *
     * <p>The header becomes owned by this message. Its function property will
     * be set to MsgHeader.sQueryResponse.</p>
     *
     * <p>The body is not parsed directly
     * cause some queries are just forwarded without the need of beeing completely
     * parsed. This allows the extention data (such as GGEP blocks) to be
     * forwarded despite there being no API to modify these.</p>
     *
     * @param header  the MsgHeader to use as header
     * @throws InvalidMessageException
     */
    public MsgQueryResponse( MsgHeader header, byte[] aBody )
    	throws InvalidMessageException
    {
        mHeader = header;
        mHeader.setPayloadType(MsgHeader.QUERY_HIT_PAYLOAD);

        body = aBody;
        header.setDataLen( body.length );
        
        int port = IOUtil.unsignedShort2Int( IOUtil.deserializeShortLE( body, 1 ) );
        if ( !HostAddress.isPortInRange( port ) )
        {
            throw new InvalidMessageException( "Port out of range: " + port );
        }
        hostAddress = new HostAddress( getHostIP(), port );
        
        isParsed = false;
    }

    private void buildBody( boolean isPushNeeded, boolean isServerBusy )
        throws UnknownHostException // if ip cant be resolved
    {
        int size = calculateBodySize();
        body = new byte[ size ];

        int recordCount = mRecords.length;
        body[ 0 ] = (byte) recordCount;
        IOUtil.serializeShortLE( (short)hostAddress.getPort(), body, 1 );

        byte[] ipAddress = hostAddress.getHostIP();
        body[ 3 ] = ipAddress[ 0 ];
        body[ 4 ] = ipAddress[ 1 ];
        body[ 5 ] = ipAddress[ 2 ];
        body[ 6 ] = ipAddress[ 3 ];
        IOUtil.serializeIntLE( mRemoteHostSpeed, body, 7 );

        int offset = 11;

        for (int i = 0; i < recordCount; i++)
        {
            offset = mRecords[ i ].serialize(body, offset);
        }

        if ( INCLUDE_QHD )
        {
            // add vendor code 'PHEX'
            body[ offset++ ] = (byte) 0x50;
            body[ offset++ ] = (byte) 0x48;
            body[ offset++ ] = (byte) 0x45;
            body[ offset++ ] = (byte) 0x58;
            // open data length
            body[ offset ++ ] = (byte) 2;
            // open data flags
            byte isPushNeededByte = (byte) 0;
            if ( isPushNeeded )
            {
                isPushNeededByte = PUSH_NEEDED_MASK;
            }
            byte isServerBusyByte = (byte) 0;
            if ( isServerBusy )
            {
                isServerBusyByte = SERVER_BUSY_MASK;
            }
            byte isGGEPUsedByte = (byte)0;
            if ( ServiceManager.sCfg.mShareBrowseDir )
            {
                isGGEPUsedByte = GGEP_MASK;
            }

            body[ offset++ ] = (byte) (
                  isPushNeededByte
                | SERVER_BUSY_MASK
                | 0 //HAS_UPLOADED_MASK we dont know that yet
                // we know we never measured that speed
                | UPLOAD_SPEED_MASK
                | GGEP_MASK );
            body[ offset++ ] = (byte) (
                  PUSH_NEEDED_MASK
                | isServerBusyByte
                | 0 //(hasUploadedSuccessfully ? HAS_UPLOADED_MASK : 0)
                // we know we never measured that speed
                | 0 //(isSpeedMeasured ? UPLOAD_SPEED_MASK : 0));
                | isGGEPUsedByte );

            // private QHD area
            // mark for chat able.
            if ( ServiceManager.sCfg.isChatEnabled )
            {
                body[ offset++ ] = (byte) 0x01;
            }
            else
            {
                body[ offset++ ] = (byte) 0x00;
            }

            //GGEP block
            if ( ServiceManager.sCfg.mShareBrowseDir )
            {
                byte[] ggepBytes = GGEPBlock.getBrowseHostGGEPBlock();
                System.arraycopy( ggepBytes, 0, body, offset, ggepBytes.length);
                offset += ggepBytes.length;
            }
        }
        offset = remoteClientID.serialize( body, offset );
    }

    public MsgHeader getHeader()
    {
        return mHeader;
    }

    /**
     * <p>Get the number of response records attached to this response.</p>
     *
     * <p><em>fixme:</em> This should be a byte, not an int. It must be within
     * the range 0-255.</p>
     */
    public short getRecordCount()
    {
        // instead of parsing the complete body we will only read out the
        // record length. This is much more performant.
        return (short)IOUtil.unsignedByte2int( body[0] );
    }

    /**
     * Returns the IP address of the host as a byte array.
     * The body is not parsed to get this data.
     */
    public byte[] getHostIP()
    {
        byte[] ip = new byte[4];
        // the ip starts at byte 3
        ip[0] = body[3];
        ip[1] = body[4];
        ip[2] = body[5];
        ip[3] = body[6];
        return ip;
    }

    public HostAddress getHostAddress()
    {
        return hostAddress;
    }

    /**
     * Get the speed of the remote servent in kb/sec.
     *
     * @return the remote host speed
     */
    public int getRemoteHostSpeed()
    {
        parseBody();
        return mRemoteHostSpeed;
    }

    /**
     * <p>If present, return the vendor code of the servent, otherwise null.</p>
     *
     * <p>For example, responses arrising from phex will have a vendor code of
     * "PHEX".</p>
     *
     * @return the vendor code
     */
    public String getVendorCode()
    {
        parseBody();
        return vendorCode;
    }

    /**
     * <p>States wether the servent will require push to fetch data.</p>
     *
     * <p>A servent will require push if it can not accept TCP connections to
     * fetch the matching resource because, for example, it is the other side of
     * a fire wall.</p>
     * Since this field can hold three states ( QHD_TRUE_FLAG, QHD_FALSE_FLAG,
     * QHD_UNKNOWN_FLAG ) and all three states are evaluated a boolean can't be used.
     *
     * @return a short that will be one of the QueryConstants QHD_UNKNOWN_FLAG,
     *         QHD_TRUE_FLAG or QHD_FALSE_FLAG
     */
    public short getPushNeededFlag()
    {
        parseBody();
        return pushNeededFlag;
    }

    /**
     * <p>States wether the servent is currently buisy.</p>
     *
     * <p>A servent is buisy if all available upload slots are filled.</p>
     *
     * Since this field can hold three states ( QHD_TRUE_FLAG, QHD_FALSE_FLAG,
     * QHD_UNKNOWN_FLAG ) and all three states are evaluated a boolean can't be used.
     *
     * @return a short that will be one of the QueryConstants QHD_UNKNOWN_FLAG,
     *         QHD_TRUE_FLAG or QHD_FALSE_FLAG
     */
    public short getServerBusyFlag()
    {
        parseBody();
        return serverBusyFlag;
    }

    /**
     * <p>States wether the servent has ever successfuly uploaded any file.</p>
     *
     * Since this field can hold three states ( QHD_TRUE_FLAG, QHD_FALSE_FLAG,
     * QHD_UNKNOWN_FLAG ) and all three states are evaluated a boolean can't be used.
     *
     * @return a short that will be one of the QueryConstants QHD_UNKNOWN_FLAG,
     *         QHD_TRUE_FLAG or QHD_FALSE_FLAG
     */
    public short getHasUploadedFlag()
    {
        parseBody();
        return hasUploadedFlag;
    }

    /**
     * <p>States wether the servent calculates its upload speed as the highest
     * average upload speed of the last 10 uploads, or as a user-defined value.
     * </p>
     * Since this field can hold three states ( QHD_TRUE_FLAG, QHD_FALSE_FLAG,
     * QHD_UNKNOWN_FLAG ) and all three states are evaluated a boolean can't be used.
     *
     * @return a short that will be one of the QueryConstants QHD_UNKNOWN_FLAG,
     *         QHD_TRUE_FLAG or QHD_FALSE_FLAG
     */
    public short getUploadSpeedFlag()
    {
        parseBody();
        return uploadSpeedFlag;
    }

    /**
     * <p>States wether the servent supportes chat connections.</p>
     *
     * @return true if a servent supports chat connections false otherwise.
     */
    public boolean isChatSupported()
    {
        parseBody();
        return isChatSupported;
    }

    /**
     * <p>States wether the servent supportes browse host.</p>
     *
     * @return true if a servent supports browse host connections false otherwise.
     */
    public boolean isBrowseHostSupported()
    {
        parseBody();
        return isBrowseHostSupported;
    }

    /**
     * Get the GUID of the remote servent.
     *
     * @return the GUID of the remote client
     */
    public GUID getRemoteClientID()
    {
        if ( remoteClientID == null )
        {
            parseRemoteClientID();
        }
        return remoteClientID;
    }

    /**
     * Get the i'th MsgResRecord that encapsulates the i'th hit to the query in
     * this response.
     *
     * @param i  the index of the record to fetch
     * @throws IndexOutOfBoundsException  if i is negative or not less than
     *         getRecordCount()
     */
    public MsgResRecord getMsgRecord( int i )
    {
        parseBody();
        return mRecords[ i ];
    }

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

    /**
     * 11 byte query response header size plus file record size
     * plus optional 7 byte QHD plus 16-byte client id
     */
    private int calculateBodySize()
    {
        // header ( # hits, port, ip, speed )
        int	size = 11;
        // record size
        size += getRecordLength();
        // optional 7 byte QHD
        if ( INCLUDE_QHD )
        {
            // vendor, open data length, open data, chat flag
            size = size + 4 + 1 + 2 + 1;
        }

        // GGEEP Block if Browse host is enabled.
        if ( ServiceManager.sCfg.mShareBrowseDir )
        {
            size += GGEPBlock.getBrowseHostGGEPBlock().length;
        }

        // 16 byte client id
        size += remoteClientID.getSize();
        return size;
    }

    private int getRecordLength()
    {
        int size = 0;
        int recordCount = mRecords.length;
        for (int i = 0; i < recordCount; i++)
        {
            MsgResRecord rec = mRecords[ i ];
            size += rec.getSize();
        }
        return size;
    }

    /**
     * Make an independant copy of a MsgQueryResponse into this message.
     *
     * @param b  the MsgQueryResponse to copy all data from
     */
    public void copy(MsgQueryResponse b)
    {
        mHeader.copy(b.getHeader());
        hostAddress = b.hostAddress;
        mRemoteHostSpeed = b.mRemoteHostSpeed;
        remoteClientID = b.remoteClientID;
        int recordCount = b.mRecords.length;
        mRecords = new MsgResRecord[ recordCount ];
        for (int i = 0; i < recordCount; i++)
        {
            MsgResRecord rec = new MsgResRecord();
            rec.copy( b.mRecords[ i ] );
            mRecords[ i ] = rec;
        }
        body = b.body;
    }
    
    public void writeMessage( GnutellaOutputStream outStream )
        throws IOException
    {
        mHeader.writeHeader( outStream );
        outStream.write( body, 0, body.length );
    }

    private void parseBody()
    {
        if ( isParsed )
        {
            return;
        }
        // Already read the header.
        int offset = 0;
        byte n = body[offset++];
        short recordCount = (short)(n < 0 ? 256 + n : n);

        // parsing of port and ip is done on demand in getHostAddress() and
        // getHostIP()
        offset += 2; // skip port
        offset += 4; // skip ip

        try
        {
            int speed = IOUtil.deserializeIntLE(body, offset);
            if ( speed < 0 )
            {// faster then Integer.MAX_VALUE??? this is crazy...
                speed = Integer.MAX_VALUE;
            }
            mRemoteHostSpeed = speed;
            offset += 4;

            mRecords = new MsgResRecord[ recordCount ];
            for (int i = 0; i < recordCount; i++)
            {
                MsgResRecord rec = new MsgResRecord();
                offset = rec.deserialize(body, offset);
                mRecords[ i ] = rec;
            }

            // Handle Bearshare meta informations. The format is documented in
            // the GnutellaProtocol04.pdf document
            pushNeededFlag = QHD_UNKNOWN_FLAG;
            serverBusyFlag = QHD_UNKNOWN_FLAG;
            hasUploadedFlag = QHD_UNKNOWN_FLAG;
            uploadSpeedFlag = QHD_UNKNOWN_FLAG;
            // GGEP extensions
            isBrowseHostSupported = false;

            if ( offset <= (mHeader.getDataLen() - 16 - 4 - 2) )
            {
                // parse meta data
                // Use ISO encoding for two bytes characters on some platforms.
                vendorCode = new String( body, offset, 4, "ISO-8859-1");
                if ( !isVendorCodeValid( vendorCode ) )
                {
                    String hexVendorCode = HexConverter.toHexString( body, offset, 4 );
                    Logger.logMessage( Logger.WARNING, Logger.NETWORK, 
                        mHeader.getFromHost(),
                        "Illegal QHD vendor code found: " + vendorCode + " ("
                        + hexVendorCode + "). Body: " +
                        HexConverter.toHexString( body ) );
                    vendorCode = hexVendorCode;
                }
                offset += 4;

                int openDataLength = IOUtil.unsignedByte2int( body[ offset ] );
                offset += 1;

                // parse upload speed, have uploaded, busy and push
                if ( openDataLength > 1)
                {   // if we have a flag byte
                    byte flag1 = body[ offset ];
                    byte flag2 = body[ offset + 1];

                    // check if push flag is meaningfull do it reversed from other checks
                    if ( ( flag2 & PUSH_NEEDED_MASK ) != 0 )
                    {
                        if ( ( flag1 & PUSH_NEEDED_MASK ) != 0 )
                        {
                            pushNeededFlag = QHD_TRUE_FLAG;
                        }
                        else
                        {
                            pushNeededFlag = QHD_FALSE_FLAG;
                        }
                    }

                    // check if server busy flag meaningfull
                    if ((flag1 & SERVER_BUSY_MASK) != 0)
                    {
                        if ( (flag2 & SERVER_BUSY_MASK) != 0 )
                        {
                            serverBusyFlag = QHD_TRUE_FLAG;
                        }
                        else
                        {
                            serverBusyFlag = QHD_FALSE_FLAG;
                        }
                    }

                    // check if the uploaded flag is meaningfull
                    if ((flag1 & HAS_UPLOADED_MASK) != 0)
                    {
                        if ( (flag2 & HAS_UPLOADED_MASK) != 0 )
                        {
                            hasUploadedFlag = QHD_TRUE_FLAG;
                        }
                        else
                        {
                            hasUploadedFlag = QHD_FALSE_FLAG;
                        }
                    }
                    if ((flag1 & UPLOAD_SPEED_MASK) != 0 )
                    {
                        if ( (flag2 & UPLOAD_SPEED_MASK) != 0 )
                        {
                            uploadSpeedFlag = QHD_TRUE_FLAG;
                        }
                        else
                        {
                            uploadSpeedFlag = QHD_FALSE_FLAG;
                        }
                    }
                    if ((flag1 & GGEP_MASK) != 0 && (flag2 & GGEP_MASK) !=0 )
                    {// parse GGEP area should follow after open data area but
                     // we can't be sure...
                        int ggepMagicIndex = offset + 2;
                        // search for real magic index
                        while ( ggepMagicIndex < body.length )
                        {
                            if ( body[ ggepMagicIndex ] == GGEPBlock.MAGIC_NUMBER )
                            {
                                // found index!
                                break;
                            }
                            ggepMagicIndex ++;
                        }
                        GGEPBlock[] ggepBlocks = null;
                        try
                        {
                            // if there are GGEPs, see if Browse Host supported...
                            ggepBlocks = GGEPBlock.parseGGEPBlocks( body, ggepMagicIndex );
                            isBrowseHostSupported = GGEPBlock.isExtensionHeaderInBlocks(
                                ggepBlocks, GGEPBlock.BROWSE_HOST_HEADER_ID );
                        }
                        catch ( InvalidGGEPBlockException exp )
                        {// ignore and continue parsing...
                            Logger.logMessage( Logger.FINE, Logger.NETWORK, exp );
                        }
                    }
                }
                // skip unknown open data length
                offset += openDataLength;

                //Parse private area of Limewire and Shareaza to read out chat
                //flag. If chatflag is 0x1 chat is supported, if 0x0 its not.
                int privateDataLength = body.length - offset - 16;
                if ( privateDataLength > 0 &&
                   ( vendorCode.equals("LIME") || vendorCode.equals("RAZA")
                  || vendorCode.equals("PHEX") ) )
                {
                    byte flag = body[ offset ];
                    isChatSupported = ( flag & CHAT_SUPPORTED_MASK ) != 0;
                }

                //System.out.println( (mHeader.getDataLen() -16 - 4 -2) + "  " + offset + "  " +
                //    /*new String( body, offset, mHeader.getDataLen() - offset) + "   " +*/
                //    openDataLength + "  " + pushNeededFlag + "  " + serverBusyFlag + "  " +
                //    uploadSpeedFlag + "  " + hasUploadedFlag + "  " + vendorCode );
            }
            parseRemoteClientID();
            isParsed = true;
        }
        catch ( java.io.UnsupportedEncodingException exp )
        {
            return;
        }
    }

    private void parseRemoteClientID()
    {
        if ( remoteClientID == null )
        {
            remoteClientID = new GUID();
        }
        remoteClientID.deserialize(body, mHeader.getDataLen() - GUID.DATA_LENGTH);
    }

    private boolean isVendorCodeValid( String vendorCode )
    {
        // verify length
        if ( vendorCode.length() != 4 )
        {
            return false;
        }
        // verify characters
        for ( int i = 0; i < 4; i++ )
        {
            if ( !XmlChars.isChar( vendorCode.charAt( i ) ) )
            {
                return false;
            }
        }
        return true;
    }
}
