/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package flex.messaging.io.amf;

import flex.messaging.MessageException;
import flex.messaging.io.ArrayCollection;
import flex.messaging.io.BeanProxy;
import flex.messaging.io.PagedRowSet;
import flex.messaging.io.PropertyProxy;
import flex.messaging.io.PropertyProxyRegistry;
import flex.messaging.io.SerializationContext;
import flex.messaging.io.SerializationDescriptor;
import flex.messaging.io.StatusInfoProxy;
import flex.messaging.io.amf.AmfTrace.VectorType;
import flex.messaging.util.Trace;
import org.w3c.dom.Document;

import javax.sql.RowSet;
import java.io.Externalizable;
import java.io.IOException;
import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Serializes data to an output stream using the new
 * AMF 3 format.
 * <p>
 * This class intends to match the Flash Player 8 C++ code
 * in avmglue/DataIO.cpp
 * </p>
 */
public class Amf3Output extends AbstractAmfOutput implements Amf3Types {
    /**
     *
     */
    protected IdentityHashMap<Object, Integer> objectTable;

    /**
     *
     */
    protected HashMap<TraitsInfo, Integer> traitsTable;

    /**
     *
     */
    protected HashMap<String, Integer> stringTable;

    public Amf3Output(SerializationContext context) {
        super(context);
        context.supportDatesByReference = true;
    }

    public void reset() {
        super.reset();
        if (objectTable != null)
            objectTable.clear();
        if (traitsTable != null)
            traitsTable.clear();
        if (stringTable != null)
            stringTable.clear();
    }

    //
    // java.io.ObjectOutput IMPLEMENTATIONS
    //

    /**
     * Serialize an Object using AMF 3.
     *
     * @param o the object to write
     * @throws IOException if the write failed
     */
    public void writeObject(Object o) throws IOException {
        if (o == null) {
            writeAMFNull();
            return;
        }

        if (!context.legacyExternalizable && o instanceof Externalizable) {
            writeCustomObject(o);
        } else if (o instanceof String || o instanceof Character) {
            String s = o.toString();
            writeAMFString(s);
        } else if (o instanceof Number) {
            if (o instanceof Integer || o instanceof Short || o instanceof Byte) {
                int i = ((Number) o).intValue();
                writeAMFInt(i);
            } else if (!context.legacyBigNumbers &&
                    (o instanceof BigInteger || o instanceof BigDecimal)) {
                // Using double to write big numbers such as BigInteger or
                // BigDecimal can result in information loss so we write
                // them as String by default...
                writeAMFString(o.toString());
            } else {
                double d = ((Number) o).doubleValue();
                writeAMFDouble(d);
            }
        } else if (o instanceof Boolean) {
            writeAMFBoolean(((Boolean) o).booleanValue());
        }
        // We have a complex type...
        else if (o instanceof Date) {
            writeAMFDate((Date) o);
        } else if (o instanceof Calendar) {
            writeAMFDate(((Calendar) o).getTime());
        } else if (o instanceof Document) {
            if (context.legacyXMLDocument)
                out.write(kXMLType); // Legacy flash.xml.XMLDocument Type
            else
                out.write(kAvmPlusXmlType); // New E4X XML Type
            if (!byReference(o)) {
                String xml = documentToString(o);
                if (isDebug)
                    trace.write(xml);

                writeAMFUTF(xml);
            }
        }
        // If there is a proxy for this,write it as a custom object so the default
        // behavior can be overriden.
        else if (o instanceof Enum && PropertyProxyRegistry.getRegistry().getProxy(o.getClass()) == null) {
            Enum<?> enumValue = (Enum<?>) o;
            writeAMFString(enumValue.name());
        } else {
            // We have an Object or Array type...
            Class cls = o.getClass();

            if (context.legacyMap && o instanceof Map && !(o instanceof ASObject)) {
                writeMapAsECMAArray((Map) o);
            } else if (!context.legacyDictionary && o instanceof Dictionary) {
                writeDictionary((Dictionary) o);
            } else if (o instanceof Collection) {
                if (o instanceof List && context.preferVectors)
                    writeListAsTypedVector((List) o);
                else if (context.legacyCollection)
                    writeCollection((Collection) o, null);
                else
                    writeArrayCollection((Collection) o, null);
            } else if (cls.isArray()) {
                Class<?> componentType = cls.getComponentType();
                // Convert to vector if requested, except for character and byte arrays
                if (context.preferVectors &&
                        !(componentType.equals(Byte.class) || componentType.equals(byte.class)) &&
                        !(componentType.equals(Character.class) || componentType.equals(char.class)))
                    writeArrayAsTypedVector(o, componentType);
                else
                    writeAMFArray(o, componentType);
            } else {
                //Special Case: wrap RowSet in PageableRowSet for Serialization
                if (o instanceof RowSet) {
                    o = new PagedRowSet((RowSet) o, Integer.MAX_VALUE, false);
                } else if (context.legacyThrowable && o instanceof Throwable) {
                    o = new StatusInfoProxy((Throwable) o);
                }

                writeCustomObject(o);
            }
        }
    }

    public void writeObjectTraits(TraitsInfo ti) throws IOException {
        String className = ti.getClassName();

        if (isDebug) {
            if (ti.isExternalizable())
                trace.startExternalizableObject(className, getObjectTableSize());
            else
                trace.startAMFObject(className, getObjectTableSize());
        }

        if (!byReference(ti)) {
            int count = 0;
            List propertyNames = null;
            boolean externalizable = ti.isExternalizable();

            if (!externalizable) {
                propertyNames = ti.getProperties();
                if (propertyNames != null)
                    count = propertyNames.size();
            }

            boolean dynamic = ti.isDynamic();

            writeUInt29(3 | (externalizable ? 4 : 0) | (dynamic ? 8 : 0) | (count << 4));
            writeStringWithoutType(className);

            if (!externalizable && propertyNames != null) {
                for (int i = 0; i < count; i++) {
                    String propName = ti.getProperty(i);
                    writeStringWithoutType(propName);
                }
            }
        }
    }

    public void writeObjectProperty(String name, Object value) throws IOException {
        if (isDebug)
            trace.namedElement(name);
        increaseNestObjectLevel();
        writeObject(value);
        decreaseNestObjectLevel();
    }

    public void writeObjectEnd() throws IOException {
        // No action required for AMF 3

        if (isDebug)
            trace.endAMFObject();
    }

    //
    // AMF SPECIFIC SERIALIZATION IMPLEMENTATIONS
    //

    /**
     *
     */
    protected void writeAMFBoolean(boolean b) throws IOException {
        if (isDebug)
            trace.write(b);

        if (b)
            out.write(kTrueType);
        else
            out.write(kFalseType);
    }

    /**
     *
     */
    protected void writeAMFDate(Date d) throws IOException {
        out.write(kDateType);

        if (!byReference(d)) {
            if (isDebug)
                trace.write(d);

            //Write out an invalid reference
            writeUInt29(1);

            // Write the time as 64bit value in ms
            out.writeDouble((double) d.getTime());
        }
    }

    /**
     *
     */
    protected void writeAMFDouble(double d) throws IOException {
        if (isDebug)
            trace.write(d);

        out.write(kDoubleType);
        out.writeDouble(d);
    }

    /**
     *
     */
    protected void writeAMFInt(int i) throws IOException {
        if (i >= INT28_MIN_VALUE && i <= INT28_MAX_VALUE) {
            if (isDebug)
                trace.write(i);

            // We have to be careful when the MSB is set, as (value >> 3) will sign extend.
            // We know there are only 29-bits of precision, so truncate. This requires
            // similar care when reading an integer.
            //i = ((i >> 3) & UINT29_MASK);
            i = i & UINT29_MASK; // Mask is 2^29 - 1
            out.write(kIntegerType);
            writeUInt29(i);
        } else {
            // Promote large int to a double
            writeAMFDouble(i);
        }
    }

    protected void writeDictionary(Dictionary dictionary) throws IOException {
        out.write(kDictionaryType);

        if (byReference(dictionary))
            return;

        writeUInt29((dictionary.size() << 1) | 1);
        writeAMFBoolean(false /*usingWeakKeys*/);

        if (isDebug) trace.startAMFDictionary(objectTable.size() - 1);

        Enumeration keys = dictionary.keys();
        while (keys.hasMoreElements()) {
            if (isDebug) trace.startDictionaryElement();
            Object key = keys.nextElement();
            increaseNestObjectLevel();
            writeObject(key);
            decreaseNestObjectLevel();
            if (isDebug) trace.addDictionaryEquals();
            Object value = dictionary.get(key);
            increaseNestObjectLevel();
            writeObject(value);
            decreaseNestObjectLevel();
        }

        if (isDebug)
            trace.endAMFDictionary();
    }

    protected void writeArrayAsTypedVector(Object array, Class<?> componentType) throws IOException {
        int vecType = kTypedVectorObject;
        if (componentType.isPrimitive()) {
            if (int.class.equals(componentType))
                vecType = kTypedVectorInt;
            else if (double.class.equals(componentType))
                vecType = kTypedVectorDouble;
        } else {
            if (Integer.class.equals(componentType))
                vecType = kTypedVectorInt;
            else if (Double.class.equals(componentType))
                vecType = kTypedVectorDouble;
        }

        out.write(vecType);

        if (byReference(array))
            return;

        int length = Array.getLength(array);
        writeUInt29((length << 1) | 1);
        writeBoolean(true /*fixed*/);

        switch (vecType) {
            case kTypedVectorInt:
                if (isDebug)
                    trace.startAMFVector(objectTable.size() - 1, VectorType.INT);

                for (int i = 0; i < length; i++) {
                    if (isDebug)
                        trace.arrayElement(i);

                    Object element = Array.get(array, i);
                    int value = ((Integer) element).intValue();

                    if (isDebug)
                        trace.write(value);

                    writeInt(value);
                }
                break;
            case kTypedVectorDouble:
                if (isDebug)
                    trace.startAMFVector(objectTable.size() - 1, VectorType.DOUBLE);

                for (int i = 0; i < length; i++) {
                    if (isDebug)
                        trace.arrayElement(i);

                    Object element = Array.get(array, i);
                    double value = ((Double) element).doubleValue();

                    if (isDebug)
                        trace.write(value);

                    writeDouble(value);
                }
                break;
            case kTypedVectorObject:
                if (isDebug)
                    trace.startAMFVector(objectTable.size() - 1, VectorType.OBJECT);

                // TODO - I don't think this className is used properly on the client currently.
                String className = componentType.getName();
                writeStringWithoutType(className);

                for (int i = 0; i < length; i++) {
                    if (isDebug)
                        trace.arrayElement(i);

                    Object element = Array.get(array, i);
                    increaseNestObjectLevel();
                    writeObject(element);
                    decreaseNestObjectLevel();
                }
                break;
            default:
                break;
        }

        if (isDebug)
            trace.endAMFVector();
    }

    protected void writeListAsTypedVector(List list) throws IOException {
        // Peek at the first three elements of the list to figure out what type
        // of Vector it should be sent as.
        int vecType = -1;
        Class<?> initialElementClass = null;
        int peekSize = Math.min(list.size(), 3);
        for (int i = 0; i < peekSize; i++) {
            Object element = list.get(i);
            if (i == 0) {
                initialElementClass = element != null ? element.getClass() : null;
            } else {
                Class<?> currentElementClass = element != null ? element.getClass() : null;
                if (initialElementClass != currentElementClass) {
                    vecType = kTypedVectorObject;
                    break;
                }
            }
        }

        if (vecType == -1) {
            if (initialElementClass == Integer.class)
                vecType = kTypedVectorInt;
            else if (initialElementClass == Double.class)
                vecType = kTypedVectorDouble;
            else
                vecType = kTypedVectorObject;

        }

        out.write(vecType);

        if (byReference(list))
            return;

        int length = list.size();
        writeUInt29((length << 1) | 1);
        writeBoolean(false /*fixed*/);

        switch (vecType) {
            case kTypedVectorInt:
                if (isDebug)
                    trace.startAMFVector(objectTable.size() - 1, VectorType.INT);

                for (int i = 0; i < length; i++) {
                    if (isDebug)
                        trace.arrayElement(i);

                    Object element = list.get(i);
                    int value = ((Integer) element).intValue();

                    if (isDebug)
                        trace.write(value);

                    writeInt(value);
                }
                break;
            case kTypedVectorDouble:
                if (isDebug)
                    trace.startAMFVector(objectTable.size() - 1, VectorType.DOUBLE);

                for (int i = 0; i < length; i++) {
                    if (isDebug)
                        trace.arrayElement(i);

                    Object element = list.get(i);
                    double value = ((Double) element).doubleValue();

                    if (isDebug)
                        trace.write(value);

                    writeDouble(value);
                }
                break;
            case kTypedVectorObject:
                if (isDebug)
                    trace.startAMFVector(objectTable.size() - 1, VectorType.OBJECT);

                // TODO - I don't think this className is used properly on the client currently.
                String className = initialElementClass != null ? initialElementClass.getName() : "";
                writeStringWithoutType(className);

                for (int i = 0; i < length; i++) {
                    if (isDebug)
                        trace.arrayElement(i);

                    Object element = list.get(i);
                    increaseNestObjectLevel();
                    writeObject(element);
                    decreaseNestObjectLevel();
                }
                break;
            default:
                break;
        }

        if (isDebug)
            trace.endAMFVector();
    }

    /**
     *
     */
    protected void writeMapAsECMAArray(Map map) throws IOException {
        out.write(kArrayType);

        if (!byReference(map)) {
            if (isDebug)
                trace.startECMAArray(getObjectTableSize());

            writeUInt29((0 << 1) | 1);

            Iterator it = map.keySet().iterator();
            while (it.hasNext()) {
                Object key = it.next();
                if (key != null) {
                    String propName = key.toString();
                    writeStringWithoutType(propName);

                    if (isDebug)
                        trace.namedElement(propName);

                    increaseNestObjectLevel();
                    writeObject(map.get(key));
                    decreaseNestObjectLevel();
                }
            }

            writeStringWithoutType(EMPTY_STRING);

            if (isDebug)
                trace.endAMFArray();
        }
    }

    /**
     *
     */
    protected void writeAMFNull() throws IOException {
        if (isDebug)
            trace.writeNull();

        out.write(kNullType);
    }

    /**
     *
     */
    protected void writeAMFString(String s) throws IOException {
        out.write(kStringType);
        writeStringWithoutType(s);

        if (isDebug) {
            trace.writeString(s);
        }
    }

    /**
     *
     */
    protected void writeStringWithoutType(String s) throws IOException {
        if (s.length() == 0) {
            // don't create a reference for the empty string,
            // as it's represented by the one byte value 1
            // len = 0, ((len << 1) | 1).
            writeUInt29(1);
            return;
        }

        if (!byReference(s)) {
            writeAMFUTF(s);
            return;
        }
    }

    /**
     *
     */
    protected void writeAMFArray(Object o, Class componentType) throws IOException {
        if (componentType.isPrimitive()) {
            writePrimitiveArray(o);
        } else if (componentType.equals(Byte.class)) {
            writeAMFByteArray((Byte[]) o);
        } else if (componentType.equals(Character.class)) {
            writeCharArrayAsString((Character[]) o);
        } else {
            writeObjectArray((Object[]) o, null);
        }
    }

    /**
     *
     */
    protected void writeArrayCollection(Collection col, SerializationDescriptor desc) throws IOException {
        out.write(kObjectType);

        if (!byReference(col)) {
            ArrayCollection ac;

            if (col instanceof ArrayCollection) {
                ac = (ArrayCollection) col;
                // TODO: QUESTION: Pete, ignoring the descriptor here... not sure if
                // we should modify the user's AC as that could cause corruption?
            } else {
                // Wrap any Collection in an ArrayCollection
                ac = new ArrayCollection(col);
                if (desc != null)
                    ac.setDescriptor(desc);
            }

            // Then wrap ArrayCollection in PropertyProxy for bean-like serialization
            PropertyProxy proxy = PropertyProxyRegistry.getProxy(ac);
            writePropertyProxy(proxy, ac);
        }
    }

    /**
     *
     */
    protected void writeCustomObject(Object o) throws IOException {
        PropertyProxy proxy = null;

        if (o instanceof PropertyProxy) {
            proxy = (PropertyProxy) o;
            o = proxy.getDefaultInstance();

            // The proxy may wrap a null default instance, if so, short circuit here.
            if (o == null) {
                writeAMFNull();
                return;
            }

            // HACK: Short circuit and unwrap if PropertyProxy is wrapping an Array
            // or Collection or Map with legacyMap as true since we don't yet have
            // the ability to proxy multiple AMF types. We write an AMF Array directly
            // instead of an AMF Object...
            else if (o instanceof Collection) {
                if (context.legacyCollection)
                    writeCollection((Collection) o, proxy.getDescriptor());
                else
                    writeArrayCollection((Collection) o, proxy.getDescriptor());
                return;
            } else if (o.getClass().isArray()) {
                writeObjectArray((Object[]) o, proxy.getDescriptor());
                return;
            } else if (context.legacyMap && o instanceof Map && !(o instanceof ASObject)) {
                writeMapAsECMAArray((Map) o);
                return;
            }
        }

        out.write(kObjectType);

        if (!byReference(o)) {
            if (proxy == null) {
                proxy = PropertyProxyRegistry.getProxyAndRegister(o);
            }

            writePropertyProxy(proxy, o);
        }
    }

    /**
     *
     */
    protected void writePropertyProxy(PropertyProxy proxy, Object instance) throws IOException {
        /*
         * At this point we substitute the instance we want to serialize.
         */
        Object newInst = proxy.getInstanceToSerialize(instance);
        if (newInst != instance) {
            // We can't use writeAMFNull here I think since we already added this object
            // to the object table on the server side.  The player won't have any way
            // of knowing we have this reference mapped to null.
            if (newInst == null)
                throw new MessageException("PropertyProxy.getInstanceToSerialize class: " + proxy.getClass() + " returned null for instance class: " + instance.getClass().getName());

            // Grab a new proxy if necessary for the new instance
            proxy = PropertyProxyRegistry.getProxyAndRegister(newInst);
            instance = newInst;
        }

        List propertyNames = null;
        boolean externalizable = proxy.isExternalizable(instance);

        if (!externalizable) {
            propertyNames = proxy.getPropertyNames(instance);
            // filter write-only properties
            if (proxy instanceof BeanProxy) {
                BeanProxy bp = (BeanProxy) proxy;
                if (propertyNames != null && !propertyNames.isEmpty()) {
                    List<String> propertiesToRemove = null;
                    for (int i = 0; i < propertyNames.size(); i++) {
                        String propName = (String) propertyNames.get(i);
                        if (bp.isWriteOnly(instance, propName)) {
                            if (propertiesToRemove == null)
                                propertiesToRemove = new ArrayList<String>();
                            propertiesToRemove.add(propName);
                        }
                    }
                    if (propertiesToRemove != null)
                        propertyNames.removeAll(propertiesToRemove);
                }
            }
        }


        TraitsInfo ti = new TraitsInfo(proxy.getAlias(instance), proxy.isDynamic(), externalizable, propertyNames);
        writeObjectTraits(ti);

        if (externalizable) {
            // Call user defined serialization
            ((Externalizable) instance).writeExternal(this);
        } else if (propertyNames != null && !propertyNames.isEmpty()) {
            for (int i = 0; i < propertyNames.size(); i++) {
                String propName = (String) propertyNames.get(i);
                Object value = proxy.getValue(instance, propName);
                writeObjectProperty(propName, value);
            }
        }

        writeObjectEnd();
    }


    /**
     * Serialize an array of primitives.
     * <p>
     * Primitives include the following:
     * boolean, char, double, float, long, int, short, byte
     * </p>
     *
     * @param obj An array of primitives
     */
    protected void writePrimitiveArray(Object obj) throws IOException {
        Class aType = obj.getClass().getComponentType();

        if (aType.equals(Character.TYPE)) {
            //Treat char[] as a String
            char[] c = (char[]) obj;
            writeCharArrayAsString(c);
        } else if (aType.equals(Byte.TYPE)) {
            writeAMFByteArray((byte[]) obj);
        } else {
            out.write(kArrayType);

            if (!byReference(obj)) {
                if (aType.equals(Boolean.TYPE)) {
                    boolean[] b = (boolean[]) obj;

                    // Write out an invalid reference, storing the length in the unused 28-bits.
                    writeUInt29((b.length << 1) | 1);

                    // Send an empty string to imply no named keys
                    writeStringWithoutType(EMPTY_STRING);

                    if (isDebug) {
                        trace.startAMFArray(getObjectTableSize());

                        for (int i = 0; i < b.length; i++) {
                            trace.arrayElement(i);
                            writeAMFBoolean(b[i]);
                        }

                        trace.endAMFArray();
                    } else {
                        for (int i = 0; i < b.length; i++) {
                            writeAMFBoolean(b[i]);
                        }
                    }
                } else if (aType.equals(Integer.TYPE) || aType.equals(Short.TYPE)) {
                    //We have a primitive number, either an int or short
                    //We write all of these as Integers...
                    int length = Array.getLength(obj);

                    // Write out an invalid reference, storing the length in the unused 28-bits.
                    writeUInt29((length << 1) | 1);
                    // Send an empty string to imply no named keys
                    writeStringWithoutType(EMPTY_STRING);

                    if (isDebug) {
                        trace.startAMFArray(getObjectTableSize());

                        for (int i = 0; i < length; i++) {
                            trace.arrayElement(i);
                            int v = Array.getInt(obj, i);
                            writeAMFInt(v);
                        }

                        trace.endAMFArray();
                    } else {
                        for (int i = 0; i < length; i++) {
                            int v = Array.getInt(obj, i);
                            writeAMFInt(v);
                        }
                    }
                } else {
                    //We have a primitive number, either a double, float, or long
                    //We write all of these as doubles...
                    int length = Array.getLength(obj);

                    // Write out an invalid reference, storing the length in the unused 28-bits.
                    writeUInt29((length << 1) | 1);
                    // Send an empty string to imply no named keys
                    writeStringWithoutType(EMPTY_STRING);

                    if (isDebug) {
                        trace.startAMFArray(getObjectTableSize());

                        for (int i = 0; i < length; i++) {
                            trace.arrayElement(i);
                            double v = Array.getDouble(obj, i);
                            writeAMFDouble(v);
                        }

                        trace.endAMFArray();
                    } else {
                        for (int i = 0; i < length; i++) {
                            double v = Array.getDouble(obj, i);
                            writeAMFDouble(v);
                        }
                    }
                }
            }
        }
    }

    /**
     *
     */
    protected void writeAMFByteArray(byte[] ba) throws IOException {
        out.write(kByteArrayType);

        if (!byReference(ba)) {
            int length = ba.length;

            // Write out an invalid reference, storing the length in the unused 28-bits.
            writeUInt29((length << 1) | 1);

            if (isDebug) {
                trace.startByteArray(getObjectTableSize(), length);
            }

            out.write(ba, 0, length);
        }
    }

    /**
     *
     */
    protected void writeAMFByteArray(Byte[] ba) throws IOException {
        out.write(kByteArrayType);

        if (!byReference(ba)) {
            int length = ba.length;

            // Write out an invalid reference, storing the length in the unused 28-bits.
            writeUInt29((length << 1) | 1);

            if (isDebug) {
                trace.startByteArray(getObjectTableSize(), length);
            }

            for (int i = 0; i < ba.length; i++) {
                Byte b = ba[i];
                if (b == null)
                    out.write(0);
                else
                    out.write(b.byteValue());
            }
        }
    }

    /**
     *
     */
    protected void writeCharArrayAsString(Character[] ca) throws IOException {
        int length = ca.length;
        char[] chars = new char[length];

        for (int i = 0; i < length; i++) {
            Character c = ca[i];
            if (c == null)
                chars[i] = 0;
            else
                chars[i] = ca[i].charValue();
        }
        writeCharArrayAsString(chars);
    }

    /**
     *
     */
    protected void writeCharArrayAsString(char[] ca) throws IOException {
        String str = new String(ca);
        writeAMFString(str);
    }

    /**
     *
     */
    protected void writeObjectArray(Object[] values, SerializationDescriptor descriptor) throws IOException {
        out.write(kArrayType);

        if (!byReference(values)) {
            if (isDebug)
                trace.startAMFArray(getObjectTableSize());

            writeUInt29((values.length << 1) | 1);

            // Send an empty string to imply no named keys
            writeStringWithoutType(EMPTY_STRING);

            for (int i = 0; i < values.length; ++i) {
                if (isDebug)
                    trace.arrayElement(i);

                Object item = values[i];
                if (item != null && descriptor != null && !(item instanceof String)
                        && !(item instanceof Number) && !(item instanceof Boolean)
                        && !(item instanceof Character)) {
                    PropertyProxy proxy = PropertyProxyRegistry.getProxy(item);
                    proxy = (PropertyProxy) proxy.clone();
                    proxy.setDescriptor(descriptor);
                    proxy.setDefaultInstance(item);
                    item = proxy;
                }
                increaseNestObjectLevel();
                writeObject(item);
                decreaseNestObjectLevel();
            }

            if (isDebug)
                trace.endAMFArray();
        }
    }

    /**
     *
     */
    protected void writeCollection(Collection c, SerializationDescriptor descriptor) throws IOException {
        out.write(kArrayType);

        // Note: We process Collections independently of Object[]
        // as we want the reference to be based on the actual
        // Collection.
        if (!byReference(c)) {
            if (isDebug)
                trace.startAMFArray(getObjectTableSize());

            writeUInt29((c.size() << 1) | 1);

            // Send an empty string to imply no named keys
            writeStringWithoutType(EMPTY_STRING);

            Iterator it = c.iterator();
            int i = 0;
            while (it.hasNext()) {
                if (isDebug)
                    trace.arrayElement(i);

                Object item = it.next();

                if (item != null && descriptor != null && !(item instanceof String)
                        && !(item instanceof Number) && !(item instanceof Boolean)
                        && !(item instanceof Character)) {
                    PropertyProxy proxy = PropertyProxyRegistry.getProxy(item);
                    proxy = (PropertyProxy) proxy.clone();
                    proxy.setDescriptor(descriptor);
                    proxy.setDefaultInstance(item);
                    item = proxy;
                }
                increaseNestObjectLevel();
                writeObject(item);
                decreaseNestObjectLevel();

                i++;
            }

            if (isDebug)
                trace.endAMFArray();
        }
    }

    /**
     *
     */
    protected void writeUInt29(int ref) throws IOException {
        // Represent smaller integers with fewer bytes using the most
        // significant bit of each byte. The worst case uses 32-bits
        // to represent a 29-bit number, which is what we would have
        // done with no compression.

        // 0x00000000 - 0x0000007F : 0xxxxxxx
        // 0x00000080 - 0x00003FFF : 1xxxxxxx 0xxxxxxx
        // 0x00004000 - 0x001FFFFF : 1xxxxxxx 1xxxxxxx 0xxxxxxx
        // 0x00200000 - 0x3FFFFFFF : 1xxxxxxx 1xxxxxxx 1xxxxxxx xxxxxxxx
        // 0x40000000 - 0xFFFFFFFF : throw range exception
        if (ref < 0x80) {
            // 0x00000000 - 0x0000007F : 0xxxxxxx
            out.writeByte(ref);
        } else if (ref < 0x4000) {
            // 0x00000080 - 0x00003FFF : 1xxxxxxx 0xxxxxxx
            out.writeByte(((ref >> 7) & 0x7F) | 0x80);
            out.writeByte(ref & 0x7F);

        } else if (ref < 0x200000) {
            // 0x00004000 - 0x001FFFFF : 1xxxxxxx 1xxxxxxx 0xxxxxxx
            out.writeByte(((ref >> 14) & 0x7F) | 0x80);
            out.writeByte(((ref >> 7) & 0x7F) | 0x80);
            out.writeByte(ref & 0x7F);

        } else if (ref < 0x40000000) {
            // 0x00200000 - 0x3FFFFFFF : 1xxxxxxx 1xxxxxxx 1xxxxxxx xxxxxxxx
            out.writeByte(((ref >> 22) & 0x7F) | 0x80);
            out.writeByte(((ref >> 15) & 0x7F) | 0x80);
            out.writeByte(((ref >> 8) & 0x7F) | 0x80);
            out.writeByte(ref & 0xFF);

        } else {
            // 0x40000000 - 0xFFFFFFFF : throw range exception
            throw new MessageException("Integer out of range: " + ref);
        }
    }

    /**
     *
     */
    public void writeAMFUTF(String s) throws IOException {
        int strlen = s.length();
        int utflen = 0;
        int c, count = 0;

        char[] charr = getTempCharArray(strlen);
        s.getChars(0, strlen, charr, 0);

        for (int i = 0; i < strlen; i++) {
            c = charr[i];
            if (c <= 0x007F) {
                utflen++;
            } else if (c > 0x07FF) {
                utflen += 3;
            } else {
                utflen += 2;
            }
        }

        writeUInt29((utflen << 1) | 1);

        byte[] bytearr = getTempByteArray(utflen);

        for (int i = 0; i < strlen; i++) {
            c = charr[i];
            if (c <= 0x007F) {
                bytearr[count++] = (byte) c;
            } else if (c > 0x07FF) {
                bytearr[count++] = (byte) (0xE0 | ((c >> 12) & 0x0F));
                bytearr[count++] = (byte) (0x80 | ((c >> 6) & 0x3F));
                bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
            } else {
                bytearr[count++] = (byte) (0xC0 | ((c >> 6) & 0x1F));
                bytearr[count++] = (byte) (0x80 | ((c >> 0) & 0x3F));
            }
        }
        out.write(bytearr, 0, utflen);
    }

    /**
     * Attempts to serialize the object as a reference.
     * If the object cannot be serialized as a reference, it is stored
     * in the reference collection for potential future encounter.
     *
     * @return Success/failure indicator as to whether the object could be
     * serialized as a reference.
     */
    protected boolean byReference(Object o) throws IOException {
        if (objectTable != null && objectTable.containsKey(o)) {
            try {
                int refNum = objectTable.get(o).intValue();

                if (isDebug)
                    trace.writeRef(refNum);

                writeUInt29(refNum << 1);

                return true;
            } catch (ClassCastException e) {
                throw new IOException("Object reference is not an Integer");
            }
        }

        if (objectTable == null)
            objectTable = new IdentityHashMap<Object, Integer>(64);
        objectTable.put(o, Integer.valueOf(objectTable.size()));
        return false;
    }

    /**
     *
     */
    public void addObjectReference(Object o) throws IOException {
        byReference(o);
    }

    /**
     *
     */
    protected boolean byReference(String s) throws IOException {
        if (stringTable != null && stringTable.containsKey(s)) {
            try {
                int refNum = stringTable.get(s).intValue();

                writeUInt29(refNum << 1);

                if (isDebug && Trace.amf)
                    trace.writeStringRef(refNum);
                return true;
            } catch (ClassCastException e) {
                throw new IOException("String reference is not an Integer");
            }
        }
        if (stringTable == null)
            stringTable = new HashMap<String, Integer>(64);
        stringTable.put(s, Integer.valueOf(stringTable.size()));
        return false;
    }

    /**
     *
     */
    protected boolean byReference(TraitsInfo ti) throws IOException {
        if (traitsTable != null && traitsTable.containsKey(ti)) {
            try {
                int refNum = traitsTable.get(ti).intValue();

                writeUInt29((refNum << 2) | 1);

                if (isDebug && Trace.amf)
                    trace.writeTraitsInfoRef(refNum);
                return true;
            } catch (ClassCastException e) {
                throw new IOException("TraitsInfo reference is not an Integer");
            }
        }
        if (traitsTable == null)
            traitsTable = new HashMap<TraitsInfo, Integer>(10);
        traitsTable.put(ti, Integer.valueOf(traitsTable.size()));
        return false;
    }

    protected int getObjectTableSize() {
        return objectTable != null ? objectTable.size() - 1 : 0;
    }
}
