/*
 * Copyright 2014 Ruediger Moeller.
 *
 * Licensed 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.nustaq.serialization;

import org.nustaq.logging.FSTLogger;
import org.nustaq.serialization.coders.Unknown;
import org.nustaq.serialization.minbin.MBObject;
import org.nustaq.serialization.util.FSTUtil;
import java.io.*;
import java.lang.reflect.*;
import java.util.*;

/**
 * Created with IntelliJ IDEA.
 * User: Möller
 * Date: 04.11.12
 * Time: 11:53
 */
/**
 * replacement of ObjectInputStream
 */
public class FSTObjectInput implements ObjectInput {

    private static final FSTLogger logger = FSTLogger.getLogger(FSTObjectInput.class);
    public static boolean REGISTER_ENUMS_READ = false; // do not register enums on read. Flag is saver in case things brake somewhere
    public static ByteArrayInputStream emptyStream = new ByteArrayInputStream(new byte[0]);

    protected FSTDecoder codec;

    protected FSTObjectRegistry objects;

    protected Stack<String> debugStack;
    protected int curDepth;

    protected ArrayList<CallbackEntry> callbacks;
//    FSTConfiguration conf;
    // mirrored from conf
    protected boolean ignoreAnnotations;
    protected FSTClazzInfoRegistry clInfoRegistry;
    // done
    protected ConditionalCallback conditionalCallback;
    protected int readExternalReadAHead = 8000;
    protected VersionConflictListener versionConflictListener;

    protected FSTConfiguration conf;

    // copied values from conf
    protected boolean isCrossPlatform;

    public FSTConfiguration getConf() {
        return conf;
    }

    @Override
    public void readFully(byte[] b) throws IOException {
        readFully(b, 0, b.length);
    }

    @Override
    public void readFully(byte[] b, int off, int len) throws IOException {
        getCodec().readPlainBytes(b, off, len);

    }

    @Override
    public int skipBytes(int n) throws IOException {
        getCodec().skip(n);
        return n;
    }

    @Override
    public boolean readBoolean() throws IOException {
        return getCodec().readFByte() == 0 ? false : true;
    }

    @Override
    public byte readByte() throws IOException {
        return getCodec().readFByte();
    }

    @Override
    public int readUnsignedByte() throws IOException {
        return ((int) getCodec().readFByte()+256) & 0xff;
    }

    @Override
    public short readShort() throws IOException {
        return getCodec().readFShort();
    }

    @Override
    public int readUnsignedShort() throws IOException {
        return ((int)readShort()+65536) & 0xffff;
    }

    @Override
    public char readChar() throws IOException {
        return getCodec().readFChar();
    }

    @Override
    public int readInt() throws IOException {
        return getCodec().readFInt();
    }

    @Override
    public long readLong() throws IOException {
        return getCodec().readFLong();
    }

    @Override
    public float readFloat() throws IOException {
        return getCodec().readFFloat();
    }

    @Override
    public double readDouble() throws IOException {
        return getCodec().readFDouble();
    }

    @Override
    public String readLine() throws IOException {
        throw new RuntimeException("not implemented");
    }

    @Override
    public String readUTF() throws IOException {
        return getCodec().readStringUTF();
    }

    public FSTDecoder getCodec() {
        return codec;
    }

    protected void setCodec(FSTDecoder codec) {
        this.codec = codec;
    }

    public boolean isClosed() {
        return closed;
    }

    protected static class CallbackEntry {
        ObjectInputValidation cb;
        int prio;

        CallbackEntry(ObjectInputValidation cb, int prio) {
            this.cb = cb;
            this.prio = prio;
        }
    }

    public static interface ConditionalCallback {
        public boolean shouldSkip(Object halfDecoded, int streamPosition, Field field);
    }

    public FSTObjectInput() throws IOException {
        this(emptyStream, FSTConfiguration.getDefaultConfiguration());
    }

    public FSTObjectInput(FSTConfiguration conf) {
        this(emptyStream, conf);
    }

    /**
     * Creates a FSTObjectInput that uses the specified
     * underlying InputStream.
     *
     * @param in the specified input stream
     */
    public FSTObjectInput(InputStream in) throws IOException {
        this(in, FSTConfiguration.getDefaultConfiguration());
    }

    /**
     * Creates a FSTObjectInput that uses the specified
     * underlying InputStream.
     *
     * Don't create a FSTConfiguration with each stream, just create one global static configuration and reuseit.
     * FSTConfiguration is threadsafe.
     *
     * @param in the specified input stream
     */
    public FSTObjectInput(InputStream in, FSTConfiguration conf) {
        setCodec(conf.createStreamDecoder());
        getCodec().setInputStream(in);
        isCrossPlatform = conf.isCrossPlatform();
        initRegistries(conf);
        this.conf = conf;
    }

    public Class getClassForName(String name) throws ClassNotFoundException {
        return getCodec().classForName(name);
    }

    protected void initRegistries(FSTConfiguration conf) {
        ignoreAnnotations = conf.getCLInfoRegistry().isIgnoreAnnotations();
        clInfoRegistry = conf.getCLInfoRegistry();

        objects = (FSTObjectRegistry) conf.getCachedObject(FSTObjectRegistry.class);
        if (objects == null) {
            objects = new FSTObjectRegistry(conf);
        } else {
            objects.clearForRead(conf);
        }
    }

    public ConditionalCallback getConditionalCallback() {
        return conditionalCallback;
    }

    public void setConditionalCallback(ConditionalCallback conditionalCallback) {
        this.conditionalCallback = conditionalCallback;
    }

    public int getReadExternalReadAHead() {
        return readExternalReadAHead;
    }

    /**
     * since the stock readXX methods on InputStream are final, i can't ensure sufficient readAhead on the inputStream
     * before calling readExternal. Default value is 16000 bytes. If you make use of the externalizable interfac
     * and write larger Objects a) cast the ObjectInput in readExternal to FSTObjectInput and call ensureReadAhead on this
     * in your readExternal method b) set a sufficient maximum using this method before serializing.
     * @param readExternalReadAHead
     */
    public void setReadExternalReadAHead(int readExternalReadAHead) {
        this.readExternalReadAHead = readExternalReadAHead;
    }

    @Override
    public Object readObject() throws ClassNotFoundException, IOException {
        try {
            return readObject((Class[]) null);
        } catch (Exception e) {
            throw new IOException(e);
        }
    }

    @Override
    public int read() throws IOException {
        return getCodec().readIntByte();
    }

    @Override
    public int read(byte[] b) throws IOException {
        getCodec().readPlainBytes(b, 0, b.length);
        return b.length;
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        getCodec().readPlainBytes(b, off, len);
        return b.length;
    }

    @Override
    public long skip(long n) throws IOException {
        getCodec().skip((int) n);
        return n;
    }

    @Override
    public int available() throws IOException {
        return getCodec().available();
    }

    protected void processValidation() throws InvalidObjectException {
        if (callbacks == null) {
            return;
        }
        Collections.sort(callbacks, new Comparator<CallbackEntry>() {
            @Override
            public int compare(CallbackEntry o1, CallbackEntry o2) {
                return o2.prio - o1.prio;
            }
        });
        for (int i = 0; i < callbacks.size(); i++) {
            CallbackEntry callbackEntry = callbacks.get(i);
            try {
                callbackEntry.cb.validateObject();
            } catch (Exception ex) {
                FSTUtil.<RuntimeException>rethrow(ex);
            }
        }
    }

    public Object readObject(Class... possibles) throws Exception {
        curDepth++;
        if ( isCrossPlatform ) {
            return readObjectInternal(null); // not supported cross platform
        }
        try {
            if (possibles != null && possibles.length > 1 ) {
                for (int i = 0; i < possibles.length; i++) {
                    Class possible = possibles[i];
                    getCodec().registerClass(possible);
                }
            }
            Object res = readObjectInternal(possibles);
            processValidation();
            return res;
        } catch (Throwable th) {
            FSTUtil.<RuntimeException>rethrow(th);
        } finally {
            curDepth--;
        }
        return null;
    }

    protected FSTClazzInfo.FSTFieldInfo infoCache;
    public Object readObjectInternal(Class... expected) throws ClassNotFoundException, IOException, IllegalAccessException, InstantiationException {
        try {
            FSTClazzInfo.FSTFieldInfo info = infoCache;
            infoCache = null;
            if (info == null )
                info = new FSTClazzInfo.FSTFieldInfo(expected, null, ignoreAnnotations);
            else
                info.possibleClasses = expected;
            Object res = readObjectWithHeader(info);
            infoCache = info;
            return res;
        } catch (Throwable t) {
            FSTUtil.<RuntimeException>rethrow(t);
        }
        return null;
    }

    public Object readObjectWithHeader(FSTClazzInfo.FSTFieldInfo referencee) throws Exception {
        FSTClazzInfo clzSerInfo;
        Class c;
        final int readPos = getCodec().getInputPos();
        byte code = getCodec().readObjectHeaderTag();  // NOTICE: THIS ADVANCES THE INPUT STREAM...
        if (code == FSTObjectOutput.OBJECT ) {
            // class name
            clzSerInfo = readClass();
            c = clzSerInfo.getClazz();
            if ( c.isArray() )
                return readArrayNoHeader(referencee,readPos,c);
            // fall through
        } else if ( code == FSTObjectOutput.TYPED ) {
            c = referencee.getType();
            clzSerInfo = getClazzInfo(c, referencee);
        } else if ( code >= 1 ) {
            try {
                c = referencee.getPossibleClasses()[code - 1];
                clzSerInfo = getClazzInfo(c, referencee);
            } catch (Throwable th) {
                clzSerInfo = null; c = null;
                FSTUtil.<RuntimeException>rethrow(th);
            }
        } else {
            Object res = instantiateSpecialTag(referencee, readPos, code);
            return res;
        }
        try {
            FSTObjectSerializer ser = clzSerInfo.getSer();
            if (ser != null) {
                Object res = instantiateAndReadWithSer(c, ser, clzSerInfo, referencee, readPos);
                getCodec().readArrayEnd(clzSerInfo);
                return res;
            } else {
                Object res = instantiateAndReadNoSer(c, clzSerInfo, referencee, readPos);
                return res;
            }
        } catch (Exception e) {
            FSTUtil.<RuntimeException>rethrow(e);
        }
        return null;
    }

    protected Object instantiateSpecialTag(FSTClazzInfo.FSTFieldInfo referencee, int readPos, byte code) throws Exception {
        if ( code == FSTObjectOutput.STRING ) { // faster than switch, note: currently string tag not used by all codecs ..
            String res = getCodec().readStringUTF();
            objects.registerObjectForRead(res, readPos);
            return res;
        } else if ( code == FSTObjectOutput.BIG_INT ) {
            return instantiateBigInt();
        } else if ( code == FSTObjectOutput.NULL ) {
            return null;
        } else
        {
            switch (code) {
//                case FSTObjectOutput.BIG_INT: { return instantiateBigInt(); }
                case FSTObjectOutput.BIG_LONG: { return Long.valueOf(getCodec().readFLong()); }
                case FSTObjectOutput.BIG_BOOLEAN_FALSE: { return Boolean.FALSE; }
                case FSTObjectOutput.BIG_BOOLEAN_TRUE: { return Boolean.TRUE; }
                case FSTObjectOutput.ONE_OF: { return referencee.getOneOf()[getCodec().readFByte()]; }
//                case FSTObjectOutput.NULL: { return null; }
                case FSTObjectOutput.DIRECT_ARRAY_OBJECT: {
                    Object directObject = getCodec().getDirectObject();
                    objects.registerObjectForRead(directObject,readPos);
                    return directObject;
                }
                case FSTObjectOutput.DIRECT_OBJECT: {
                    Object directObject = getCodec().getDirectObject();
                    if (directObject.getClass() == byte[].class) { // fixme. special for minibin, move it there
                        if ( referencee != null && referencee.getType() == boolean[].class )
                        {
                            byte[] ba = (byte[]) directObject;
                            boolean res[] = new boolean[ba.length];
                            for (int i = 0; i < res.length; i++) {
                                res[i] = ba[i] != 0;
                            }
                            directObject = res;
                        }
                    }
                    objects.registerObjectForRead(directObject,readPos);
                    return directObject;
                }
//                case FSTObjectOutput.STRING: return getCodec().readStringUTF();
                case FSTObjectOutput.HANDLE: {
                    Object res = instantiateHandle(referencee);
                    getCodec().readObjectEnd();
                    return res;
                }
                case FSTObjectOutput.ARRAY: {
                    Object res = instantiateArray(referencee, readPos);
                    return res;
                }
                case FSTObjectOutput.ENUM: { return instantiateEnum(referencee, readPos); }
            }
            throw new RuntimeException("unknown object tag "+code);
        }
    }

    protected FSTClazzInfo getClazzInfo(Class c, FSTClazzInfo.FSTFieldInfo referencee) {
        FSTClazzInfo clzSerInfo;
        FSTClazzInfo lastInfo = referencee.lastInfo;
        if ( lastInfo != null && lastInfo.clazz == c && lastInfo.conf == conf) {
            clzSerInfo = lastInfo;
        } else {
            clzSerInfo = clInfoRegistry.getCLInfo(c, conf);
            referencee.lastInfo = clzSerInfo;
        }
        return clzSerInfo;
    }

    protected Object instantiateHandle(FSTClazzInfo.FSTFieldInfo referencee) throws IOException {
        int handle = getCodec().readFInt();
        Object res = objects.getReadRegisteredObject(handle);
        if (res == null) {
            throw new IOException("unable to ressolve handle " + handle + " " + referencee.getDesc() + " " + getCodec().getInputPos() );
        }
        return res;
    }

    protected Object instantiateArray(FSTClazzInfo.FSTFieldInfo referencee, int readPos) throws Exception {
        Object res = readArray(referencee, readPos); // NEED TO PASS ALONG THE POS FOR THE ARRAY

        /*
            registerObjectForRead alerady gets called by readArray (and with the proper pos now).  that said, I'm unclear
            on the intent of the if ( ! referencee.isFlat() ) so I wanted to comment on that

        if ( ! referencee.isFlat() ) {
            objects.registerObjectForRead(res, readPos);
        }
        */

        return res;
    }

    protected Object instantiateEnum(FSTClazzInfo.FSTFieldInfo referencee, int readPos) throws IOException, ClassNotFoundException {
        FSTClazzInfo clzSerInfo;
        Class c;
        clzSerInfo = readClass();
        c = clzSerInfo.getClazz();
        int ordinal = getCodec().readFInt();
        Object[] enumConstants = clzSerInfo.getEnumConstants();
        if ( enumConstants == null ) {
            // pseudo enum of anonymous classes tom style ?
            return null;
        }
        Object res = enumConstants[ordinal];
        if ( REGISTER_ENUMS_READ ) {
            if ( ! referencee.isFlat() ) { // should be unnecessary
                objects.registerObjectForRead(res, readPos);
            }
        }
        return res;
    }

    protected Object instantiateBigInt() throws IOException {
        int val = getCodec().readFInt();
        return Integer.valueOf(val);
    }

    protected Object instantiateAndReadWithSer(Class c, FSTObjectSerializer ser, FSTClazzInfo clzSerInfo, FSTClazzInfo.FSTFieldInfo referencee, int readPos) throws Exception {
        boolean serInstance = false;
        Object newObj = ser.instantiate(c, this, clzSerInfo, referencee, readPos);
        if (newObj == null) {
            newObj = clzSerInfo.newInstance(getCodec().isMapBased());
        } else
            serInstance = true;
        if (newObj == null) {
            throw new IOException(referencee.getDesc() + ":Failed to instantiate '" + c.getName() + "'. Register a custom serializer implementing instantiate or define empty constructor..");
        }
        if ( newObj == FSTObjectSerializer.REALLY_NULL ) {
            newObj = null;
        } else {
            if (newObj.getClass() != c && ser == null ) {
                // for advanced trickery (e.g. returning non-serializable from FSTSerializer)
                // this hurts. so in case of FSTSerializers incoming clzInfo will refer to the
                // original class, not the one actually instantiated
                c = newObj.getClass();
                clzSerInfo = clInfoRegistry.getCLInfo(c, conf);
            }
            if ( ! referencee.isFlat() && ! clzSerInfo.isFlat() && !ser.alwaysCopy()) {
                objects.registerObjectForRead(newObj, readPos);
            }
            if ( !serInstance )
                ser.readObject(this, newObj, clzSerInfo, referencee);
        }
        getCodec().consumeEndMarker(); //=> bug when writing objects unlimited
        return newObj;
    }

    protected Object instantiateAndReadNoSer(Class c, FSTClazzInfo clzSerInfo, FSTClazzInfo.FSTFieldInfo referencee, int readPos) throws Exception {
        Object newObj;
        newObj = clzSerInfo.newInstance(getCodec().isMapBased());
        if (newObj == null) {
            throw new IOException(referencee.getDesc() + ":Failed to instantiate '" + c.getName() + "'. Register a custom serializer implementing instantiate or define empty constructor.");
        }
        //fixme: code below improves unshared decoding perf, however disables to run mixed mode (clients can decide)
        //actually would need 2 flags for encode/decode
        //tested with json mixed mode does not work anyway ...
        final boolean needsRefLookup = conf.shareReferences && !referencee.isFlat() && !clzSerInfo.isFlat();
        // previously :
//        final boolean needsRefLookup = !referencee.isFlat() && !clzSerInfo.isFlat();
        if (needsRefLookup) {
            objects.registerObjectForRead(newObj, readPos);
        }
        if ( clzSerInfo.isExternalizable() )
        {
            int tmp = readPos;
            getCodec().ensureReadAhead(readExternalReadAHead);
            ((Externalizable)newObj).readExternal(this);
            getCodec().readExternalEnd();
            if ( clzSerInfo.getReadResolveMethod() != null ) {
                final Object prevNew = newObj;
                newObj = handleReadRessolve(clzSerInfo, newObj);
                if ( newObj != prevNew && needsRefLookup ) {
                    objects.replace(prevNew, newObj, tmp);
                }
            }
        } else if (clzSerInfo.useCompatibleMode())
        {
            Object replaced = readObjectCompatible(referencee, clzSerInfo, newObj);
            if (replaced != null && replaced != newObj) {
                objects.replace(newObj, replaced, readPos);
                newObj = replaced;
            }
        } else {
            FSTClazzInfo.FSTFieldInfo[] fieldInfo = clzSerInfo.getFieldInfo();
            readObjectFields(referencee, clzSerInfo, fieldInfo, newObj,0,0);
        }
        return newObj;
    }

    protected Object readObjectCompatible(FSTClazzInfo.FSTFieldInfo referencee, FSTClazzInfo serializationInfo, Object newObj) throws Exception {
        Class cl = serializationInfo.getClazz();
        readObjectCompatibleRecursive(referencee, newObj, serializationInfo, cl);
        if (newObj != null &&
                serializationInfo.getReadResolveMethod() != null) {
            newObj = handleReadRessolve(serializationInfo, newObj);
        }
        return newObj;
    }

    protected Object handleReadRessolve(FSTClazzInfo serializationInfo, Object newObj) throws IllegalAccessException {
        Object rep = null;
        try {
            rep = serializationInfo.getReadResolveMethod().invoke(newObj);
        } catch (InvocationTargetException e) {
            FSTUtil.<RuntimeException>rethrow(e);
        }
        newObj = rep;//FIXME: support this in call
        return newObj;
    }

    protected void readObjectCompatibleRecursive(FSTClazzInfo.FSTFieldInfo referencee, Object toRead, FSTClazzInfo serializationInfo, Class cl) throws Exception {
        FSTClazzInfo.FSTCompatibilityInfo fstCompatibilityInfo = serializationInfo.getCompInfo().get(cl);
        if (!Serializable.class.isAssignableFrom(cl)) {
            return; // ok here, as compatible mode will never be triggered for "forceSerializable"
        }
        readObjectCompatibleRecursive(referencee, toRead, serializationInfo, cl.getSuperclass());
        if (fstCompatibilityInfo != null && fstCompatibilityInfo.getReadMethod() != null) {
            try {
                int tag = readByte(); // expect 55
                if ( tag == 66 ) {
                    // no write method defined, but read method defined ...
                    // expect defaultReadObject
                    getCodec().moveTo(getCodec().getInputPos() - 1); // need to push back tag, cause defaultWriteObject on writer side does not write tag
//                    input.pos--;
                }
                ObjectInputStream objectInputStream = getObjectInputStream(cl, serializationInfo, referencee, toRead);
                fstCompatibilityInfo.getReadMethod().invoke(toRead, objectInputStream);
                fakeWrapper.pop();
            } catch (Exception e) {
                FSTUtil.<RuntimeException>rethrow(e);
            }
        } else {
            if (fstCompatibilityInfo != null) {
                int tag = readByte();
                if ( tag == 55 )
                {
                    // came from writeMethod, but no readMethod defined => assume defaultWriteObject
                    tag = readByte(); // consume tag of defaultwriteobject (99)
                    if ( tag == 77 ) // came from putfield
                    {
                        HashMap<String, Object> fieldMap = (HashMap<String, Object>) FSTObjectInput.this.readObjectInternal(HashMap.class);
                        final FSTClazzInfo.FSTFieldInfo[] fieldArray = fstCompatibilityInfo.getFieldArray();
                        for (int i = 0; i < fieldArray.length; i++) {
                            FSTClazzInfo.FSTFieldInfo fstFieldInfo = fieldArray[i];
                            final Object val = fieldMap.get(fstFieldInfo.getName());
                            if ( val != null ) {
                                fstFieldInfo.setObjectValue(toRead,val);
                            }
                        }
                        return;
                    }
                }
                readObjectFields(referencee, serializationInfo, fstCompatibilityInfo.getFieldArray(), toRead,0,0);
            }
        }
    }

    public void defaultReadObject(FSTClazzInfo.FSTFieldInfo referencee, FSTClazzInfo serializationInfo, Object newObj)
    {
        try {
            readObjectFields(referencee,serializationInfo,serializationInfo.getFieldInfo(),newObj,0,-1); // -1 flag to indicate no object end should be called
        } catch (Exception e) {
            FSTUtil.<RuntimeException>rethrow(e);
        }
    }

    protected void readObjectFields(FSTClazzInfo.FSTFieldInfo referencee, FSTClazzInfo serializationInfo, FSTClazzInfo.FSTFieldInfo[] fieldInfo, Object newObj, int startIndex, int version) throws Exception {

        if ( getCodec().isMapBased() ) {
            readFieldsMapBased(referencee, serializationInfo, newObj);
            if ( version >= 0 /*&& newObj instanceof Unknown == false*/ ) {
                getCodec().readObjectEnd();
            }
            return;
        }
        if ( version < 0 )
            version = 0;
        int booleanMask = 0;
        int boolcount = 8;
        final int length = fieldInfo.length;
        int conditional = 0;
        for (int i = startIndex; i < length; i++) {
            try {
                FSTClazzInfo.FSTFieldInfo subInfo = fieldInfo[i];
                if (subInfo.getVersion() > version ) {
                    int nextVersion = getCodec().readVersionTag();
                    if ( nextVersion == 0 ) // old object read
                    {
                        oldVersionRead(newObj);
                        return;
                    }
                    if ( nextVersion != subInfo.getVersion() ) {
                        throw new RuntimeException("read version tag "+nextVersion+" fieldInfo has "+subInfo.getVersion());
                    }
                    readObjectFields(referencee,serializationInfo,fieldInfo,newObj,i,nextVersion);
                    return;
                }
                if (subInfo.isPrimitive()) {
                    int integralType = subInfo.getIntegralType();
                    if (integralType == FSTClazzInfo.FSTFieldInfo.BOOL) {
                        if (boolcount == 8) {
                            booleanMask = ((int) getCodec().readFByte() + 256) &0xff;
                            boolcount = 0;
                        }
                        boolean val = (booleanMask & 128) != 0;
                        booleanMask = booleanMask << 1;
                        boolcount++;
                        subInfo.setBooleanValue(newObj, val);
                    } else {
                        switch (integralType) {
                            case FSTClazzInfo.FSTFieldInfo.BYTE:   subInfo.setByteValue(newObj, getCodec().readFByte()); break;
                            case FSTClazzInfo.FSTFieldInfo.CHAR:   subInfo.setCharValue(newObj, getCodec().readFChar()); break;
                            case FSTClazzInfo.FSTFieldInfo.SHORT:  subInfo.setShortValue(newObj, getCodec().readFShort()); break;
                            case FSTClazzInfo.FSTFieldInfo.INT:    subInfo.setIntValue(newObj, getCodec().readFInt()); break;
                            case FSTClazzInfo.FSTFieldInfo.LONG:   subInfo.setLongValue(newObj, getCodec().readFLong()); break;
                            case FSTClazzInfo.FSTFieldInfo.FLOAT:  subInfo.setFloatValue(newObj, getCodec().readFFloat()); break;
                            case FSTClazzInfo.FSTFieldInfo.DOUBLE: subInfo.setDoubleValue(newObj, getCodec().readFDouble()); break;
                        }
                    }
                } else {
                    if ( subInfo.isConditional() ) {
                        if ( conditional == 0 ) {
                            conditional = getCodec().readPlainInt();
                            if ( skipConditional(newObj, conditional, subInfo) ) {
                                getCodec().moveTo(conditional);
                                continue;
                            }
                        }
                    }
                    // object
                    Object subObject = readObjectWithHeader(subInfo);
                    subInfo.setObjectValue(newObj, subObject);
                }
            } catch (IllegalAccessException ex) {
                throw new IOException(ex);
            }
        }
        int debug = getCodec().readVersionTag();// just consume '0'
    }

    public VersionConflictListener getVersionConflictListener() {
        return versionConflictListener;
    }

    /**
     * see @Version annotation
     * @param versionConflictListener
     */
    public void setVersionConflictListener(VersionConflictListener versionConflictListener) {
        this.versionConflictListener = versionConflictListener;
    }

    protected void oldVersionRead(Object newObj) {
        if ( versionConflictListener != null )
            versionConflictListener.onOldVersionRead(newObj);
    }

    protected void readFieldsMapBased(FSTClazzInfo.FSTFieldInfo referencee, FSTClazzInfo serializationInfo, Object newObj) throws Exception {
        String name;
        int len = getCodec().getObjectHeaderLen(); // check if len is known in advance
        if ( len < 0 )
            len = Integer.MAX_VALUE;
        int count = 0;
        boolean isUnknown = newObj.getClass() == Unknown.class; // json
        boolean inArray = isUnknown && getCodec().inArray();    // json externalized/custom serialized
        getCodec().startFieldReading(newObj);
        // fixme: break up this loop into separate impls.
        while( count < len ) {
            if ( inArray ) {
                // unknwon json object written by externalize or custom serializer
                Object o = readObjectWithHeader(null);
                if ( o != null && getCodec().isEndMarker(o.toString()) )
                    return;
                ((Unknown)newObj).add(o);
                continue;
            }
            name= getCodec().readStringUTF();
            //int debug = getCodec().getInputPos();
            if ( len == Integer.MAX_VALUE && getCodec().isEndMarker(name) )
                return;
            count++;
            if (isUnknown) {
                FSTClazzInfo.FSTFieldInfo fakeField = new FSTClazzInfo.FSTFieldInfo(null, null, true);
                fakeField.fakeName = name;
                Object toSet = readObjectWithHeader(fakeField);
                ((Unknown)newObj).set(name, toSet);
            } else
            if ( newObj.getClass() == MBObject.class ) {
                Object toSet = readObjectWithHeader(null);
                ((MBObject)newObj).put(name,toSet);
            } else {
                FSTClazzInfo.FSTFieldInfo fieldInfo = serializationInfo.getFieldInfo(name, null);
                if (fieldInfo == null) {
                    logger.log(FSTLogger.Level.WARN, "warning: unknown field: " + name + " on class " + serializationInfo.getClazz().getName(), null);
                } else {
                    if (fieldInfo.isPrimitive()) {
                        // direct primitive field
                        switch (fieldInfo.getIntegralType()) {
                            case FSTClazzInfo.FSTFieldInfo.BOOL:
                                fieldInfo.setBooleanValue(newObj, getCodec().readFByte() == 0 ? false : true);
                                break;
                            case FSTClazzInfo.FSTFieldInfo.BYTE:
                                fieldInfo.setByteValue(newObj, getCodec().readFByte());
                                break;
                            case FSTClazzInfo.FSTFieldInfo.CHAR:
                                fieldInfo.setCharValue(newObj, getCodec().readFChar());
                                break;
                            case FSTClazzInfo.FSTFieldInfo.SHORT:
                                fieldInfo.setShortValue(newObj, getCodec().readFShort());
                                break;
                            case FSTClazzInfo.FSTFieldInfo.INT:
                                fieldInfo.setIntValue(newObj, getCodec().readFInt());
                                break;
                            case FSTClazzInfo.FSTFieldInfo.LONG:
                                fieldInfo.setLongValue(newObj, getCodec().readFLong());
                                break;
                            case FSTClazzInfo.FSTFieldInfo.FLOAT:
                                fieldInfo.setFloatValue(newObj, getCodec().readFFloat());
                                break;
                            case FSTClazzInfo.FSTFieldInfo.DOUBLE:
                                fieldInfo.setDoubleValue(newObj, getCodec().readFDouble());
                                break;
                            default:
                                throw new RuntimeException("unkown primitive type " + fieldInfo);
                        }
                    } else {
                        Object toSet = readObjectWithHeader(fieldInfo);
                        toSet = getCodec().coerceElement(fieldInfo.getType(), toSet);
                        fieldInfo.setObjectValue(newObj, toSet);
                    }
                }
            }
        }
        getCodec().endFieldReading(newObj);
    }

    protected boolean skipConditional(Object newObj, int conditional, FSTClazzInfo.FSTFieldInfo subInfo) {
        if ( conditionalCallback != null ) {
            return conditionalCallback.shouldSkip(newObj,conditional,subInfo.getField());
        }
        return false;
    }

    protected void readCompatibleObjectFields(FSTClazzInfo.FSTFieldInfo referencee, FSTClazzInfo serializationInfo, FSTClazzInfo.FSTFieldInfo[] fieldInfo, Map res) throws Exception {
        int booleanMask = 0;
        int boolcount = 8;
        for (int i = 0; i < fieldInfo.length; i++) {
            try {
                FSTClazzInfo.FSTFieldInfo subInfo = fieldInfo[i];
                if (subInfo.isIntegral() && !subInfo.isArray()) {
                    final Class subInfoType = subInfo.getType();
                    if (subInfoType == boolean.class) {
                        if (boolcount == 8) {
                            booleanMask = ((int) getCodec().readFByte() + 256) &0xff;
                            boolcount = 0;
                        }
                        boolean val = (booleanMask & 128) != 0;
                        booleanMask = booleanMask << 1;
                        boolcount++;
                        res.put(subInfo.getName(), val);
                    }
                    if (subInfoType == byte.class) {
                        res.put(subInfo.getName(), getCodec().readFByte());
                    } else if (subInfoType == char.class) {
                        res.put(subInfo.getName(), getCodec().readFChar());
                    } else if (subInfoType == short.class) {
                        res.put(subInfo.getName(), getCodec().readFShort());
                    } else if (subInfoType == int.class) {
                        res.put(subInfo.getName(), getCodec().readFInt());
                    } else if (subInfoType == double.class) {
                        res.put(subInfo.getName(), getCodec().readFDouble());
                    } else if (subInfoType == float.class) {
                        res.put(subInfo.getName(), getCodec().readFFloat());
                    } else if (subInfoType == long.class) {
                        res.put(subInfo.getName(), getCodec().readFLong());
                    }
                } else {
                    // object
                    Object subObject = readObjectWithHeader(subInfo);
                    res.put(subInfo.getName(), subObject);
                }
            } catch (IllegalAccessException ex) {
                throw new IOException(ex);
            }
        }
    }

    public String readStringUTF() throws IOException {
        return getCodec().readStringUTF();
    }

    /**
     * len < 127 !!!!!
     * @return
     * @throws IOException
     */
    public String readStringAsc() throws IOException {
        return getCodec().readStringAsc();
    }

    protected Object readArray(FSTClazzInfo.FSTFieldInfo referencee, int pos) throws Exception {
        Object classOrArray = getCodec().readArrayHeader();
        if (pos < 0)
            pos = getCodec().getInputPos();
        if ( classOrArray instanceof Class == false )
            return classOrArray;
        if ( classOrArray == null )
            return null;
        Object o = readArrayNoHeader(referencee, pos, (Class) classOrArray);
        getCodec().readArrayEnd(null);
        return o;
    }

    protected Object readArrayNoHeader(FSTClazzInfo.FSTFieldInfo referencee, int pos, Class arrCl) throws Exception {
        final int len = getCodec().readFInt();
        if (len == -1) {
            return null;
        }
        Class arrType = arrCl.getComponentType();
        if (!arrType.isArray()) {
            Object array = Array.newInstance(arrType, len);
            if ( ! referencee.isFlat() )
                objects.registerObjectForRead(array, pos );
            if (arrType.isPrimitive()) {
                return getCodec().readFPrimitiveArray(array, arrType, len);
            } else { // Object Array
                Object arr[] = (Object[]) array;
                for (int i = 0; i < len; i++) {
                    Object value = readObjectWithHeader(referencee);
                    value = getCodec().coerceElement(arrType, value);
                    arr[i] = value;
                }
                getCodec().readObjectEnd();
            }
            return array;
        } else { // multidim array
            Object array[] = (Object[]) Array.newInstance(arrType, len);
            if ( ! referencee.isFlat() ) {
                objects.registerObjectForRead(array, pos);
            }
            FSTClazzInfo.FSTFieldInfo ref1 = new FSTClazzInfo.FSTFieldInfo(referencee.getPossibleClasses(), null, clInfoRegistry.isIgnoreAnnotations());
            for (int i = 0; i < len; i++) {
                Object subArray = readArray(ref1, -1);
                array[i] = subArray;
            }
            return array;
        }
    }

    public void registerObject(Object o, int streamPosition, FSTClazzInfo info, FSTClazzInfo.FSTFieldInfo referencee) {
        if ( ! objects.disabled && !referencee.isFlat() && (info == null || ! info.isFlat() ) ) {
            objects.registerObjectForRead(o, streamPosition);
        }
    }

    public FSTClazzInfo readClass() throws IOException, ClassNotFoundException {
        return getCodec().readClass();
    }

    protected void resetAndClearRefs() {
        try {
            reset();
            objects.clearForRead(conf);
        } catch (IOException e) {
            FSTUtil.<RuntimeException>rethrow(e);
        }
    }

    public void reset() throws IOException {
        getCodec().reset();
    }

    public void resetForReuse(InputStream in) throws IOException {
        if ( closed ) {
            throw new RuntimeException("can't reuse closed stream");
        }
        getCodec().reset();
        getCodec().setInputStream(in);
        objects.clearForRead(conf);
        callbacks = null; //fix memory leak on reuse from default FstConfiguration
    }

    public void resetForReuseCopyArray(byte bytes[], int off, int len) throws IOException {
        if ( closed ) {
            throw new RuntimeException("can't reuse closed stream");
        }
        getCodec().reset();
        objects.clearForRead(conf);
        getCodec().resetToCopyOf(bytes, off, len);
        callbacks = null; //fix memory leak on reuse from default FstConfiguration
    }

    public void resetForReuseUseArray(byte bytes[]) throws IOException {
        resetForReuseUseArray(bytes, bytes.length);
    }

    public void resetForReuseUseArray(byte bytes[], int len) throws IOException {
        if ( closed ) {
            throw new RuntimeException("can't reuse closed stream");
        }
        objects.clearForRead(conf);
        getCodec().resetWith(bytes, len);
        callbacks = null; //fix memory leak on reuse from default FstConfiguration
    }

    public final int readFInt() throws IOException {
        return getCodec().readFInt();
    }

    protected boolean closed = false;
    @Override
    public void close() throws IOException {
        closed = true;
        resetAndClearRefs();
        conf.returnObject(objects);
        getCodec().close();
    }

    ////////////////////////////////////////////////////// epic compatibility hack /////////////////////////////////////////////////////////

    protected MyObjectStream fakeWrapper; // some jdk classes hash for ObjectStream, so provide the same instance always

    protected ObjectInputStream getObjectInputStream(final Class cl, final FSTClazzInfo clInfo, final FSTClazzInfo.FSTFieldInfo referencee, final Object toRead) throws IOException {
        ObjectInputStream wrapped = new ObjectInputStream() {
            @Override
            public Object readObjectOverride() throws IOException, ClassNotFoundException {
                try {
                    byte b = FSTObjectInput.this.readByte();
                    if ( b != FSTObjectOutput.SPECIAL_COMPATIBILITY_OBJECT_TAG ) {
                        Constructor<?>[] constructors = OptionalDataException.class.getDeclaredConstructors();
                        FSTObjectInput.this.pushBack(1);
                        for (int i = 0; i < constructors.length; i++) {
                            Constructor constructor = constructors[i];
                            Class[] typeParameters = constructor.getParameterTypes();
                            if ( typeParameters != null && typeParameters.length == 1 && typeParameters[0] == int.class) {
                                constructor.setAccessible(true);
                                OptionalDataException ode;
                                try {
                                    ode = (OptionalDataException) constructor.newInstance(0);
                                    throw ode;
                                } catch (InvocationTargetException e) {
                                    break;
                                }
                            }
                        }
                        throw new EOFException("if your code relies on this, think");
                    }
                    return FSTObjectInput.this.readObjectInternal(referencee.getPossibleClasses());
                } catch (IllegalAccessException e) {
                    throw new IOException(e);
                } catch (InstantiationException e) {
                    throw new IOException(e);
                }
            }

            @Override
            public Object readUnshared() throws IOException, ClassNotFoundException {
                try {
                    return FSTObjectInput.this.readObjectInternal(referencee.getPossibleClasses()); // fixme
                } catch (IllegalAccessException e) {
                    throw new IOException(e);
                } catch (InstantiationException e) {
                    throw new IOException(e);
                }
            }

            @Override
            public void defaultReadObject() throws IOException, ClassNotFoundException {
                try {
                    int tag = readByte();
                    if ( tag == 77 ) // came from writeFields
                    {
                        fieldMap = (HashMap<String, Object>) FSTObjectInput.this.readObjectInternal(HashMap.class);
                        // object has been written with writeFields, is no read with defaultReadObjects,
                        // need to autoapply map to object vars.
                        // this might be redundant in case readObject() pulls a getFields() .. (see bitset testcase)
                        for (Iterator<String> iterator = fieldMap.keySet().iterator(); iterator.hasNext(); ) {
                            String key = iterator.next();
                            FSTClazzInfo.FSTFieldInfo fieldInfo = clInfo.getFieldInfo(key, null);// in case fieldName is not unique => cannot recover/fix
                            if ( fieldInfo != null ) {
                                fieldInfo.setObjectValue(toRead,fieldMap.get(key));
                            }
                        }
                    } else {
                        FSTObjectInput.this.readObjectFields(
                                referencee,
                                clInfo,
                                clInfo.getCompInfo().get(cl).getFieldArray(),
                                toRead,
                                0,
                                0
                        ); // FIXME: only fields of current class
                    }
                } catch (Exception e) {
                    throw new IOException(e);
                }
            }

            HashMap<String, Object> fieldMap;

            @Override
            public GetField readFields() throws IOException, ClassNotFoundException {
                int tag = readByte();
                try {
                    FSTClazzInfo.FSTCompatibilityInfo fstCompatibilityInfo = clInfo.getCompInfo().get(cl);
                    if (tag==99) { // came from defaultwriteobject
                        // Note: in case number and names of instance fields of reader/writer are different,
                        // this fails as code below implicitely assumes, fields of writer == fields of reader
                        // unfortunately one can use defaultWriteObject at writer side but use getFields at reader side
                        // in readObject(). if then fields differ, code below reads BS and fails.
                        // Its impossible to fix that except by always using putField + getField for
                        // JDK compatibility classes, however this will waste lots of performance. As
                        // it would be necessary to *always* write full metainformation (a map of fieldName => value pairs)
                        // see #53
                        fieldMap = new HashMap<String, Object>();
                        FSTObjectInput.this.readCompatibleObjectFields(referencee, clInfo, fstCompatibilityInfo.getFieldArray(), fieldMap);
                        getCodec().readVersionTag(); // consume dummy version tag as created by defaultWriteObject
                    } else if (tag == 66) { // has been written from writeObjectCompatible without writeMethod
                        fieldMap = new HashMap<String, Object>();
                        FSTObjectInput.this.readCompatibleObjectFields(referencee, clInfo, fstCompatibilityInfo.getFieldArray(), fieldMap);
                        getCodec().readVersionTag(); // consume dummy version tag as created by defaultWriteObject
                    } else {
                        fieldMap = (HashMap<String, Object>) FSTObjectInput.this.readObjectInternal(HashMap.class);
                    }
                } catch (Exception e) {
                    FSTUtil.<RuntimeException>rethrow(e);
                }
                return new GetField() {
                    @Override
                    public ObjectStreamClass getObjectStreamClass() {
                        return ObjectStreamClass.lookup(cl);
                    }

                    @Override
                    public boolean defaulted(String name) throws IOException {
                        return fieldMap.get(name) == null;
                    }

                    @Override
                    public boolean get(String name, boolean val) throws IOException {
                        if (fieldMap.get(name) == null) {
                            return val;
                        }
                        return ((Boolean) fieldMap.get(name)).booleanValue();
                    }

                    @Override
                    public byte get(String name, byte val) throws IOException {
                        if (fieldMap.get(name) == null) {
                            return val;
                        }
                        return ((Byte) fieldMap.get(name)).byteValue();
                    }

                    @Override
                    public char get(String name, char val) throws IOException {
                        if (fieldMap.get(name) == null) {
                            return val;
                        }
                        return ((Character) fieldMap.get(name)).charValue();
                    }

                    @Override
                    public short get(String name, short val) throws IOException {
                        if (fieldMap.get(name) == null) {
                            return val;
                        }
                        return ((Short) fieldMap.get(name)).shortValue();
                    }

                    @Override
                    public int get(String name, int val) throws IOException {
                        if (fieldMap.get(name) == null) {
                            return val;
                        }
                        return ((Integer) fieldMap.get(name)).intValue();
                    }

                    @Override
                    public long get(String name, long val) throws IOException {
                        if (fieldMap.get(name) == null) {
                            return val;
                        }
                        return ((Long) fieldMap.get(name)).longValue();
                    }

                    @Override
                    public float get(String name, float val) throws IOException {
                        if (fieldMap.get(name) == null) {
                            return val;
                        }
                        return ((Float) fieldMap.get(name)).floatValue();
                    }

                    @Override
                    public double get(String name, double val) throws IOException {
                        if (fieldMap.get(name) == null) {
                            return val;
                        }
                        return ((Double) fieldMap.get(name)).doubleValue();
                    }

                    @Override
                    public Object get(String name, Object val) throws IOException {
                        Object res = fieldMap.get(name);
                        if (res == null) {
                            return val;
                        }
                        return res;
                    }
                };
            }

            @Override
            public void registerValidation(ObjectInputValidation obj, int prio) throws NotActiveException, InvalidObjectException {
                if (callbacks == null) {
                    callbacks = new ArrayList<CallbackEntry>();
                }
                callbacks.add(new CallbackEntry(obj, prio));
            }

            @Override
            public int read() throws IOException {
                return getCodec().readFByte();
            }

            @Override
            public int read(byte[] buf, int off, int len) throws IOException {
                return FSTObjectInput.this.read(buf, off, len);
            }

            @Override
            public int available() throws IOException {
                return FSTObjectInput.this.available();
            }

            @Override
            public void close() throws IOException {
            }

            @Override
            public boolean readBoolean() throws IOException {
                return FSTObjectInput.this.readBoolean();
            }

            @Override
            public byte readByte() throws IOException {
                return getCodec().readFByte();
            }

            @Override
            public int readUnsignedByte() throws IOException {
                return FSTObjectInput.this.readUnsignedByte();
            }

            @Override
            public char readChar() throws IOException {
                return getCodec().readFChar();
            }

            @Override
            public short readShort() throws IOException {
                return getCodec().readFShort();
            }

            @Override
            public int readUnsignedShort() throws IOException {
                return FSTObjectInput.this.readUnsignedShort();
            }

            @Override
            public int readInt() throws IOException {
                return getCodec().readFInt();
            }

            @Override
            public long readLong() throws IOException {
                return getCodec().readFLong();
            }

            @Override
            public float readFloat() throws IOException {
                return getCodec().readFFloat();
            }

            @Override
            public double readDouble() throws IOException {
                return getCodec().readFDouble();
            }

            @Override
            public void readFully(byte[] buf) throws IOException {
                FSTObjectInput.this.readFully(buf);
            }

            @Override
            public void readFully(byte[] buf, int off, int len) throws IOException {
                FSTObjectInput.this.readFully(buf, off, len);
            }

            @Override
            public int skipBytes(int len) throws IOException {
                return FSTObjectInput.this.skipBytes(len);
            }

            @Override
            public String readUTF() throws IOException {
                return getCodec().readStringUTF();
            }

            @Override
            public String readLine() throws IOException {
                return FSTObjectInput.this.readLine();
            }

            @Override
            public int read(byte[] b) throws IOException {
                return FSTObjectInput.this.read(b);
            }

            @Override
            public long skip(long n) throws IOException {
                return FSTObjectInput.this.skip(n);
            }

            @Override
            public void mark(int readlimit) {
                throw new RuntimeException("not implemented");
            }

            @Override
            public void reset() throws IOException {
                FSTObjectInput.this.reset();
            }

            @Override
            public boolean markSupported() {
                return false;
            }
        };
        if ( fakeWrapper == null ) {
            fakeWrapper = new MyObjectStream();
        }
        fakeWrapper.push(wrapped);
        return fakeWrapper;
    }

    protected void pushBack(int i) {
        getCodec().pushBack(i);
    }

    protected static class MyObjectStream extends ObjectInputStream {

        ObjectInputStream wrapped;
        ArrayDeque<ObjectInputStream> wrappedStack = new ArrayDeque<ObjectInputStream>();

        public void push( ObjectInputStream in ) {
            wrappedStack.push(in);
            wrapped = in;
        }

        public void pop() {
            wrapped = wrappedStack.pop();
        }

        MyObjectStream() throws IOException, SecurityException {
        }

        @Override
        public Object readObjectOverride() throws IOException, ClassNotFoundException {
            return wrapped.readObject();
        }

        @Override
        public Object readUnshared() throws IOException, ClassNotFoundException {
            return wrapped.readUnshared();
        }

        @Override
        public void defaultReadObject() throws IOException, ClassNotFoundException {
            wrapped.defaultReadObject();
        }

        @Override
        public ObjectInputStream.GetField readFields() throws IOException, ClassNotFoundException {
            return wrapped.readFields();
        }

        @Override
        public void registerValidation(ObjectInputValidation obj, int prio) throws NotActiveException, InvalidObjectException {
            wrapped.registerValidation(obj,prio);
        }

        @Override
        public int read() throws IOException {
            return wrapped.read();
        }

        @Override
        public int read(byte[] buf, int off, int len) throws IOException {
            return wrapped.read(buf, off, len);
        }

        @Override
        public int available() throws IOException {
            return wrapped.available();
        }

        @Override
        public void close() throws IOException {
            wrapped.close();
        }

        @Override
        public boolean readBoolean() throws IOException {
            return wrapped.readBoolean();
        }

        @Override
        public byte readByte() throws IOException {
            return wrapped.readByte();
        }

        @Override
        public int readUnsignedByte() throws IOException {
            return wrapped.readUnsignedByte();
        }

        @Override
        public char readChar() throws IOException {
            return wrapped.readChar();
        }

        @Override
        public short readShort() throws IOException {
            return wrapped.readShort();
        }

        @Override
        public int readUnsignedShort() throws IOException {
            return wrapped.readUnsignedShort();
        }

        @Override
        public int readInt() throws IOException {
            return wrapped.readInt();
        }

        @Override
        public long readLong() throws IOException {
            return wrapped.readLong();
        }

        @Override
        public float readFloat() throws IOException {
            return wrapped.readFloat();
        }

        @Override
        public double readDouble() throws IOException {
            return wrapped.readDouble();
        }

        @Override
        public void readFully(byte[] buf) throws IOException {
            wrapped.readFully(buf);
        }

        @Override
        public void readFully(byte[] buf, int off, int len) throws IOException {
            wrapped.readFully(buf, off, len);
        }

        @Override
        public int skipBytes(int len) throws IOException {
            return wrapped.skipBytes(len);
        }

        @Override
        public String readUTF() throws IOException {
            return wrapped.readUTF();
        }

        @Override
        public String readLine() throws IOException {
            return wrapped.readLine();
        }

        @Override
        public int read(byte[] b) throws IOException {
            return wrapped.read(b);
        }

        @Override
        public long skip(long n) throws IOException {
            return wrapped.skip(n);
        }

        @Override
        public void mark(int readlimit) {
            wrapped.mark(readlimit);
        }

        @Override
        public void reset() throws IOException {
            wrapped.reset();
        }

        @Override
        public boolean markSupported() {
            return wrapped.markSupported();
        }
    }

}


/*
 * Copyright 2014 Ruediger Moeller.
 *
 * Licensed 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.nustaq.offheap.structs.structtypes;

import org.nustaq.offheap.structs.FSTStruct;
import org.nustaq.offheap.structs.NoAssist;

/**
 * this class can be used to represent strings in structs. the content is rewritable, but cannot grow.
 * max length is determined once you create the struct object.
 */
public class StructString extends FSTStruct implements Comparable {

    protected int len = 0;
    protected char chars[];

    public StructString(int size) {
        chars = new char[size];
        len = 0;
    }

    public StructString(String init, int size) {
        chars = new char[size];
        len = 0;
        setString(init);
    }

    public StructString(String s) {
        chars = s.toCharArray();
        len = s.length();
    }

    /**
     * modify content of this StructString. The length of the new String must not exceed
     * the length of internal char array
     * @param s
     */
    public void setString(String s) {
        if ( s == null ) {
            setLen(0);
            return;
        }
        if ( s.length() > charsLen() ) {
            throw new RuntimeException("String length exceeds buffer size. String len "+s.length()+" charsLen:"+charsLen());
        }
        for (int i=0; i < s.length(); i++) {
            chars(i,s.charAt(i));
        }
        len = s.length();
    }

    public int getLen() {
        return len;
    }

    public void setLen(int len) {
        this.len = len;
    }

    public void chars(int i, char val) {
        chars[i] = val;
    }

    public char chars(int i) {
        return chars[i];
    }

    public int compareTo(StructString str) {
        int l1 = len;
        int l2 = str.getLen();
        int max = Math.min(l1, l2);
        int i = 0;
        while (i < max) {
            char c1 = chars(i);
            char c2 = str.chars(i);
            if (c1 != c2) {
                return c1 - c2;
            }
            i++;
        }
        return l1 - l2;
    }

    public int compareToString(String str) {
        int l1 = len;
        int l2 = str.length();
        int max = Math.min(l1, l2);
        int i = 0;
        while (i < max) {
            char c1 = chars(i);
            char c2 = str.charAt(i);
            if (c1 != c2) {
                return c1 - c2;
            }
            i++;
        }
        return l1 - l2;
    }

    public int charsLen() {
        return chars.length;
    }

    public String toString() {
        // fixme: optimize this by direct copy
        char ch[] = new char[len];
        for ( int i=0; i < len; i++ ) {
            ch[i] = chars(i);
        }
        return new String(ch);
    }

    @Override
    public int hashCode() {
        int l = len;
        int h = 0;
        if (l > 0) {
            for (int i = 0; i < len; i++) {
                h = 31 * h + chars(i);
            }
        }
        return h;
    }

    @NoAssist
    public boolean equals( Object o ) {
        if ( o instanceof StructString ) {
            StructString ss = (StructString) o;
            if ( ss.getLen() != getLen() ) {
                return false;
            }
            for ( int i = 0; i < ss.getLen(); i++ ) {
                if ( ss.chars(i) != chars(i) ) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    @Override @NoAssist
    public int compareTo(Object o) {
        if ( o instanceof StructString) {
            return compareTo((StructString)o);
        }
        return -1;
    }

    @Override @NoAssist
    public Object getFieldValues() {
        return toString();
    }
}


/*
 * Copyright 2014 Ruediger Moeller.
 *
 * Licensed 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.nustaq.serialization.coders;

import org.nustaq.serialization.*;
import org.nustaq.serialization.minbin.MBOut;
import org.nustaq.serialization.minbin.MinBin;

import java.io.*;
import java.lang.reflect.Array;

/**
 * Date: 30.03.2014
 * Time: 18:47
 *
 * serializes into self describing binary format MinBin
 */
public class FSTMinBinEncoder implements FSTEncoder {

    MBOut out = new MBOut();
    OutputStream outputStream;
    FSTConfiguration conf;
    private int offset = 0;

    public FSTMinBinEncoder(FSTConfiguration conf) {
        this.conf = conf;
    }

    @Override
    public void writeRawBytes(byte[] bufferedName, int off, int length) throws IOException {
        out.writeArray(bufferedName, off, length);
    }

    @Override
    public void setConf(FSTConfiguration conf) {
        this.conf = conf;
    }

    /**
     * does not write class tag and length
     *
     * @param array
     * @param start
     * @param length @throws java.io.IOException
     */
    @Override
    public void writePrimitiveArray(Object array, int start, int length) throws IOException {
        out.writeArray(array, start, length);
    }

    @Override
    public void writeStringUTF(String str) throws IOException {
        out.writeTag(str);
    }

    @Override
    public void writeFShort(short c) throws IOException {
        out.writeInt(MinBin.INT_16,c);
    }

    @Override
    public void writeFChar(char c) throws IOException {
        out.writeInt(MinBin.CHAR, c);
    }

    @Override
    public void writeFByte(int v) throws IOException {
        out.writeInt(MinBin.INT_8, v);
    }

    @Override
    public void writeFInt(int anInt) throws IOException {
        out.writeInt(MinBin.INT_32,anInt);
    }

    @Override
    public void writeFLong(long anInt) throws IOException {
        out.writeInt(MinBin.INT_64, anInt);
    }

    @Override
    public void writeFFloat(float value) throws IOException {
        out.writeTag(value);
    }

    @Override
    public void writeFDouble(double value) throws IOException {
        out.writeTag(value);
    }

    @Override
    public int getWritten() {
        return out.getWritten()+offset;
    }

    @Override
    public void skip(int i) {
        throw new RuntimeException("not supported");
    }

    /**
     * close and flush to underlying stream if present. The stream is also closed
     *
     * @throws java.io.IOException
     */
    @Override
    public void close() throws IOException {
        if ( outputStream != null ) {

            outputStream.close();
        }
    }

    @Override
    public void reset(byte[] bytez) {
        offset = 0;
        if (bytez!=null)
            out.reset(bytez);
        else
            out.reset();
    }

    /**
     * resets stream (positions are lost)
     *
     * @throws java.io.IOException
     */
    @Override
    public void flush() throws IOException {
        if ( outputStream != null ) {
            outputStream.write(out.getBytez(),0,out.getWritten());
            offset = out.getWritten();
            out.resetPosition();
        }
    }

    /**
     * used to write uncompressed int (guaranteed length = 4) at a (eventually recent) position
     *
     * @param position
     * @param v
     */
    @Override
    public void writeInt32At(int position, int v) {
        throw new RuntimeException("not supported");
    }

    /**
     * if output stream is null, just encode into a byte array
     *
     * @param outstream
     */
    @Override
    public void setOutstream(OutputStream outstream) {
        this.outputStream = outstream;
    }

    @Override
    public void ensureFree(int bytes) throws IOException {

    }

    @Override
    public byte[] getBuffer() {
        return out.getBytez();
    }

    @Override
    public void registerClass(Class possible) {

    }

    @Override
    public void writeClass(Class cl) {
       // already written in write tag
    }

    @Override
    public void writeClass(FSTClazzInfo clInf) {
        // already written in write tag
    }

    @Override
    public boolean writeAttributeName(FSTClazzInfo.FSTFieldInfo subInfo, Object value) {
        byte[] bufferedName = (byte[]) subInfo.getBufferedName();
        if ( bufferedName != null ) {
            out.writeRaw(bufferedName,0,bufferedName.length);
        } else {
            // fully cache metadata
            int pos = out.getWritten();

            out.writeTag(subInfo.getName());

            int len = out.getWritten() - pos;
            bufferedName = new byte[len];
            System.arraycopy(out.getBytez(),pos,bufferedName,0,len);
            subInfo.setBufferedName(bufferedName);
        }
        return false;
    }

    @Override
    public boolean writeTag(byte tag, Object infoOrObject, long somValue, Object toWrite, FSTObjectOutput oout) throws IOException {
        switch (tag) {
            case FSTObjectOutput.HANDLE:
                out.writeTagHeader(MinBin.HANDLE);
                out.writeInt(MinBin.INT_32,somValue);
                return true;
            case FSTObjectOutput.NULL:
                out.writeTag(null);
                return true;
            case FSTObjectOutput.TYPED:
            case FSTObjectOutput.OBJECT:
                FSTClazzInfo clzInfo = (FSTClazzInfo) infoOrObject;
                if (clzInfo.useCompatibleMode() && clzInfo.getSer() == null ) {
                    throw new RuntimeException("Unsupported backward compatibility mode for class '"+clzInfo.getClazz().getName()+"'. Pls register a Custom Serializer to fix");
                }
                if (clzInfo.getClazz() == String.class )
                    break;
                if (clzInfo.getClazz() == Double.class )
                    break;
                if (clzInfo.getClazz() == Float.class )
                    break;
                if (clzInfo.getClazz() == Byte.class )
                    break;
                if (clzInfo.getClazz() == Short.class )
                    break;
                if (clzInfo.getClazz() == Integer.class )
                    break;
                if (clzInfo.getClazz() == Long.class )
                    break;
                if (clzInfo.getClazz() == Character.class )
                    break;
                if (clzInfo.getClazz() == Boolean.class )
                    break;
//                if ( clzInfo.getClazz() == Byte.class || clzInfo.getClazz() == Short.class || clzInfo.getClazz() == Character.class ) {
//                    out.writeObject(toWrite);
//                    return true;
//                }
                if ( clzInfo.getSer()!=null || clzInfo.isExternalizable() ) {
                    out.writeTagHeader(MinBin.SEQUENCE);
                    writeSymbolicClazz(clzInfo.getClazz());
                    out.writeIntPacked(-1); // END Marker required
                } else
                {
                    out.writeTagHeader(MinBin.OBJECT);
                    writeSymbolicClazz(clzInfo.getClazz());
                    out.writeIntPacked(clzInfo.getFieldInfo().length);
                }
                break;
            case FSTObjectOutput.ONE_OF:
                throw new RuntimeException("not implemented");
            case FSTObjectOutput.STRING:
                break; // ignore, header created by calling writeUTF
            case FSTObjectOutput.BIG_BOOLEAN_FALSE:
                out.writeTag(Boolean.FALSE);
                break; // ignore, header created by writing long. FIXME: won't work
            case FSTObjectOutput.BIG_BOOLEAN_TRUE:
                out.writeTag(Boolean.TRUE);
                break; // ignore, header created by writing long. FIXME: won't work
            case FSTObjectOutput.BIG_LONG:
                break; // ignore, header implicitely created by writing long.
            case FSTObjectOutput.BIG_INT:
                break;// ignore, header implicitely created by writing int.
            case FSTObjectOutput.ARRAY:
                Class<?> clz = infoOrObject.getClass();
                Class<?> componentType = clz.getComponentType();
                if ( clz.isArray() && componentType.isPrimitive() )
                {
                    if ( componentType == double.class ) {
                        out.writeTagHeader(MinBin.SEQUENCE);
                        writeSymbolicClazz(clz);
                        int length = Array.getLength(infoOrObject);
                        out.writeIntPacked(length);
                        for ( int i = 0; i < length; i++ ) {
                            out.writeTag(Array.getDouble(infoOrObject,i));
                        }
                    } else if ( componentType == float.class ) {
                        out.writeTagHeader(MinBin.SEQUENCE);
                        writeSymbolicClazz(clz);
                        int length = Array.getLength(infoOrObject);
                        out.writeIntPacked(length);
                        for ( int i = 0; i < length; i++ ) {
                            out.writeTag(Array.getFloat(infoOrObject, i));
                        }
                    } else {
                        out.writeArray(infoOrObject, 0, Array.getLength(infoOrObject));
                    }
                    return true;
                } else {
                    out.writeTagHeader(MinBin.SEQUENCE);
                    writeSymbolicClazz(clz);
                }
                break;
            case FSTObjectOutput.ENUM:
                out.writeTagHeader(MinBin.SEQUENCE);
                boolean isEnumClass = toWrite.getClass().isEnum();
                Class c = toWrite.getClass();
                if (!isEnumClass) {
                    // weird stuff ..
                    while (c != null && !c.isEnum()) {
                        c = toWrite.getClass().getSuperclass();
                    }
                    if (c == null) {
                        throw new RuntimeException("Can't handle this enum: " + toWrite.getClass());
                    }
                    writeSymbolicClazz(c);
                } else {
                    writeSymbolicClazz(c);
                }
                out.writeIntPacked(1);
                out.writeObject(toWrite.toString());
                return true;
            default:
                throw new RuntimeException("unexpected tag "+tag);
        }
        return false;
    }

    private void writeSymbolicClazz(Class<?> clz) {
        byte b[] = conf.getCrossPlatformBinaryCache(clz.getName());
        if ( b == null ) {
            int pos = out.getWritten();

            out.writeTag(classToString(clz));

            int len = out.getWritten() - pos;
            b = new byte[len];
            System.arraycopy(out.getBytez(), pos, b, 0, len);
            conf.registerCrossPlatformClassBinaryCache(clz.getName(), b);
        } else {
            out.writeRaw(b,0,b.length);
        }
        return;
    }

    protected String classToString(Class clz) {
        return conf.getCPNameForClass(clz);
    }

    @Override
    public void externalEnd(FSTClazzInfo clz) {
        if ( clz == null ||
             clz.isExternalizable() ||
             ( clz.getSer() instanceof FSTCrossPlatformSerialzer && ((FSTCrossPlatformSerialzer) clz.getSer()).writeTupleEnd())
           ) {
            out.writeTag(MinBin.END_MARKER);
        }
    }

    @Override
    public boolean isWritingAttributes() {
        return true;
    }

    public boolean isPrimitiveArray(Object array, Class<?> componentType) {
        return componentType.isPrimitive() && array instanceof double[] == false && array instanceof float[] == false;
    }

    public boolean isTagMultiDimSubArrays() {
        return true;
    }

    @Override
    public void writeVersionTag(int version) throws IOException {
        // versioning not supported for minbin
    }

    @Override
    public boolean isByteArrayBased() {
        return true;
    }

    @Override
    public void writeArrayEnd() {

    }

    @Override
    public void writeFieldsEnd(FSTClazzInfo serializationInfo) {

    }

    @Override
    public FSTConfiguration getConf() {
        return conf;
    }

}


/*
 * Copyright 2014 Ruediger Moeller.
 *
 * Licensed 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.nustaq.offheap.structs;

import org.nustaq.offheap.bytez.BasicBytez;
import org.nustaq.offheap.bytez.ByteSource;
import org.nustaq.offheap.bytez.Bytez;
import org.nustaq.offheap.bytez.malloc.MallocBytez;
import org.nustaq.offheap.bytez.onheap.HeapBytez;
import org.nustaq.offheap.structs.unsafeimpl.FSTStructFactory;
import org.nustaq.serialization.util.FSTUtil;

import java.io.*;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;

/**
 * Date: 01.07.13
 * Time: 20:20
 * *
 * Base class of all structs. Inherit this to define your own structs.
 * Refer to the documentation in the wiki regarding limitations of struct classes/members
 */
public class FSTStruct implements Serializable {

    transient public long ___offset;
    transient public Bytez ___bytes;
    transient public FSTStructFactory ___fac;
    transient public int ___elementSize;
    transient public FSTStructChange tracker;

    /**
     * must include bytearray offset in case of unsafe use
     *
     * @param off
     */
    protected void setAbsoluteOffset(long off) {
        ___offset = off;
    }

    /**
     * @return internal offset, use getIndexInBase to get the start index of the struct within the base byte array
     */
    protected long getAbsoluteOffset() {
        return ___offset;
    }

    /**
     * @return the start index of this struct within the underlying byte array. getByteSize returns len.
     * In case of offheap, the address of the first valid byte is returned
     */
    public long getOffset() {
        return (___offset);
    }

    /**
     * @deprecated use getOffset
     * see getOffset
     */
    public long getIndexInBase() {
        return (___offset);
    }

    /**
     * @return the number of bytes used by the struct pointed to
     */
    public int getByteSize() {
        if (!isOffHeap()) {
            return 0;
        }
        return ___bytes.getInt(___offset);
    }

    public Class getPointedClass() {
        if (!isOffHeap())
            throw new RuntimeException("cannot call on heap");
        Class clazz = ___fac.getClazz(getClzId());
        if (clazz == null) {
            return FSTStruct.class;
        }
        return clazz;
    }

    public int getClzId() {
        if (!isOffHeap())
            throw new RuntimeException("cannot call on heap");
        return ___bytes.getInt(___offset + 4);
    }

    public boolean pointsToNull() {
        return getClzId() <= 0;
    }

    protected void addOffset(long off) {
        ___offset += off;
    }

    protected void setBase(Bytez base) {
        ___bytes = base;
    }

    /**
     * @return the underlying byte array.
     */
    public Bytez getBase() {
        return ___bytes;
    }

    public FSTStructFactory getFac() {
        return ___fac;
    }

    /**
     * set this struct pointer to base array at given offset (=bufoff+index)
     *
     * @param base
     * @param offset direct offset to byte array element (=FSTStruct.bufoff+array index)
     */
    public void baseOn(byte base[], long offset, FSTStructFactory fac) {
        ___bytes = new HeapBytez(base); ___offset = offset; ___fac = fac;
    }

    public void baseOn(Bytez base, long offset, FSTStructFactory fac) {
        ___bytes = base; ___offset = offset; ___fac = fac;
    }

    /**
     * set this struct pointer to base array at given index
     *
     * @param base
     * @param index
     */
    public void baseOn(byte base[], int index) {
        ___bytes = new HeapBytez(base); ___offset = index;
        if (___fac == null)
            ___fac = FSTStructFactory.getInstance();
    }

    public void baseOn(Bytez base, int index) {
        ___bytes = base; ___offset = index;
        if (___fac == null)
            ___fac = FSTStructFactory.getInstance();
    }

    public boolean isIdenticTo(FSTStruct other) {
        return other.getBase().equals(___bytes) && other.getAbsoluteOffset() == ___offset;
    }

    public int hashCode() {
        if (!isOffHeap())
            return onHeapHashcode();
        return ___bytes.getInt(___offset) ^ ___bytes.getInt(___offset + getByteSize() - 4);
    }

    public int onHeapHashcode() {
        return super.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (obj instanceof FSTStruct) {
            FSTStruct other = (FSTStruct) obj;
            final int len = getByteSize();
            if (other.getByteSize() == len) {
                long ix = getOffset();
                long ox = other.getOffset();
                for (int i = 0; i < len; i++) {
                    if (___bytes.get(i + ix) != other.___bytes.get(i + ox))
                        return false;
                }
                return true;
            } else
                return false;
        }
        return super.equals(obj);
    }

    public boolean isOffHeap() {
        return ___fac != null;
    }

    public int getElementInArraySize() {
        return ___elementSize;
    }

    public boolean isStructArrayPointer() {
        return ___elementSize > 0;
    }

    public boolean isNull() {
        return getClzId() <= 0;
    }

    /**
     * important: iterators, array access and access to substructures of a struct always return
     * pointers, which are cached per thread (one instance only!). To keep a pointer to a struct (e.g. search struct array and find an
     * element which should be kept as result) you need to detach it (removed from cache).
     * This method allocates a pointer.
     */
    public <T extends FSTStruct> T detach() {
        if (isOffHeap()) {
            ___fac.detach(this);
        }
        return (T) this;
    }

    /**
     * move offsets and memorybase to given pointer and return the pointer.
     * Eases dereferencing of nested structs without object creation.
     * If the given pointer is null, a new one will be created (alloc)
     * <pre>
     *   MyStruct st = FSTStructFactory.getInstance().createEmptyStructPointer(FSTStruct.class);
     *   otherStruct.getEmbeddedMyStruct().to(st);
     * </pre>
     */
    public <T extends FSTStruct> T detachTo(T pointer) {
        if ( pointer == null ) {
            return detach();
        }
        if (isOffHeap()) {
            pointer.___fac = ___fac;
            pointer.___bytes = ___bytes;
            pointer.___elementSize = ___elementSize;
            pointer.___offset = ___offset;
            return pointer;
        }
        return (T) this;
    }

    /**
     * Warning: no bounds checking. Moving the pointer outside the underlying Bytez will cause access violations
     */
    public final void next() {
        if (___elementSize > 0)
            ___offset += ___elementSize;
        else
            throw new RuntimeException("not pointing to a struct array");
    }

    /**
     * Warning: no bounds checking. Moving the pointer outside the underlying Bytez will cause access violations
     */
    public final void next(int offset) {
        ___offset += offset;
    }

    /**
     * Warning: no bounds checking. Moving the pointer outside the underlying Bytez will cause access violations
     */
    public final void previous() {
        if (___elementSize > 0)
            ___offset -= ___elementSize;
        else
            throw new RuntimeException("not pointing to a struct array");
    }

    public <T extends FSTStruct> T cast(Class<T> to) {
        int clzId = ___fac.getClzId(to);
        if (this.getClass().getSuperclass() == to)
            return (T) this;
        FSTStruct res = ___fac.createStructPointer(___bytes, (int) (___offset), clzId);
        res.___elementSize = ___elementSize;
        return (T) res;
    }

    /**
     * @return a volatile pointer of the exact type this points to
     */
    public <T extends FSTStruct> T cast() {
        int clzId = getClzId();
        if (___fac.getClazz(clzId) == getClass().getSuperclass())
            return (T) this;
        FSTStruct res = ___fac.getStructPointerByOffset(___bytes, ___offset);
        res.___elementSize = ___elementSize;
        return (T) res;
    }

    public byte getByte() {
        return ___bytes.get(___offset);
    }

    public void setByte(byte i) {
        ___bytes.put(___offset, i);
    }

    public char getChar() {
        return ___bytes.getChar(___offset);
    }

    public short getShort() {
        return ___bytes.getShort(___offset);
    }

    public void setShort(short i) {
        ___bytes.putShort(___offset, i);
    }

    public int getInt() {
        return ___bytes.getInt(___offset);
    }

    public void setInt(int i) {
        ___bytes.putInt(___offset, i);
    }

    public long getLong() {
        return ___bytes.getLong(___offset);
    }

    public void setLong(long i) {
        ___bytes.putLong(___offset, i);
    }

    public float getFloat() {
        return ___bytes.getFloat(___offset);
    }

    public double getDouble() {
        return ___bytes.getDouble(___offset);
    }

    public void getBytes(byte[] target, int startIndexInTarget, int len) {
        if (!isOffHeap()) {
            throw new RuntimeException("must be offheap to call this");
        }
        ___bytes.getArr(___offset, target, startIndexInTarget, len);
    }

    public void getBytes(Bytez target, int startIndexInTarget) {
        if (!isOffHeap()) {
            throw new RuntimeException("must be offheap to call this");
        }
        if (target.length() < startIndexInTarget + getByteSize()) {
            throw new RuntimeException("ArrayIndexOutofBounds byte len:" + target.length() + " start+size:" + (startIndexInTarget + getByteSize()));
        }
//        unsafe.copyMemory(___bytes,___offset, target, bufoff+startIndexInTarget, getByteSize());
        ___bytes.copyTo(target, startIndexInTarget, ___offset, getByteSize());
    }

    public void setBytes(byte[] source, int sourceIndex, int len) {
        if (!isOffHeap()) {
            throw new RuntimeException("must be offheap to call this");
        }
        ___bytes.set(___offset, source, sourceIndex, len);
    }

    public void setBytes(ByteSource source, long sourceIndex, int len) {
        if (!isOffHeap()) {
            throw new RuntimeException("must be offheap to call this");
        }
        if (source instanceof BasicBytez) {
            ((BasicBytez) source).copyTo(___bytes, ___offset, sourceIndex, len);
        } else {
            for (long i = 0; i < len; i++)
                ___bytes.put(___offset + i, source.get(i + sourceIndex));
        }
    }

    public void setBytes(Bytez source, long sourceIndex, int len) {
        if (!isOffHeap()) {
            throw new RuntimeException("must be offheap to call this");
        }
//        unsafe.copyMemory(source, bufoff+sourceIndex, ___bytes, ___offset, len);
        source.copyTo(___bytes, ___offset, sourceIndex, len);
    }

    /**
     * returns a complete copy of this object allocating a new Bytez capable of holding the data.
     *
     * @return
     */
    public FSTStruct createCopy() {
        if (!isOffHeap()) {
            throw new RuntimeException("must be offheap to call this");
        }
        byte b[] = new byte[getByteSize()];
        HeapBytez res = new HeapBytez(b);
        getBytes(res, 0);
        return ___fac.createStructWrapper(res, 0);
    }

    /**
     * works only if change tracking is enabled
     */
    public FSTStruct startChangeTracking() {
        tracker = new FSTStructChange();
        return this;
    }

    /**
     * works only if change tracking is enabled
     */
    public FSTStructChange finishChangeTracking() {
        tracker.snapshotChanges((int) getOffset(), getBase());
        FSTStructChange res = tracker;
        tracker = null;
        return res;
    }

    public boolean isChangeTracking() {
        return tracker != null;
    }

    public FSTStruct toOffHeap() {
        if (isOffHeap())
            return this;
        return FSTStructFactory.getInstance().toStruct(this);
    }

    //////////////////////////////////////////////
    // helper to support stringy web protocols

    public Object getFieldValues() {
        throw new RuntimeException("only supported for structs");
    }

    //////////////////////////////////////////////
    // support for copyFree ByteBuffer conversion

    static Field address = null;
    static Field capacity = null;

    static ThreadLocal<ByteBuffer> tmpBuf = new ThreadLocal<ByteBuffer>() {

        @Override
        protected ByteBuffer initialValue() {
            List<Field> fields = new ArrayList<>();
            ByteBuffer tmpSend = ByteBuffer.allocateDirect(0);
            FSTUtil.getAllFields( fields, tmpSend.getClass() );
            if ( address == null || capacity == null ) {
                for (int i = 0; i < fields.size(); i++) {
                    Field field = fields.get(i);
                    if ( field.getName().equals("address") ) {
                        address = field;
                    } else if ( field.getName().equals("capacity") ) {
                        capacity = field;
                    }
                }
                address.setAccessible(true);
                capacity.setAccessible(true);
            }
            return tmpSend;
        }
    };

    /**
     * @return a temporary per thread instance of ByteBuffer pointing to this structs data. The length of the
     * buffer is same as the length of this struct.
     *
     * The instance returned is valid until the currentThread calls this method next time.
     *
     * Can be used to feed e.g. NIO api. Use ByteBuffer.duplicate() to obtain a non-temporary instance
     *
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public ByteBuffer asByteBufferTemporary() {
        if ( getBase() instanceof MallocBytez ) {
            MallocBytez base = (MallocBytez) getBase();
            ByteBuffer bb = tmpBuf.get();
            try {
                address.setLong(bb, base.getBaseAdress() + getOffset());
                capacity.setInt( bb, getByteSize());
            } catch (IllegalAccessException e) {
                FSTUtil.<RuntimeException>rethrow(e);
            }
            bb.limit((int) (getOffset() + getByteSize()));
            bb.position((int) getOffset());
            return tmpBuf.get();
        } else { // assume HeapBytez. Allocates
            return ByteBuffer.wrap(getBase().asByteArray(), (int) getOffset(), getByteSize());
        }
    }

}


/*
 * Copyright 2014 Ruediger Moeller.
 *
 * Licensed 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.nustaq.offheap.structs;

import org.nustaq.offheap.bytez.Bytez;
import org.nustaq.offheap.bytez.BytezAllocator;
import org.nustaq.offheap.bytez.malloc.MallocBytezAllocator;
import org.nustaq.offheap.bytez.onheap.HeapBytezAllocator;
import org.nustaq.offheap.structs.structtypes.StructArray;
import org.nustaq.offheap.structs.unsafeimpl.FSTStructFactory;
import org.nustaq.serialization.util.FSTUtil;

/**
 * Date: 04.10.13
 * Time: 20:51
 * implemenst struct instances. By default heap is used to obtain underlying byte arrays.
 * By supplementing a BytezAllocator, structs can also be created offheap.
 */
public class FSTStructAllocator {
    public static boolean DUMP_ALLOC = false;
    protected int chunkSize;
    protected Bytez chunk;
    protected int chunkIndex;
    BytezAllocator alloc = new HeapBytezAllocator();
    int chunkObjCount = 0;
//    BytezAllocator alloc = new MallocBytezAllocator();

    protected FSTStructAllocator() {

    }

    /**
     * @param b
     * @param index
     * @return a new allocated pointer matching struct type stored in b[]
     */
    public static FSTStruct createStructPointer(Bytez b, int index) {
        return FSTStructFactory.getInstance().getStructPointerByOffset(b, index).detach();
    }

    /**
     * @param onHeapTemplate
     * @param <T>
     * @return return a byte array based struct instance for given on-heap template. Allocates a new byte[] with each call
     */
    public static <T extends FSTStruct> T toStruct(T onHeapTemplate) {
        return FSTStructFactory.getInstance().toStruct(onHeapTemplate);
    }

    /**
     * @param b
     * @param index
     * @return a pointer matching struct type stored in b[] from the thread local cache
     */
    public static FSTStruct getVolatileStructPointer(Bytez b, int index) {
        return (FSTStruct) FSTStructFactory.getInstance().getStructPointerByOffset(b, index);
    }

    /**
     * @param clazz
     * @param <C>
     * @return a newly allocated pointer matching. use baseOn to point it to a meaningful location
     */
    public static <C extends FSTStruct> C newPointer(Class<C> clazz) {
        try {
            return (C) allocInstance(clazz);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    protected static <C extends FSTStruct> Object allocInstance(Class<C> clazz) throws Exception {
        return FSTUtil.getUnsafe().allocateInstance(FSTStructFactory.getInstance().getProxyClass(clazz));
//        return FSTStructFactory.getInstance().getProxyClass(clazz).newInstance();
    }

    /**
     * Create a Heap-Byte Array Structallocator with given chunk size in bytes. If allocated structs are larger than the given size, a new bytearray is
     * created for the allocation.
     * @param chunkSizeBytes
     */
    public FSTStructAllocator(int chunkSizeBytes) {
        this.chunkSize = chunkSizeBytes;
    }

    /**
     * optionally uses another allocator (e.g. for Real Off Heap with MallocBytez)
     *
     * Warning: Currently there is not automatic free for OffHeap Bytez, so only use for
     * statically allocated structures. You can call free() to release all offheap structures
     * allocated by this instance, however you'll get crashes if there are still references
     * from y<our application to the free'd memory.
     * A good approach is to keep only large static structures OffHeap (refdata, buffers) so there is no need for freeing.
     * Also IO related single big blobs apply for Off Heap.
     *
     * This does not apply for HeapBytez, which are handled by GC as usual. Here a chunk is freed, if there is no Struct
     * Object in the Heap referencing the unerlying byte array. If you use large chunk sizes, this can be a problem
     * because a single stuct can prevent a large chunk from being freed. So in general for On-Heap use as small
     * as possible chunks, off heap use large chunk sizes and free manually.
     *
     * @param chunkSizeBytes - set to a reasonable size, memory is allocated in thes chunk sizes. If very small, each
     *                       struct alloc will result in its individual chunk being allocated.
     * @param allocator
     */
    public FSTStructAllocator(int chunkSizeBytes, BytezAllocator allocator ) {
        this.chunkSize = chunkSizeBytes;
        alloc = allocator;
    }

    /**
     * create a new struct array of same type as template
     * @param size
     * @return
     */
    public <X extends FSTStruct> StructArray<X> newArray(int size, X templ) {
        return newArray(size,templ,alloc);
    }

    /**
     * create a new struct array of same type as template
     * @param size
     * @return
     */
    public <X extends FSTStruct> StructArray<X> newArray(int size, X templ, BytezAllocator alloc) {
        StructArray<X> aTemplate = new StructArray<X>(size, templ);
        int siz = getFactory().calcStructSize(aTemplate);
        try {
            if ( siz < chunkSize )
                return newStruct(aTemplate);
            else {
                return getFactory().toStruct(aTemplate,alloc);
            }
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * frees associated offheap memory, in case making objects created INVALID (Access violation!).
     * is a noop for on heap byte array allocator
     */
    public void free() {
        alloc.freeAll();
    }

    /**
     * allocate a Struct instance from an arbitrary template instance.
     *
     * @param aTemplate
     * @param <S>
     * @return
     */
    public <S extends FSTStruct> S newStruct(S aTemplate) {
        return newStruct(aTemplate,alloc);
    }

    public <S extends FSTStruct> S newStruct(S aTemplate, BytezAllocator alloc) {
        aTemplate = getFactory().toStruct(aTemplate);
        if (aTemplate.getByteSize()>=chunkSize)
            return (S)aTemplate.createCopy();
        int byteSize = aTemplate.getByteSize();
        synchronized (this) {
            if (chunk == null || chunkIndex+byteSize >= chunk.length()) {
                chunk = alloc.alloc(chunkSize);
                if ( DUMP_ALLOC )
                System.out.println("[Allocator] sum allocated "+MallocBytezAllocator.alloced.get()/1024/1024+" MB");
                chunkIndex = 0;
                chunkObjCount = 0;
            }
//            FSTStruct.unsafe.copyMemory(aTemplate.___bytes, aTemplate.___offset, chunk, FSTStruct.bufoff + chunkIndex, byteSize);
            aTemplate.___bytes.copyTo(chunk, chunkIndex, aTemplate.___offset, byteSize);
            S res = (S) getFactory().createStructWrapper(chunk, chunkIndex );
            chunkIndex+=byteSize;
            chunkObjCount++;
            return res;
        }
    }

    protected FSTStructFactory getFactory() {
        return FSTStructFactory.getInstance();
    }

}