package org.red5.io.amf3;

/*
 * RED5 Open Source Flash Server - http://www.osflash.org/red5
 *
 * Copyright (c) 2006-2007 by respective authors (see below). All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any later
 * version.
 *
 * This library 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with this library; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.collections.BeanMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.mina.common.ByteBuffer;
import org.red5.annotations.Anonymous;
import org.red5.annotations.DontSerialize;
import org.red5.compatibility.flex.messaging.io.ObjectProxy;
import org.red5.io.amf.AMF;
import org.red5.io.object.RecordSet;
import org.red5.io.object.Serializer;
import org.red5.io.utils.XMLUtils;
import org.w3c.dom.Document;

/**
 * AMF3 output writer
 *
 * @see  org.red5.io.amf3.AMF3
 * @see  org.red5.io.amf3.Input
 * @author The Red5 Project (red5@osflash.org)
 * @author Joachim Bauch (jojo@struktur.de)
 */
public class Output extends org.red5.io.amf.Output implements org.red5.io.object.Output {

	protected static Log log = LogFactory.getLog(Output.class.getName());

	/**
	 * Set to a value above <tt>0</tt> to disable writing of the AMF3 object tag.
	 */
	private int amf3_mode;
	/**
	 * List of strings already written.
	 * */
	private List<String> stringReferences;

	/**
	 * Constructor of AMF3 output.
	 *
	 * @param buf
	 *            instance of ByteBuffer
	 * @see ByteBuffer
	 */
	public Output(ByteBuffer buf) {
		super(buf);
		amf3_mode = 0;
		stringReferences = new LinkedList<String>();
	}

	/**
	 * Provide access to raw data.
	 *
	 * @return ByteBuffer
	 */
	protected ByteBuffer getBuffer() {
		return buf;
	}

    /** {@inheritDoc} */
	@Override
	public boolean supportsDataType(byte type) {
		return true;
	}

	// Basic Data Types

	protected void writeAMF3() {
		if (amf3_mode == 0)
			buf.put(AMF.TYPE_AMF3_OBJECT);
	}

    /** {@inheritDoc} */
	@Override
	public void writeBoolean(Boolean bol) {
		writeAMF3();
		buf.put(bol ? AMF3.TYPE_BOOLEAN_TRUE : AMF3.TYPE_BOOLEAN_FALSE);
	}

    /** {@inheritDoc} */
	@Override
	public void writeNull() {
		writeAMF3();
		buf.put(AMF3.TYPE_NULL);
	}

    /** {@inheritDoc} */
	protected void putInteger(long value) {
		if (value < 0) {
			buf.put((byte) (0x80 | ((value >> 22) & 0xff)));
			buf.put((byte) (0x80 | ((value >> 15) & 0x7f)));
			buf.put((byte) (0x80 | ((value >> 8) & 0x7f)));
			buf.put((byte) (value & 0xff));
		} else if (value <= 0x7f) {
			buf.put((byte) value);
		} else if (value <= 0x3fff) {
			buf.put((byte) (0x80 | ((value >> 7) & 0x7f)));
			buf.put((byte) (value & 0x7f));
		} else if (value <= 0x1fffff) {
			buf.put((byte) (0x80 | ((value >> 14) & 0x7f)));
			buf.put((byte) (0x80 | ((value >> 7) & 0x7f)));
			buf.put((byte) (value & 0x7f));
		} else {
			buf.put((byte) (0x80 | ((value >> 22) & 0xff)));
			buf.put((byte) (0x80 | ((value >> 15) & 0x7f)));
			buf.put((byte) (0x80 | ((value >> 8) & 0x7f)));
			buf.put((byte) (value & 0xff));
		}
	}

	/** {@inheritDoc} */
    protected static byte[] encodeString(String string) {
    	byte[] encoded;
    	synchronized (stringCache) {
    		encoded = stringCache.get(string);
    	}
    	if (encoded == null) {
    		java.nio.ByteBuffer buf = AMF3.CHARSET.encode(string);
    		encoded = new byte[buf.limit()];
    		buf.get(encoded);
    		synchronized (stringCache) {
    			stringCache.put(string, encoded);
    		}
    	}
    	return encoded;
    }
    
	/** {@inheritDoc} */
	protected void putString(String str, byte[] encoded) {
		final int len = encoded.length;
		int pos = stringReferences.indexOf(str);
		if (pos >= 0) {
			// Reference to existing string
			putInteger(pos << 1);
			return;
		}

		putInteger(len << 1 | 1);
		buf.put(encoded);
		stringReferences.add(str);
	}

    /** {@inheritDoc} */
	@Override
	public void putString(String string) {
		if ("".equals(string)) {
			// Empty string;
			putInteger(1);
			return;
		}

    	final byte[] encoded = encodeString(string);
		putString(string, encoded);
	}

    /** {@inheritDoc} */
	@Override
	public void writeNumber(Number num) {
		writeAMF3();
		if (num.longValue() < AMF3.MIN_INTEGER_VALUE || num.longValue() > AMF3.MAX_INTEGER_VALUE) {
			// Out of range for integer encoding
			buf.put(AMF3.TYPE_NUMBER);
			buf.putDouble(num.doubleValue());
		} else if (num instanceof Long || num instanceof Integer || num instanceof Short || num instanceof Byte) {
			buf.put(AMF3.TYPE_INTEGER);
			putInteger(num.longValue());
		} else {
			buf.put(AMF3.TYPE_NUMBER);
			buf.putDouble(num.doubleValue());
		}
	}

    /** {@inheritDoc} */
	@Override
	public void writeString(String string) {
		writeAMF3();
		buf.put(AMF3.TYPE_STRING);
		if ("".equals(string)) {
			putInteger(1);
		} else {
	    	final byte[] encoded = encodeString(string);
			putString(string, encoded);
		}
	}

    /** {@inheritDoc} */
	@Override
	public void writeDate(Date date) {
		writeAMF3();
		buf.put(AMF3.TYPE_DATE);
		if (hasReference(date)) {
			putInteger(getReferenceId(date) << 1);
			return;
		}

		storeReference(date);
		putInteger(1);
		buf.putDouble(date.getTime());
	}

    /** {@inheritDoc} */
    public void writeArray(Collection array, Serializer serializer) {
		writeAMF3();
		buf.put(AMF3.TYPE_ARRAY);
    	if (hasReference(array)) {
    		putInteger(getReferenceId(array) << 1);
    		return;
    	}

    	storeReference(array);
		amf3_mode += 1;
		int count = array.size();
		putInteger(count << 1 | 1);
		putString("");
		for (Object item: array) {
			serializer.serialize(this, item);
		}
		amf3_mode -= 1;
    }

    /** {@inheritDoc} */
    public void writeArray(Object[] array, Serializer serializer) {
		writeAMF3();
		buf.put(AMF3.TYPE_ARRAY);
    	if (hasReference(array)) {
    		putInteger(getReferenceId(array) << 1);
    		return;
    	}

    	storeReference(array);
		amf3_mode += 1;
		int count = array.length;
		putInteger(count << 1 | 1);
		putString("");
		for (Object item: array) {
			serializer.serialize(this, item);
		}
		amf3_mode -= 1;
    }

    /** {@inheritDoc} */
    public void writeArray(Object array, Serializer serializer) {
		writeAMF3();
		buf.put(AMF3.TYPE_ARRAY);
    	if (hasReference(array)) {
    		putInteger(getReferenceId(array) << 1);
    		return;
    	}

    	storeReference(array);
		amf3_mode += 1;
		int count = Array.getLength(array);
		putInteger(count << 1 | 1);
		putString("");
		for (int i=0; i<count; i++) {
			serializer.serialize(this, Array.get(array, i));
		}
		amf3_mode -= 1;
    }

    /** {@inheritDoc} */
    public void writeMap(Map<Object, Object> map, Serializer serializer) {
		writeAMF3();
		buf.put(AMF3.TYPE_ARRAY);
    	if (hasReference(map)) {
    		putInteger(getReferenceId(map) << 1);
    		return;
    	}

    	storeReference(map);
		// Search number of starting integer keys
		int count = 0;
		for (int i=0; i<map.size(); i++) {
			try {
				if (!map.containsKey(i))
					break;
			} catch (ClassCastException err) {
				// Map has non-number keys.
				break;
			}
			count++;
		}

		amf3_mode += 1;
		if (count == map.size()) {
			// All integer keys starting from zero: serialize as regular array
			putInteger(count << 1 | 1);
			putString("");
			for (int i=0; i<count; i++) {
				serializer.serialize(this, map.get(i));
			}
			amf3_mode -= 1;
			return;
		}

		putInteger(count << 1 | 1);
		// Serialize key-value pairs first
		for (Map.Entry<Object, Object> entry: map.entrySet()) {
			Object key = entry.getKey();
			if ((key instanceof Number) && !(key instanceof Float) && !(key instanceof Double) &&
					((Number) key).longValue() >= 0 && ((Number) key).longValue() < count) {
				// Entry will be serialized later
				continue;
			}
			putString(key.toString());
			serializer.serialize(this, entry.getValue());
		}
		putString("");
		// Now serialize integer keys starting from zero
		for (int i=0; i<count; i++) {
			serializer.serialize(this, map.get(i));
		}
		amf3_mode -= 1;
    }

    /** {@inheritDoc} */
    public void writeMap(Collection array, Serializer serializer) {
		writeAMF3();
		buf.put(AMF3.TYPE_ARRAY);
    	if (hasReference(array)) {
    		putInteger(getReferenceId(array) << 1);
    		return;
    	}

    	storeReference(array);
    	// TODO: we could optimize this by storing the first integer
    	//       keys after the key-value pairs
		amf3_mode += 1;
		putInteger(1);
		int idx = 0;
    	for (Object item: array) {
    		if (item != null) {
    			putString(String.valueOf(idx));
    			serializer.serialize(this, item);
    		}
    		idx++;
    	}
    	amf3_mode -= 1;
		putString("");
    }

    /** {@inheritDoc} */
	@Override
	protected void writeArbitraryObject(Object object, Serializer serializer) {
		Class<?> objectClass = object.getClass();
    	String className = objectClass.getName();
    	if (className.startsWith("org.red5.compatibility.")) {
    		// Strip compatibility prefix from classname
    		className = className.substring(23);
    	}
    	
        // If we need to serialize class information...
    	if (!objectClass.isAnnotationPresent(Anonymous.class)) {
			putString(className);
		} else {
			putString("");
		}

    	// Store key/value pairs
    	amf3_mode += 1;
		// Get public field values
		Map<String, Object> values = new HashMap<String, Object>();
        // Iterate thru fields of an object to build "name-value" map from it
        for (Field field : objectClass.getFields()) {
			if (field.isAnnotationPresent(DontSerialize.class)) {
				if (log.isDebugEnabled()) {
					log.debug("Skipping " + field.getName() + " because its marked with @DontSerialize");
				}
				continue;
			} else {
				int modifiers = field.getModifiers();
				if (Modifier.isTransient(modifiers)) {
					log.warn("Using \"transient\" to declare fields not to be serialized is " +
						"deprecated and will be removed in Red5 0.8, use \"@DontSerialize\" instead.");
					continue;
				}
			}

			Object value;
			try {
                // Get field value
                value = field.get(object);
			} catch (IllegalAccessException err) {
                // Swallow on private and protected properties access exception
                continue;
			}
            // Put field to the map of "name-value" pairs
            values.put(field.getName(), value);
		}

		// Output public values
		Iterator<Map.Entry<String, Object>> it = values.entrySet().iterator();
        // Iterate thru map and write out properties with separators
        while (it.hasNext()) {
			Map.Entry<String, Object> entry = it.next();
            // Write out prop name
			putString(entry.getKey());
            // Write out
            serializer.serialize(this, entry.getValue());
		}
    	amf3_mode -= 1;
        // Write out end of object marker
		putString("");
	}

    /** {@inheritDoc} */
    public void writeObject(Object object, Serializer serializer) {
		writeAMF3();
		buf.put(AMF3.TYPE_OBJECT);
    	if (hasReference(object)) {
    		putInteger(getReferenceId(object) << 1);
    		return;
    	}

    	storeReference(object);
    	String className = object.getClass().getName();
    	if (className.startsWith("org.red5.compatibility.")) {
    		// Strip compatibility prefix from classname
    		className = className.substring(23);
    	}
    	
    	if (object instanceof IExternalizable) {
    		// The object knows how to serialize itself.
        	int type = 1 << 1 | 1;
        	if (object instanceof ObjectProxy) {
        		type |= AMF3.TYPE_OBJECT_PROXY << 2;
        	} else {
        		type |= AMF3.TYPE_OBJECT_EXTERNALIZABLE << 2;
        	}
        	putInteger(type);
        	putString(className);
        	amf3_mode += 1;
        	((IExternalizable) object).writeExternal(new DataOutput(this, serializer));
        	amf3_mode -= 1;
        	return;
    	}

    	// We have an inline class that is not a reference.
    	// We store the properties using key/value pairs
    	int type = AMF3.TYPE_OBJECT_VALUE << 2 | 1 << 1 | 1;
    	putInteger(type);

        // Create new map out of bean properties
        BeanMap beanMap = new BeanMap(object);
        // Set of bean attributes
        Set set = beanMap.entrySet();
		if ((set.size() == 0) || (set.size() == 1 && beanMap.containsKey("class"))) {
			// BeanMap is empty or can only access "class" attribute, skip it
			writeArbitraryObject(object, serializer);
			return;
		}

        // Write out either start of object marker for class name or "empty" start of object marker
		Class<?> objectClass = object.getClass();
		if (!objectClass.isAnnotationPresent(Anonymous.class)) {
        	// classname
        	putString(className);
		} else {
			putString("");
		}

    	// Store key/value pairs
    	amf3_mode += 1;
		Iterator it = set.iterator();
        while (it.hasNext()) {
			BeanMap.Entry entry = (BeanMap.Entry) it.next();
			String keyName = entry.getKey().toString();
			if ("class".equals(keyName)) {
				continue;
			}

			// Check if the Field corresponding to the getter/setter pair is transient
			try {
				Field field = objectClass.getDeclaredField(keyName);
				if (field.isAnnotationPresent(DontSerialize.class)) {
					if (log.isDebugEnabled()) {
						log.debug("Skipping " + field.getName() + " because its marked with @DontSerialize");
					}
					continue;
				} else {
					int modifiers = field.getModifiers();
					if (Modifier.isTransient(modifiers)) {
						log.warn("Using \"transient\" to declare fields not to be serialized is " +
							"deprecated and will be removed in Red5 0.8, use \"@DontSerialize\" instead.");
						continue;
					}
				}
			} catch (NoSuchFieldException nfe) {
				// Ignore this exception and use the default behaviour
				log.debug("writeObject caught NoSuchFieldException");
			}

			putString(keyName);
			serializer.serialize(this, entry.getValue());
		}
    	amf3_mode -= 1;

    	// End of object marker
    	putString("");
    }

    /** {@inheritDoc} */
	@Override
    public void writeObject(Map<Object, Object> map, Serializer serializer) {
		writeAMF3();
		buf.put(AMF3.TYPE_OBJECT);
    	if (hasReference(map)) {
    		putInteger(getReferenceId(map) << 1);
    		return;
    	}

    	storeReference(map);
    	// We have an inline class that is not a reference.
    	// We store the properties using key/value pairs
    	int type = AMF3.TYPE_OBJECT_VALUE << 2 | 1 << 1 | 1;
    	putInteger(type);

    	// No classname
    	putString("");

    	// Store key/value pairs
    	amf3_mode += 1;
    	for (Map.Entry<Object, Object> entry: map.entrySet()) {
    		putString(entry.getKey().toString());
    		serializer.serialize(this, entry.getValue());
    	}
    	amf3_mode -= 1;

    	// End of object marker
    	putString("");
    }

    /** {@inheritDoc} */
	@Override
    public void writeRecordSet(RecordSet recordset, Serializer serializer) {
    	writeString("Not implemented.");
    }

    /** {@inheritDoc} */
	@Override
	public void writeXML(Document xml) {
		writeAMF3();
		buf.put(AMF3.TYPE_XML);
		if (hasReference(xml)) {
    		putInteger(getReferenceId(xml) << 1);
    		return;
		}
		final byte[] encoded = encodeString(XMLUtils.docToString(xml));
		putInteger(encoded.length << 1 | 1);
		buf.put(encoded);
		storeReference(xml);
	}
	
    /** {@inheritDoc} */
    public void writeByteArray(ByteArray array) {
    	writeAMF3();
    	buf.put(AMF3.TYPE_BYTEARRAY);
    	if (hasReference(array)) {
    		putInteger(getReferenceId(array) << 1);
    		return;
    	}

    	storeReference(array);
    	ByteBuffer data = array.getData();
    	putInteger(data.limit() << 1 | 1);
    	byte[] tmp = new byte[data.limit()];
    	int old = data.position();
    	try {
    		data.position(0);
    		data.get(tmp);
    		buf.put(tmp);
    	} finally {
    		data.position(old);
    	}
    }
	
}
