/*
 * 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 org.apache.ignite.internal.marshaller.optimized;

import java.io.Externalizable;
import java.io.IOException;
import java.io.NotActiveException;
import java.io.ObjectInputStream;
import java.io.ObjectInputValidation;
import java.io.ObjectStreamClass;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.ConcurrentMap;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.internal.util.GridUnsafe;
import org.apache.ignite.internal.util.io.GridDataInput;
import org.apache.ignite.internal.util.typedef.internal.SB;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.marshaller.MarshallerContext;

import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.ARRAY_LIST;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.BOOLEAN;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.BOOLEAN_ARR;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.BYTE;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.BYTE_ARR;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.CHAR;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.CHAR_ARR;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.CLS;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.DATE;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.DOUBLE;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.DOUBLE_ARR;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.ENUM;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.EXTERNALIZABLE;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.FLOAT;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.FLOAT_ARR;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.HANDLE;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.HASH_MAP;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.HASH_SET;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.HASH_SET_MAP_OFF;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.INT;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.INT_ARR;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.JDK;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.LINKED_HASH_MAP;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.LINKED_HASH_SET;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.LINKED_LIST;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.LONG;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.LONG_ARR;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.NULL;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.OBJ_ARR;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.PROPS;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.PROXY;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.SERIALIZABLE;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.SHORT;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.SHORT_ARR;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.STR;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.UUID;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.classDescriptor;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.setBoolean;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.setByte;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.setChar;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.setDouble;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.setFloat;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.setInt;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.setLong;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.setObject;
import static org.apache.ignite.internal.marshaller.optimized.OptimizedMarshallerUtils.setShort;

/**
 * Optimized object input stream.
 */
class OptimizedObjectInputStream extends ObjectInputStream {
    /** Dummy object for HashSet. */
    private static final Object DUMMY = new Object();

    /** */
    private final HandleTable handles = new HandleTable(10);

    /** */
    private MarshallerContext ctx;

    /** */
    private OptimizedMarshallerIdMapper mapper;

    /** */
    private ClassLoader clsLdr;

    /** */
    private GridDataInput in;

    /** */
    private Object curObj;

    /** */
    private OptimizedClassDescriptor.ClassFields curFields;

    /** */
    private Class<?> curCls;

    /** */
    private ConcurrentMap<Class, OptimizedClassDescriptor> clsMap;

    /** The flag shown, the reader uses class loader cache used or not. */
    private boolean useCache;

    /**
     * @param in Input.
     * @throws IOException In case of error.
     */
    OptimizedObjectInputStream(GridDataInput in) throws IOException {
        this.in = in;
        this.useCache = true;
    }

    /**
     * @param clsMap Class descriptors by class map.
     * @param ctx Context.
     * @param mapper ID mapper.
     * @param clsLdr Class loader.
     * @param useCache True if class loader cache will be used, false otherwise.
     */
    void context(
        ConcurrentMap<Class, OptimizedClassDescriptor> clsMap,
        MarshallerContext ctx,
        OptimizedMarshallerIdMapper mapper,
        ClassLoader clsLdr,
        boolean useCache)
    {
        this.clsMap = clsMap;
        this.ctx = ctx;
        this.mapper = mapper;
        this.clsLdr = clsLdr;
        this.useCache = useCache;
    }

    /**
     * @return Input.
     */
    public GridDataInput in() {
        return in;
    }

    /**
     * @param in Input.
     */
    public void in(GridDataInput in) {
        this.in = in;
    }

    /** {@inheritDoc} */
    @Override public void close() throws IOException {
        reset();

        ctx = null;
        clsLdr = null;
        clsMap = null;
    }

    /** {@inheritDoc} */
    @SuppressWarnings("NonSynchronizedMethodOverridesSynchronizedMethod")
    @Override public void reset() throws IOException {
        in.reset();
        handles.clear();

        curObj = null;
        curFields = null;
    }

    /** {@inheritDoc} */
    @Override public Object readObjectOverride() throws ClassNotFoundException, IOException {
        Object oldObj = curObj;

        OptimizedClassDescriptor.ClassFields oldFields = curFields;

        try {
            return readObject0();
        }
        finally {
            curObj = oldObj;

            curFields = oldFields;
        }
    }

    /**
     * Reads object from stream.
     *
     * @return Object.
     * @throws ClassNotFoundException If class was not found.
     * @throws IOException In case of error.
     */
    private Object readObject0() throws ClassNotFoundException, IOException {
        curObj = null;
        curFields = null;

        byte ref = in.readByte();

        switch (ref) {
            case NULL:
                return null;

            case HANDLE:
                return handles.lookup(readInt());

            case JDK:
                try {
                    return ctx.jdkMarshaller().unmarshal(this, clsLdr);
                }
                catch (IgniteCheckedException e) {
                    IOException ioEx = e.getCause(IOException.class);

                    if (ioEx != null)
                        throw ioEx;
                    else
                        throw new IOException("Failed to deserialize object with JDK marshaller.", e);
                }

            case BYTE:
                return readByte();

            case SHORT:
                return readShort();

            case INT:
                return readInt();

            case LONG:
                return readLong();

            case FLOAT:
                return readFloat();

            case DOUBLE:
                return readDouble();

            case CHAR:
                return readChar();

            case BOOLEAN:
                return readBoolean();

            case BYTE_ARR:
                return readByteArray();

            case SHORT_ARR:
                return readShortArray();

            case INT_ARR:
                return readIntArray();

            case LONG_ARR:
                return readLongArray();

            case FLOAT_ARR:
                return readFloatArray();

            case DOUBLE_ARR:
                return readDoubleArray();

            case CHAR_ARR:
                return readCharArray();

            case BOOLEAN_ARR:
                return readBooleanArray();

            case OBJ_ARR:
                return readArray(readClass());

            case STR:
                return readString();

            case UUID:
                return readUuid();

            case PROPS:
                return readProperties();

            case ARRAY_LIST:
                return readArrayList();

            case HASH_MAP:
                return readHashMap(false);

            case HASH_SET:
                return readHashSet(HASH_SET_MAP_OFF);

            case LINKED_LIST:
                return readLinkedList();

            case LINKED_HASH_MAP:
                return readLinkedHashMap(false);

            case LINKED_HASH_SET:
                return readLinkedHashSet(HASH_SET_MAP_OFF);

            case DATE:
                return readDate();

            case CLS:
                return readClass();

            case PROXY:
                Class<?>[] intfs = new Class<?>[readInt()];

                for (int i = 0; i < intfs.length; i++)
                    intfs[i] = readClass();

                InvocationHandler ih = (InvocationHandler)readObject();

                return Proxy.newProxyInstance(clsLdr != null ? clsLdr : U.gridClassLoader(), intfs, ih);

            case ENUM:
            case EXTERNALIZABLE:
            case SERIALIZABLE:
                int typeId = readInt();

                OptimizedClassDescriptor desc = typeId == 0 ?
                    classDescriptor(clsMap, U.forName(readUTF(), clsLdr, ctx.classNameFilter()), useCache, ctx, mapper) :
                    classDescriptor(clsMap, typeId, clsLdr, useCache, ctx, mapper);

                curCls = desc.describedClass();

                try {
                    return desc.read(this);
                }
                catch (IOException e) {
                    throw new IOException("Failed to deserialize object [typeName=" +
                        desc.describedClass().getName() + ']', e);
                }

            default:
                SB msg = new SB("Unexpected error occurred during unmarshalling");

                if (curCls != null)
                    msg.a(" of an instance of the class: ").a(curCls.getName());

                msg.a(". Check that all nodes are running the same version of Ignite and that all nodes have " +
                    "GridOptimizedMarshaller configured with identical optimized classes lists, if any " +
                    "(see setClassNames and setClassNamesPath methods). If your serialized classes implement " +
                    "java.io.Externalizable interface, verify that serialization logic is correct.");

                throw new IOException(msg.toString());
        }
    }

    /**
     * @return Class.
     * @throws ClassNotFoundException If class was not found.
     * @throws IOException In case of other error.
     */
    private Class<?> readClass() throws ClassNotFoundException, IOException {
        int compTypeId = readInt();

        return compTypeId == 0 ? U.forName(readUTF(), clsLdr, null, useCache) :
            classDescriptor(clsMap, compTypeId, clsLdr, useCache, ctx, mapper).describedClass();
    }

    /**
     * Reads array from this stream.
     *
     * @param compType Array component type.
     * @return Array.
     * @throws ClassNotFoundException If class not found.
     * @throws IOException In case of error.
     */
    <T> T[] readArray(Class<T> compType) throws ClassNotFoundException, IOException {
        int len = in.readInt();

        T[] arr = (T[])Array.newInstance(compType, len);

        handles.assign(arr);

        for (int i = 0; i < len; i++)
            arr[i] = (T)readObject();

        return arr;
    }

    /**
     * Reads {@link UUID} from this stream.
     *
     * @return UUID.
     * @throws IOException In case of error.
     */
    UUID readUuid() throws IOException {
        UUID uuid = new UUID(readLong(), readLong());

        handles.assign(uuid);

        return uuid;
    }

    /**
     * Reads {@link Properties} from this stream.
     *
     * @return Properties.
     * @throws ClassNotFoundException If class not found.
     * @throws IOException In case of error.
     */
    Properties readProperties() throws ClassNotFoundException, IOException {
        Properties dflts = readBoolean() ? null : (Properties)readObject();

        Properties props = new Properties(dflts);

        int size = in.readInt();

        for (int i = 0; i < size; i++)
            props.setProperty(readUTF(), readUTF());

        handles.assign(props);

        return props;
    }

    /**
     * Reads and sets all non-static and non-transient field values from this stream.
     *
     * @param obj Object.
     * @param fieldOffs Field offsets.
     * @throws ClassNotFoundException If class not found.
     * @throws IOException In case of error.
     */
    void readFields(Object obj, OptimizedClassDescriptor.ClassFields fieldOffs) throws ClassNotFoundException,
        IOException {
        for (int i = 0; i < fieldOffs.size(); i++) {
            OptimizedClassDescriptor.FieldInfo t = fieldOffs.get(i);

            try {
                switch ((t.type())) {
                    case BYTE:
                        byte resByte = readByte();

                        if (t.field() != null)
                            setByte(obj, t.offset(), resByte);

                        break;

                    case SHORT:
                        short resShort = readShort();

                        if (t.field() != null)
                            setShort(obj, t.offset(), resShort);

                        break;

                    case INT:
                        int resInt = readInt();

                        if (t.field() != null)
                            setInt(obj, t.offset(), resInt);

                        break;

                    case LONG:
                        long resLong = readLong();

                        if (t.field() != null)
                            setLong(obj, t.offset(), resLong);

                        break;

                    case FLOAT:
                        float resFloat = readFloat();

                        if (t.field() != null)
                            setFloat(obj, t.offset(), resFloat);

                        break;

                    case DOUBLE:
                        double resDouble = readDouble();

                        if (t.field() != null)
                            setDouble(obj, t.offset(), resDouble);

                        break;

                    case CHAR:
                        char resChar = readChar();

                        if (t.field() != null)
                            setChar(obj, t.offset(), resChar);

                        break;

                    case BOOLEAN:
                        boolean resBoolean = readBoolean();

                        if (t.field() != null)
                            setBoolean(obj, t.offset(), resBoolean);

                        break;

                    case OTHER:
                        Object resObject = readObject();

                        if (t.field() != null)
                            setObject(obj, t.offset(), resObject);
                }
            }
            catch (IOException e) {
                throw new IOException("Failed to deserialize field [name=" + t.name() + ']', e);
            }
        }
    }

    /**
     * Reads {@link Externalizable} object.
     *
     * @param constructor Constructor.
     * @param readResolveMtd {@code readResolve} method.
     * @return Object.
     * @throws ClassNotFoundException If class not found.
     * @throws IOException In case of error.
     */
    Object readExternalizable(Constructor<?> constructor, Method readResolveMtd)
        throws ClassNotFoundException, IOException {
        Object obj;

        try {
            obj = constructor.newInstance();
        }
        catch (InstantiationException | InvocationTargetException | IllegalAccessException e) {
            throw new IOException(e);
        }

        int handle = handles.assign(obj);

        Externalizable extObj = ((Externalizable)obj);

        extObj.readExternal(this);

        if (readResolveMtd != null) {
            try {
                obj = readResolveMtd.invoke(obj);

                handles.set(handle, obj);
            }
            catch (IllegalAccessException | InvocationTargetException e) {
                throw new IOException(e);
            }
        }

        return obj;
    }

    /**
     * Reads serializable object.
     *
     * @param cls Class.
     * @param mtds {@code readObject} methods.
     * @param readResolveMtd {@code readResolve} method.
     * @param fields class fields details.
     * @return Object.
     * @throws ClassNotFoundException If class not found.
     * @throws IOException In case of error.
     */
    Object readSerializable(Class<?> cls, List<Method> mtds, Method readResolveMtd,
        OptimizedClassDescriptor.Fields fields) throws ClassNotFoundException, IOException {
        Object obj;

        try {
            obj = GridUnsafe.allocateInstance(cls);
        }
        catch (InstantiationException e) {
            throw new IOException(e);
        }

        int handle = handles.assign(obj);

        for (int i = 0; i < mtds.size(); i++) {
            Method mtd = mtds.get(i);

            if (mtd != null) {
                curObj = obj;
                curFields = fields.fields(i);

                try {
                    mtd.invoke(obj, this);
                }
                catch (IllegalAccessException | InvocationTargetException e) {
                    throw new IOException(e);
                }
            }
            else
                readFields(obj, fields.fields(i));
        }

        if (readResolveMtd != null) {
            try {
                obj = readResolveMtd.invoke(obj);

                handles.set(handle, obj);
            }
            catch (IllegalAccessException | InvocationTargetException e) {
                throw new IOException(e);
            }
        }

        return obj;
    }

    /**
     * Reads {@link ArrayList}.
     *
     * @return List.
     * @throws ClassNotFoundException If class not found.
     * @throws IOException In case of error.
     */
    ArrayList<?> readArrayList() throws ClassNotFoundException, IOException {
        int size = readInt();

        ArrayList<Object> list = new ArrayList<>(size);

        handles.assign(list);

        for (int i = 0; i < size; i++)
            list.add(readObject());

        return list;
    }

    /**
     * Reads {@link HashMap}.
     *
     * @param set Whether reading underlying map from {@link HashSet}.
     * @return Map.
     * @throws ClassNotFoundException If class not found.
     * @throws IOException In case of error.
     */
    HashMap<?, ?> readHashMap(boolean set) throws ClassNotFoundException, IOException {
        int size = readInt();
        float loadFactor = readFloat();

        HashMap<Object, Object> map = new HashMap<>(size, loadFactor);

        if (!set)
            handles.assign(map);

        for (int i = 0; i < size; i++) {
            Object key = readObject();
            Object val = !set ? readObject() : DUMMY;

            map.put(key, val);
        }

        return map;
    }

    /**
     * Reads {@link HashSet}.
     *
     * @param mapFieldOff Map field offset.
     * @return Set.
     * @throws ClassNotFoundException If class not found.
     * @throws IOException In case of error.
     */
    HashSet<?> readHashSet(long mapFieldOff) throws ClassNotFoundException, IOException {
        try {
            HashSet<Object> set = (HashSet<Object>)GridUnsafe.allocateInstance(HashSet.class);

            handles.assign(set);

            setObject(set, mapFieldOff, readHashMap(true));

            return set;
        }
        catch (InstantiationException e) {
            throw new IOException(e);
        }
    }

    /**
     * Reads {@link LinkedList}.
     *
     * @return List.
     * @throws ClassNotFoundException If class not found.
     * @throws IOException In case of error.
     */
    LinkedList<?> readLinkedList() throws ClassNotFoundException, IOException {
        int size = readInt();

        LinkedList<Object> list = new LinkedList<>();

        handles.assign(list);

        for (int i = 0; i < size; i++)
            list.add(readObject());

        return list;
    }

    /**
     * Reads {@link LinkedHashMap}.
     *
     * @param set Whether reading underlying map from {@link LinkedHashSet}.
     * @return Map.
     * @throws ClassNotFoundException If class not found.
     * @throws IOException In case of error.
     */
    LinkedHashMap<?, ?> readLinkedHashMap(boolean set) throws ClassNotFoundException, IOException {
        int size = readInt();
        float loadFactor = readFloat();
        boolean accessOrder = readBoolean();

        LinkedHashMap<Object, Object> map = new LinkedHashMap<>(size, loadFactor, accessOrder);

        if (!set)
            handles.assign(map);

        for (int i = 0; i < size; i++) {
            Object key = readObject();
            Object val = !set ? readObject() : DUMMY;

            map.put(key, val);
        }

        return map;
    }

    /**
     * Reads {@link LinkedHashSet}.
     *
     * @param mapFieldOff Map field offset.
     * @return Set.
     * @throws ClassNotFoundException If class not found.
     * @throws IOException In case of error.
     */
    LinkedHashSet<?> readLinkedHashSet(long mapFieldOff) throws ClassNotFoundException, IOException {
        try {
            LinkedHashSet<Object> set = (LinkedHashSet<Object>)GridUnsafe.allocateInstance(LinkedHashSet.class);

            handles.assign(set);

            setObject(set, mapFieldOff, readLinkedHashMap(true));

            return set;
        }
        catch (InstantiationException e) {
            throw new IOException(e);
        }
    }

    /**
     * Reads {@link Date}.
     *
     * @return Date.
     * @throws ClassNotFoundException If class not found.
     * @throws IOException In case of error.
     */
    Date readDate() throws ClassNotFoundException, IOException {
        Date date = new Date(readLong());

        handles.assign(date);

        return date;
    }

    /**
     * Reads array of {@code byte}s.
     *
     * @return Array.
     * @throws IOException In case of error.
     */
    byte[] readByteArray() throws IOException {
        byte[] arr = in.readByteArray();

        handles.assign(arr);

        return arr;
    }

    /**
     * Reads array of {@code short}s.
     *
     * @return Array.
     * @throws IOException In case of error.
     */
    short[] readShortArray() throws IOException {
        short[] arr = in.readShortArray();

        handles.assign(arr);

        return arr;
    }

    /**
     * Reads array of {@code int}s.
     *
     * @return Array.
     * @throws IOException In case of error.
     */
    int[] readIntArray() throws IOException {
        int[] arr = in.readIntArray();

        handles.assign(arr);

        return arr;
    }

    /**
     * Reads array of {@code long}s.
     *
     * @return Array.
     * @throws IOException In case of error.
     */
    long[] readLongArray() throws IOException {
        long[] arr = in.readLongArray();

        handles.assign(arr);

        return arr;
    }

    /**
     * Reads array of {@code float}s.
     *
     * @return Array.
     * @throws IOException In case of error.
     */
    float[] readFloatArray() throws IOException {
        float[] arr = in.readFloatArray();

        handles.assign(arr);

        return arr;
    }

    /**
     * Reads array of {@code double}s.
     *
     * @return Array.
     * @throws IOException In case of error.
     */
    double[] readDoubleArray() throws IOException {
        double[] arr = in.readDoubleArray();

        handles.assign(arr);

        return arr;
    }

    /**
     * Reads array of {@code char}s.
     *
     * @return Array.
     * @throws IOException In case of error.
     */
    char[] readCharArray() throws IOException {
        char[] arr = in.readCharArray();

        handles.assign(arr);

        return arr;
    }

    /**
     * Reads array of {@code boolean}s.
     *
     * @return Array.
     * @throws IOException In case of error.
     */
    boolean[] readBooleanArray() throws IOException {
        boolean[] arr = in.readBooleanArray();

        handles.assign(arr);

        return arr;
    }

    /**
     * Reads {@link String}.
     *
     * @return String.
     * @throws IOException In case of error.
     */
    public String readString() throws IOException {
        String str = in.readUTF();

        handles.assign(str);

        return str;
    }

    /** {@inheritDoc} */
    @Override public void readFully(byte[] b) throws IOException {
        in.readFully(b);
    }

    /** {@inheritDoc} */
    @Override public void readFully(byte[] b, int off, int len) throws IOException {
        in.readFully(b, off, len);
    }

    /** {@inheritDoc} */
    @Override public int skipBytes(int n) throws IOException {
        return in.skipBytes(n);
    }

    /** {@inheritDoc} */
    @Override public boolean readBoolean() throws IOException {
        return in.readBoolean();
    }

    /** {@inheritDoc} */
    @Override public byte readByte() throws IOException {
        return in.readByte();
    }

    /** {@inheritDoc} */
    @Override public int readUnsignedByte() throws IOException {
        return in.readUnsignedByte();
    }

    /** {@inheritDoc} */
    @Override public short readShort() throws IOException {
        return in.readShort();
    }

    /** {@inheritDoc} */
    @Override public int readUnsignedShort() throws IOException {
        return in.readUnsignedShort();
    }

    /** {@inheritDoc} */
    @Override public char readChar() throws IOException {
        return in.readChar();
    }

    /** {@inheritDoc} */
    @Override public int readInt() throws IOException {
        return in.readInt();
    }

    /** {@inheritDoc} */
    @Override public long readLong() throws IOException {
        return in.readLong();
    }

    /** {@inheritDoc} */
    @Override public float readFloat() throws IOException {
        return in.readFloat();
    }

    /** {@inheritDoc} */
    @Override public double readDouble() throws IOException {
        return in.readDouble();
    }

    /** {@inheritDoc} */
    @Override public int read() throws IOException {
        return in.read();
    }

    /** {@inheritDoc} */
    @Override public int read(byte[] b) throws IOException {
        return in.read(b);
    }

    /** {@inheritDoc} */
    @Override public int read(byte[] b, int off, int len) throws IOException {
        return in.read(b, off, len);
    }

    /** {@inheritDoc} */
    @SuppressWarnings("deprecation")
    @Override public String readLine() throws IOException {
        return in.readLine();
    }

    /** {@inheritDoc} */
    @Override public String readUTF() throws IOException {
        return in.readUTF();
    }

    /** {@inheritDoc} */
    @Override public Object readUnshared() throws IOException, ClassNotFoundException {
        return readObject();
    }

    /** {@inheritDoc} */
    @Override public void defaultReadObject() throws IOException, ClassNotFoundException {
        if (curObj == null)
            throw new NotActiveException("Not in readObject() call.");

        readFields(curObj, curFields);
    }

    /** {@inheritDoc} */
    @Override public ObjectInputStream.GetField readFields() throws IOException, ClassNotFoundException {
        if (curObj == null)
            throw new NotActiveException("Not in readObject() call.");

        return new GetFieldImpl(this);
    }

    /** {@inheritDoc} */
    @Override public void registerValidation(ObjectInputValidation obj, int pri) {
        // No-op.
    }

    /** {@inheritDoc} */
    @Override public int available() throws IOException {
        return -1;
    }

    /**
     * Returns objects that were added to handles table.
     * Used ONLY for test purposes.
     *
     * @return Handled objects.
     */
    Object[] handledObjects() {
        return handles.entries;
    }

    /**
     * Lightweight identity hash table which maps objects to integer handles,
     * assigned in ascending order.
     */
    private static class HandleTable {
        /** Array mapping handle -> object/exception (depending on status). */
        private Object[] entries;

        /** Number of handles in table. */
        private int size;

        /**
         * Creates handle table with the given initial capacity.
         *
         * @param initCap Initial capacity.
         */
        HandleTable(int initCap) {
            entries = new Object[initCap];
        }

        /**
         * Assigns next available handle to given object, and returns assigned
         * handle.
         *
         * @param obj Object.
         * @return Handle.
         */
        int assign(Object obj) {
            if (size >= entries.length)
                grow();

            entries[size] = obj;

            return size++;
        }

        /**
         * Assigns new object to existing handle. Old object is forgotten.
         *
         * @param handle Handle.
         * @param obj Object.
         */
        void set(int handle, Object obj) {
            entries[handle] = obj;
        }

        /**
         * Looks up and returns object associated with the given handle.
         *
         * @param handle Handle.
         * @return Object.
         */
        Object lookup(int handle) {
            return entries[handle];
        }

        /**
         * Resets table to its initial state.
         */
        void clear() {
            Arrays.fill(entries, 0, size, null);

            size = 0;
        }

        /**
         * Expands capacity of internal arrays.
         */
        private void grow() {
            int newCap = (entries.length << 1) + 1;

            Object[] newEntries = new Object[newCap];

            System.arraycopy(entries, 0, newEntries, 0, size);

            entries = newEntries;
        }
    }

    /**
     * {@link GetField} implementation.
     */
    private static class GetFieldImpl extends GetField {
        /** Field info. */
        private final OptimizedClassDescriptor.ClassFields fieldInfo;

        /** Values. */
        private final Object[] objs;

        /**
         * @param in Stream.
         * @throws IOException In case of error.
         * @throws ClassNotFoundException If class not found.
         */
        private GetFieldImpl(OptimizedObjectInputStream in) throws IOException, ClassNotFoundException {
            fieldInfo = in.curFields;

            objs = new Object[fieldInfo.size()];

            for (int i = 0; i < fieldInfo.size(); i++) {
                OptimizedClassDescriptor.FieldInfo t = fieldInfo.get(i);

                Object obj = null;

                switch (t.type()) {
                    case BYTE:
                        obj = in.readByte();

                        break;

                    case SHORT:
                        obj = in.readShort();

                        break;

                    case INT:
                        obj = in.readInt();

                        break;

                    case LONG:
                        obj = in.readLong();

                        break;

                    case FLOAT:
                        obj = in.readFloat();

                        break;

                    case DOUBLE:
                        obj = in.readDouble();

                        break;

                    case CHAR:
                        obj = in.readChar();

                        break;

                    case BOOLEAN:
                        obj = in.readBoolean();

                        break;

                    case OTHER:
                        obj = in.readObject();
                }

                objs[i] = obj;
            }
        }

        /** {@inheritDoc} */
        @Override public ObjectStreamClass getObjectStreamClass() {
            throw new UnsupportedOperationException();
        }

        /** {@inheritDoc} */
        @Override public boolean defaulted(String name) throws IOException {
            return objs[fieldInfo.getIndex(name)] == null;
        }

        /** {@inheritDoc} */
        @Override public boolean get(String name, boolean dflt) throws IOException {
            return value(name, dflt);
        }

        /** {@inheritDoc} */
        @Override public byte get(String name, byte dflt) throws IOException {
            return value(name, dflt);
        }

        /** {@inheritDoc} */
        @Override public char get(String name, char dflt) throws IOException {
            return value(name, dflt);
        }

        /** {@inheritDoc} */
        @Override public short get(String name, short dflt) throws IOException {
            return value(name, dflt);
        }

        /** {@inheritDoc} */
        @Override public int get(String name, int dflt) throws IOException {
            return value(name, dflt);
        }

        /** {@inheritDoc} */
        @Override public long get(String name, long dflt) throws IOException {
            return value(name, dflt);
        }

        /** {@inheritDoc} */
        @Override public float get(String name, float dflt) throws IOException {
            return value(name, dflt);
        }

        /** {@inheritDoc} */
        @Override public double get(String name, double dflt) throws IOException {
            return value(name, dflt);
        }

        /** {@inheritDoc} */
        @Override public Object get(String name, Object dflt) throws IOException {
            return value(name, dflt);
        }

        /**
         * @param name Field name.
         * @param dflt Default value.
         * @return Value.
         */
        private <T> T value(String name, T dflt) {
            return objs[fieldInfo.getIndex(name)] != null ? (T)objs[fieldInfo.getIndex(name)] : dflt;
        }
    }
}
