/*
 * BEncoder.java
 *
 * Created on June 4, 2003, 10:17 PM
 * Copyright (C) 2003, 2004, 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 org.gudy.azureus2.core3.util;

import java.io.*;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.util.*;

import org.gudy.azureus2.core3.xml.util.XUXmlWriter;

/**
 * A set of utility methods to encode a Map into a bencoded array of byte.
 * integer are represented as Long, String as byte[], dictionnaries as Map, and list as List.
 *
 * @author  TdC_VgA
 */
public class 
BEncoder 
{    
	private static final int BUFFER_DOUBLE_LIMIT	= 256*1024;
	
	private static final byte[] MINUS_1_BYTES = "-1".getBytes();
	
	private static volatile int non_ascii_logs;

    public static byte[] 
    encode(
    	Map object ) 
    
    	throws IOException
    {
       return( encode( object, false ));
    }    
    
    public static byte[] 
    encode(
    	Map 	object, 
    	boolean url_encode ) 
    
    	throws IOException
    {
    	BEncoder encoder = new BEncoder(url_encode);
    	
    	encoder.encodeObject( object);
    	
    	return( encoder.toByteArray());
    }  
    
    private byte[]		current_buffer		= new byte[256];
    private int			current_buffer_pos	= 0;
    private byte[][]	old_buffers;		
    
    private byte[]		int_buffer			= new byte[12];
    
    private boolean	url_encode;
    
    private
    BEncoder(
    	boolean	_url_encode )
    {
    	url_encode	= _url_encode;
    }
        
    private boolean 
	encodeObject(
		Object 					object) 
    
    	throws IOException
	{
    	
        if (object instanceof BEncodableObject) {
            object = ((BEncodableObject)object).toBencodeObject();
        }

        if ( object instanceof String || object instanceof Float){
        	
            String tempString = (object instanceof String) ? (String)object : String.valueOf((Float)object);

            	// usually this is simpler to encode by hand as chars < 0x80 map directly in UTF-8
            
            boolean	simple = true;
            
            int	char_count = tempString.length();
            
            byte[]	encoded = new byte[char_count];
            
            for (int i=0;i<char_count;i++){
            	
            	char c = tempString.charAt(i);
            	
            	if ( c < 0x80 ){
            		
            		encoded[i] = (byte)c;
            		
            	}else{
            		
            		simple = false;
            		
            		break;
            	}
            }
            
            if ( simple ){
            	
             	writeInt( char_count );
	            
	            writeChar( ':' );

            	writeBytes( encoded );
            	
            }else{
            	           	
	            ByteBuffer	bb 	= Constants.DEFAULT_CHARSET.encode( tempString );           
	            
	            writeInt( bb.limit() );
	            
	            writeChar(':');
	            
	            writeByteBuffer(bb );
            }
            
        }else if(object instanceof Map){
        	
            Map tempMap = (Map)object;
            
            SortedMap tempTree = null;
            
            	// unfortunately there are some occasions where we want to ensure that
            	// the 'key' of the map is not mangled by assuming its UTF-8 encodable.
            	// In particular the response from a tracker scrape request uses the
            	// torrent hash as the KEY. Hence the introduction of the type below
            	// to allow the constructor of the Map to indicate that the keys should
            	// be extracted using a BYTE_ENCODING 
            	
            boolean	byte_keys = object instanceof ByteEncodedKeyHashMap;
            
            //write the d            
            writeChar('d');
            
            //are we sorted?
            if ( tempMap instanceof TreeMap ){
            	
                tempTree = (TreeMap)tempMap;
                
            }else{
            		tempTree = new TreeMap(tempMap);
            }            
                   
            Iterator	it = tempTree.entrySet().iterator();
            
            while( it.hasNext()){
            	
            	Map.Entry	entry = (Map.Entry)it.next();
			
            	Object o_key = entry.getKey();
   			   		           	
   			   	Object value = entry.getValue();

   			   	if (value != null)
				{
					if (o_key instanceof byte[])
					{
						encodeObject(o_key);
						if (!encodeObject(value))
							encodeObject("");
					} else if(o_key instanceof String)
					{
						String key = (String) o_key;
						if (byte_keys)
						{
							try
							{
								encodeObject(Constants.BYTE_CHARSET.encode(key));
								if (!encodeObject(value))
									encodeObject("");
							} catch (UnsupportedEncodingException e)
							{
								throw (new IOException("BEncoder: unsupport encoding: " + e.getMessage()));
							}
						} else
						{
								// if we put non-ascii chars in as keys we can get horrible expanding
								// config issues as we cycle through decode/encode cycles with certain
								// characters
							
							if ( Constants.IS_CVS_VERSION ){
								char[]	chars = key.toCharArray();
								
								for ( char c: chars ){

									if (c >= '\u0080'){

										if ( non_ascii_logs < 50 ){
											
											non_ascii_logs++;
											
											Debug.out( "Non-ASCII key: " + key );
										}
										
										break;
									}
								}
							}
							encodeObject(key); // Key goes in as UTF-8
							if (!encodeObject(value))
								encodeObject("");
						}
					} else
						Debug.out( "Attempt to encode an unsupported map key type: " + object.getClass() + ";value=" + object);
				}     
            }
            
            writeChar('e');
            
            
        }else if(object instanceof List){
        	
            List tempList = (List)object;
            
            	//write out the l
            
            writeChar('l');                                   
            
            for(int i = 0; i<tempList.size(); i++){
                
            	encodeObject( tempList.get(i));                            
            }   
            
            writeChar('e');                          
            
        }else if(object instanceof Long){
        	
            Long tempLong = (Long)object;         
            //write out the l       
            writeChar('i');
            writeLong(tempLong.longValue());
            writeChar('e');
         }else if(object instanceof Integer){
         	
			Integer tempInteger = (Integer)object;         
			//write out the l       
			writeChar('i');
			writeInt(tempInteger.intValue());
			writeChar('e');
			
       }else if(object instanceof byte[]){
       	
            byte[] tempByteArray = (byte[])object;
            writeInt(tempByteArray.length);
            writeChar(':');
            if ( url_encode ){
            	writeBytes(URLEncoder.encode(new String(tempByteArray, Constants.BYTE_ENCODING), Constants.BYTE_ENCODING ).getBytes());
            }else{
            	writeBytes(tempByteArray);
            }
            
       }else if(object instanceof ByteBuffer ){
       	
       		ByteBuffer  bb = (ByteBuffer)object;
       		writeInt(bb.limit());
       		writeChar(':');
            writeByteBuffer(bb);
            
       }else if ( object == null ){
    	   
    	   	// ideally we'd bork here but I don't want to run the risk of breaking existing stuff so just log
    	   
    	   
    	   Debug.out( "Attempt to encode a null value: sofar=" + getEncodedSoFar());
    	   return false;
    	   
       }else{
        	
    	   
    	   Debug.out( "Attempt to encode an unsupported entry type: " + object.getClass() + ";value=" + object);
    	   return false;
       }
        
        return true;
    }
    
    private void
    writeChar(
    	char		c )
   	{
    	int rem = current_buffer.length - current_buffer_pos;
    	
    	if ( rem > 0 ){
    		
    		current_buffer[current_buffer_pos++] = (byte)c;
    		
    	}else{
    		
       		int	next_buffer_size = current_buffer.length < BUFFER_DOUBLE_LIMIT?(current_buffer.length << 1):(current_buffer.length + BUFFER_DOUBLE_LIMIT );

    		byte[]	new_buffer = new byte[ next_buffer_size ];
       		
    		new_buffer[ 0 ] = (byte)c;

    		if ( old_buffers == null ){
    			
    			old_buffers = new byte[][]{ current_buffer };
    			
    		}else{
    			
    			byte[][] new_old_buffers = new byte[old_buffers.length+1][];
    			
    			System.arraycopy( old_buffers, 0, new_old_buffers, 0, old_buffers.length );
    			
    			new_old_buffers[ old_buffers.length ] = current_buffer;
    			
    			old_buffers = new_old_buffers;
    		}
    		
    		current_buffer		= new_buffer;
    		current_buffer_pos 	= 1;
     	}
   	}
   	
    private void
    writeInt(
    	int		i )
    {
    		// we get a bunch of -1 values, optimise
    	
    	if ( i == -1 ){
    		
    		writeBytes( MINUS_1_BYTES );
    		
    		return;
    	}
    	
    	int start = intToBytes( i );
    	   	
    	writeBytes( int_buffer, start, 12 - start );
    }
    
    private void
    writeLong(
    	long	l )
    {
     	if ( l <= Integer.MAX_VALUE && l >= Integer.MIN_VALUE ){
    		
    		writeInt((int)l);
    		
    	}else{
    		
    		writeBytes(Long.toString( l ).getBytes());
    	}
    }
    
    private void
    writeBytes(
    	byte[]			bytes )
    {
    	writeBytes( bytes, 0, bytes.length );
    }
    
    private void
    writeBytes(
    	byte[]			bytes,
    	int				offset,
    	int				length )
    {
    	int rem = current_buffer.length - current_buffer_pos;
    	
    	if ( rem >= length ){
    		
    		System.arraycopy( bytes, offset, current_buffer, current_buffer_pos, length );
    		
    		current_buffer_pos += length;
    		
    	}else{
    		
    		if ( rem > 0 ){
    			
	    		System.arraycopy( bytes, offset, current_buffer, current_buffer_pos, rem );
	
	    		length -= rem;
    		}
    		    
    		int	next_buffer_size = current_buffer.length < BUFFER_DOUBLE_LIMIT?(current_buffer.length << 1):(current_buffer.length + BUFFER_DOUBLE_LIMIT );
    				
    		byte[]	new_buffer = new byte[ Math.max( next_buffer_size, length + 512 ) ];
       		   		
    		System.arraycopy( bytes, offset + rem, new_buffer, 0, length );

    		if ( old_buffers == null ){
    			
    			old_buffers = new byte[][]{ current_buffer };
    			
    		}else{
    			
    			byte[][] new_old_buffers = new byte[old_buffers.length+1][];
    			
    			System.arraycopy( old_buffers, 0, new_old_buffers, 0, old_buffers.length );
    			
    			new_old_buffers[ old_buffers.length ] = current_buffer;
    			
    			old_buffers = new_old_buffers;
    		}
    		
    		current_buffer		= new_buffer;
    		current_buffer_pos 	= length;
     	}   
    }
    
    private void
	writeByteBuffer(
		ByteBuffer		bb )
    {
    	writeBytes( bb.array(), bb.arrayOffset() + bb.position(), bb.remaining());
    }

    private String
    getEncodedSoFar()
    {
    	return( new String( toByteArray()));
    }
    
    private byte[]
    toByteArray()
    {
    	if ( old_buffers == null ){
    		
    		byte[]	res = new byte[current_buffer_pos];
    		
    		System.arraycopy( current_buffer, 0, res, 0, current_buffer_pos );
    		
    		// System.out.println( "-> " + current_buffer_pos );
    		
    		return( res );
    		
    	}else{
    		
    		int	total = current_buffer_pos;
    		
    		for (int i=0;i<old_buffers.length;i++){
    			
    			total += old_buffers[i].length;
    		}
    		
    		byte[] res = new byte[total];
    		
    		int	pos = 0;
    		
    		//String str = "";
    		
    		for (int i=0;i<old_buffers.length;i++){

    			byte[] buffer = old_buffers[i];
    			
    			int	len = buffer.length;
    			
    			System.arraycopy( buffer, 0, res, pos, len );
    			
    			pos += len;
    			
    			//str += (str.length()==0?"":",") + len;
    		}
    		
      		System.arraycopy( current_buffer, 0, res, pos, current_buffer_pos );
      		 
    		//System.out.println( "-> " + str + "," + current_buffer_pos );

      		return( res );
    	}
    }
                   
    
    private static Object
    normaliseObject(
    	Object		o )
    {
    	if ( o instanceof Integer ){
       		o = new Long(((Integer)o).longValue());
      	}else if ( o instanceof Boolean ){
       		o = new Long(((Boolean)o).booleanValue()?1:0);
      	}else if ( o instanceof Float ){
       		o = String.valueOf((Float)o);
       	}else if ( o instanceof byte[] ){    		
       		try{
       			o = new String((byte[])o,"UTF-8");			
       		}catch( Throwable e ){
       		}
       	}
    	
    	return( o );
    }
    
    public static boolean isEncodable(Object toCheck) {
		if (toCheck instanceof Integer || toCheck instanceof Long || toCheck instanceof Boolean || toCheck instanceof Float || toCheck instanceof byte[] || toCheck instanceof String || toCheck instanceof BEncodableObject)
			return true;
		if (toCheck instanceof Map)
		{
			for (Iterator it = ((Map) toCheck).keySet().iterator(); it.hasNext();)
			{
				Map.Entry entry = (Map.Entry) it.next();
				Object key = entry.getKey();
				if (!(key instanceof String || key instanceof byte[]) || !isEncodable(entry.getValue()))
					return false;
			}
			return true;
		}
		if (toCheck instanceof List)
		{
			for (Iterator it = ((List) toCheck).iterator(); it.hasNext();)
				if (!isEncodable(it.next()))
					return false;
			return true;
		}
		return false;
	}
    
    
    public static boolean
    objectsAreIdentical(
    	Object		o1,
    	Object		o2 )
    {
    	if ( o1 == null && o2 == null ){
    		
    		return( true );
    		
    	}else if ( o1 == null || o2 == null ){
    		
    		return( false );
    	}
    	
      	if ( o1.getClass() != o2.getClass()){
      		 
    		if ( 	( o1 instanceof Map && o2 instanceof Map ) ||
    				( o1 instanceof List && o2 instanceof List )){
    			
    			// things actually OK
    			
    		}else{
    			
		    	o1 = normaliseObject( o1 );
		    	o2 = normaliseObject( o2 );
	       	
		    	if ( o1.getClass() != o2.getClass()){
	    				    			
			    	Debug.out( "Failed to normalise classes " + o1.getClass() + "/" + o2.getClass());
			    		
			    	return( false );
		    	}
      		}
    	}
    	
    	if ( 	o1 instanceof Long ||
    			o1 instanceof String ){		
    		
    		return( o1.equals( o2 ));
    		
     	}else if ( o1 instanceof byte[] ){
     		
     		return( Arrays.equals((byte[])o1,(byte[])o2 ));
     		    		
    	}else if ( o1 instanceof List ){
    		
    		return( listsAreIdentical((List)o1,(List)o2));
    		
       	}else if ( o1 instanceof Map ){
       	    		
    		return( mapsAreIdentical((Map)o1,(Map)o2));
    		
       	}else if ( 	o1 instanceof Integer ||
	    			o1 instanceof Boolean ||
	    			o1 instanceof Float ||
	    			o1 instanceof ByteBuffer ){
    		
    		return( o1.equals( o2 ));

    	}else{
    		
    		Debug.out( "Invalid type: " + o1 );
    		
    		return( false );
    	}
    }
    
    public static boolean
	listsAreIdentical(
		List	list1,
		List	list2 )
    {
    	if ( list1 == null && list2 == null ){
    		
    		return( true );
    		
    	}else if ( list1 == null || list2 == null ){
    		
    		return( false );
    	}
    	
    	if ( list1.size() != list2.size()){
    		
    		return( false );
    	}
    	
    	for ( int i=0;i<list1.size();i++){
    		
    		if ( !objectsAreIdentical( list1.get(i), list2.get(i))){
    			
    			return( false );
    		}
    	}
    	
    	return( true );
    }
    
    public static boolean
	mapsAreIdentical(
		Map	map1,
		Map	map2 )
	{
    	if ( map1 == null && map2 == null ){
    		
    		return( true );
    		
    	}else if ( map1 == null || map2 == null ){
    		
    		return( false );
    	}
    	
    	if ( map1.size() != map2.size()){
    		
    		return( false );
    	}
    	
    	Iterator	it = map1.keySet().iterator();
    	
    	while( it.hasNext()){
    		
    		Object	key = it.next();
    		
    		Object	v1 = map1.get(key);
    		Object	v2 = map2.get(key);
    		
    		if ( !objectsAreIdentical( v1, v2 )){
    			
    			return( false );
    		}
    	}
    	
    	return( true );
    }	
    
    public static Map
    cloneMap(
    	Map		map )
    {
    	if ( map == null ){
    		
    		return( null );
    	}
    	
    	Map res = new TreeMap();
    	
    	Iterator	it = map.entrySet().iterator();
    	
    	while( it.hasNext()){
    		
    		Map.Entry	entry = (Map.Entry)it.next();
    		
    		Object	key 	= entry.getKey();
    		Object	value	= entry.getValue();

    			// keys must be String (or very rarely byte[])
    		
    		if ( key instanceof byte[] ){
    			
    			key = ((byte[])key).clone();
    		}
    		
    		res.put( key, clone( value ));
    	}
    	
    	return( res );
    }
    
    public static List
    cloneList(
    	List		list )
    {
    	if ( list == null ){
    		
    		return( null );
    	}
    	
    	List	res = new ArrayList(list.size());
    	
    	Iterator	it = list.iterator();
    	
    	while( it.hasNext()){
    		
    		res.add( clone( it.next()));
    	}
    	
    	return( res );
    }
    
    public static Object
    clone(
    	Object	obj )
    {
    	if ( obj instanceof List ){
    		
    		return( cloneList((List)obj));
    		
    	}else if ( obj instanceof Map ){
    		
    		return( cloneMap((Map)obj));
    		
    	}else if ( obj instanceof byte[]){
    		
    		return(((byte[])obj).clone());
    		
    	}else{
    			// assume immutable - String,Long etc
    		
    		return( obj );
    	}
    }
    
    public static StringBuffer
    encodeToXML(
    	Map			map,
    	boolean		simple )
    {
     	XMLEncoder writer = new XMLEncoder();
  
     	return( writer.encode( map, simple ));
    }    
    
    	/*
    	 * The following code is from Integer.java as we don't want to 
    	 */
    final static byte[] digits = {
    	'0' , '1' , '2' , '3' , '4' , '5' ,
    	'6' , '7' , '8' , '9' , 'a' , 'b' ,
    	'c' , 'd' , 'e' , 'f' , 'g' , 'h' ,
    	'i' , 'j' , 'k' , 'l' , 'm' , 'n' ,
    	'o' , 'p' , 'q' , 'r' , 's' , 't' ,
    	'u' , 'v' , 'w' , 'x' , 'y' , 'z'
        };
    
    final static byte [] DigitTens = {
    	'0', '0', '0', '0', '0', '0', '0', '0', '0', '0',
    	'1', '1', '1', '1', '1', '1', '1', '1', '1', '1',
    	'2', '2', '2', '2', '2', '2', '2', '2', '2', '2',
    	'3', '3', '3', '3', '3', '3', '3', '3', '3', '3',
    	'4', '4', '4', '4', '4', '4', '4', '4', '4', '4',
    	'5', '5', '5', '5', '5', '5', '5', '5', '5', '5',
    	'6', '6', '6', '6', '6', '6', '6', '6', '6', '6',
    	'7', '7', '7', '7', '7', '7', '7', '7', '7', '7',
    	'8', '8', '8', '8', '8', '8', '8', '8', '8', '8',
    	'9', '9', '9', '9', '9', '9', '9', '9', '9', '9',
    	} ; 

    final static byte [] DigitOnes = { 
    	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    	} ;
        
    	/**
    	 * writes to int_buffer and returns start position in buffer (always runs to end of buffer)
    	 * @param i
    	 * @return
    	 */
    
    private int 
    intToBytes(
    	int 	i )
    {
        int q, r;
        int charPos = 12;
        byte sign = 0;

        if (i < 0) { 
            sign = '-';
            i = -i;
        }

        // Generate two digits per iteration
        while (i >= 65536) {
            q = i / 100;
        // really: r = i - (q * 100);
            r = i - ((q << 6) + (q << 5) + (q << 2));
            i = q;
            int_buffer [--charPos] = DigitOnes[r];
            int_buffer [--charPos] = DigitTens[r];
        }

        // Fall thru to fast mode for smaller numbers
        // assert(i <= 65536, i);
        for (;;) { 
            q = (i * 52429) >>> (16+3);
            r = i - ((q << 3) + (q << 1));  // r = i-(q*10) ...
            int_buffer [--charPos] = digits [r];
            i = q;
            if (i == 0) break;
        }
        if (sign != 0) {
        	int_buffer [--charPos] = sign;
        }
        return charPos;
    }
    
    protected static class
    XMLEncoder
    	extends XUXmlWriter
    {
    	protected
    	XMLEncoder()
    	{
    	}
    	
    	protected StringBuffer
    	encode(
    		Map		map,
    		boolean	simple )
    	{
    		StringWriter	writer = new StringWriter(1024);
    		
    		setOutputWriter( writer );
    		
    		setGenericSimple( simple );
    		
    		writeGeneric( map );
    		
    		flushOutputStream();
    		
    		return( writer.getBuffer());
    	}
    }
}
