/*
 * 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 com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.core.SerializableString;
import com.fasterxml.jackson.core.io.IOContext;
import com.fasterxml.jackson.core.json.UTF8JsonGenerator;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import org.nustaq.offheap.bytez.onheap.HeapBytez;
import org.nustaq.offheap.structs.FSTStruct;
import org.nustaq.serialization.coders.*;
import org.nustaq.serialization.util.DefaultFSTInt2ObjectMapFactory;
import org.nustaq.serialization.util.FSTInputStream;
import org.nustaq.serialization.util.FSTInt2ObjectMapFactory;
import org.nustaq.serialization.util.FSTUtil;
import org.nustaq.serialization.serializers.*;
import org.objenesis.Objenesis;
import org.objenesis.ObjenesisStd;

import java.io.*;
import java.lang.ref.SoftReference;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Created with IntelliJ IDEA.
 * User: ruedi
 * Date: 18.11.12
 * Time: 20:41
 *
 * Holds a serialization configuration/metadata.
 * Reuse this class !!! construction is very expensive. (just keep static instances around or use thread locals)
 *
 */
public class FSTConfiguration {

    static enum ConfType {
        DEFAULT, UNSAFE, MINBIN, JSON, JSONPRETTY
    }

    /**
     * if all attempts fail to find a class this guy is asked.
     * Can be used in case e.g. dynamic classes need get generated.
     */
    public interface LastResortClassResolver {
        public Class getClass( String clName );
    }

    /**
     * Security: disallow packages/classes upon deserialization
     */
    public interface ClassSecurityVerifier {
        /**
         * return false if your application does not allow to deserialize objects of type
         * cl. This can be implemented using whitelisting/blacklisting whole packages, subpackages, single classes
         *
         * Note: this also disallows serialization of forbidden classes. For assymetric use cases register a custom
         * serializer in order to prevent reading/writing of certain classes.
         *
         * @param cl - the class being serialized/deserialized
         * @return
         */
        boolean allowClassDeserialization( Class cl );
    }

    StreamCoderFactory streamCoderFactory = new FSTDefaultStreamCoderFactory(this);

    String name;

    ClassSecurityVerifier verifier;
    ConfType type = ConfType.DEFAULT;
    FSTClazzInfoRegistry serializationInfoRegistry = new FSTClazzInfoRegistry();
    HashMap<Class,List<SoftReference>> cachedObjects = new HashMap<Class, List<SoftReference>>(97);
    FSTInt2ObjectMapFactory intToObjectMapFactory = new DefaultFSTInt2ObjectMapFactory();
    FSTClazzNameRegistry classRegistry = new FSTClazzNameRegistry(null);
    boolean preferSpeed = false; // hint to prefer speed over size in case, currently ignored.
    boolean shareReferences = true;
    volatile ClassLoader classLoader = getClass().getClassLoader();
    boolean forceSerializable = false; // serialize objects which are not instanceof serializable using default serialization scheme.
    FSTClassInstantiator instantiator = new FSTDefaultClassInstantiator();

    Object coderSpecific;
    LastResortClassResolver lastResortResolver;

    boolean forceClzInit = false; // always execute default fields init, even if no transients

    FSTJsonFieldNames jsonFieldNames = new FSTJsonFieldNames("typ", "obj", "styp", "seq", "enum", "val", "ref");

    public FSTJsonFieldNames getJsonFieldNames() {
        return jsonFieldNames;
    }
    public void setJsonFieldNames(final FSTJsonFieldNames fieldNames) {
        this.jsonFieldNames = fieldNames;
    }

    public ClassSecurityVerifier getVerifier() {
        return verifier;
    }

    public FSTConfiguration setVerifier(ClassSecurityVerifier verifier) {
        this.verifier = verifier;
        return this;
    }

    public FSTInt2ObjectMapFactory getIntToObjectMapFactory() {
        return intToObjectMapFactory;
    }

    // cache fieldinfo. This can be shared with derived FSTConfigurations in order to reduce footprint
    static class FieldKey {
        Class clazz;
        String fieldName;

        public FieldKey(Class clazz, String fieldName) {
            this.clazz = clazz;
            this.fieldName = fieldName;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            FieldKey fieldKey = (FieldKey) o;

            if (!clazz.equals(fieldKey.clazz)) return false;
            return fieldName.equals(fieldKey.fieldName);

        }

        @Override
        public int hashCode() {
            int result = clazz.hashCode();
            result = 31 * result + fieldName.hashCode();
            return result;
        }
    }
    ConcurrentHashMap<FieldKey,FSTClazzInfo.FSTFieldInfo> fieldInfoCache;

    /**
     * debug helper
     * @return
     */
    public String getName() {
        return name;
    }

    /**
     * debug helper
     * @param name
     */
    public void setName(String name) {
        this.name = name;
    }

    /////////////////////////////////////
    // cross platform stuff only

    int cpAttrIdCount = 0;
    // contains symbol => full qualified name
    private HashMap<String, String> minbinNames = new HashMap<>();
    // may contain symbol => cached binary output
    private HashMap<String, byte[]> minBinNamesBytez = new HashMap<>();
    // contains full qualified name => symbol
    private HashMap<String, String> minbinNamesReverse = new HashMap<>();
    private boolean crossPlatform = false; // if true do not support writeObject/readObject etc.

    // non-final for testing
    public static boolean isAndroid = System.getProperty("java.runtime.name", "no").toLowerCase().contains("android");

    // end cross platform stuff only
    /////////////////////////////////////

    static AtomicBoolean conflock = new AtomicBoolean(false);
    static FSTConfiguration singleton;
    public static FSTConfiguration getDefaultConfiguration() {
        do { } while ( !conflock.compareAndSet(false, true) );
        try {
            if (singleton == null)
                singleton = createDefaultConfiguration();
            return singleton;
        } finally {
            conflock.set(false);
        }
    }

    /**
     * Warning: MinBin contains full metainformation (fieldnames,..), so its way slower than the other configs.
     * It should be used in case of cross language (e.g. java - javascript) serialization only.
     * Additionally you can read MinBin serialized streams without access to original classes.
     *
     * See MBPrinter on an example on how to read a MinBin stream without having access to
     * original classes. Useful for cross language serialization/long term archiving.
     *
     * Warning: MinBin serialization ('binary JSon') is much slower than the other
     * serialization configurations.
     *
     * @return a configuration to encode MinBin format.
     */
    public static FSTConfiguration createMinBinConfiguration() {
        return createMinBinConfiguration(null);
    }

    protected static FSTConfiguration createMinBinConfiguration(ConcurrentHashMap<FieldKey, FSTClazzInfo.FSTFieldInfo> shared) {
        final FSTConfiguration res = createDefaultConfiguration(shared);
        res.setCrossPlatform(true);
        res.type = ConfType.MINBIN;
        res.setStreamCoderFactory(new MinBinStreamCoderFactory(res));

        // override some serializers
        FSTSerializerRegistry reg = res.serializationInfoRegistry.getSerializerRegistry();
        reg.putSerializer(EnumSet.class, new FSTCPEnumSetSerializer(), true);
        reg.putSerializer(Throwable.class, new FSTCPThrowableSerializer(), true);

        // for crossplatform fallback does not work => register default serializers for collections and subclasses
        reg.putSerializer(AbstractCollection.class, new FSTCollectionSerializer(), true); // subclass should register manually
        reg.putSerializer(AbstractMap.class, new FSTMapSerializer(), true); // subclass should register manually

        res.registerCrossPlatformClassMapping(new String[][]{
                {"map", HashMap.class.getName()},
                {"list", ArrayList.class.getName()},
                {"set", HashSet.class.getName()},
                {"long", Long.class.getName()},
                {"integer", Integer.class.getName()},
                {"short", Short.class.getName()},
                {"byte", Byte.class.getName()},
                {"char", Character.class.getName()},
                {"float", Float.class.getName()},
                {"double", Double.class.getName()},
                {"date", Date.class.getName()},
                {"enumSet", "java.util.RegularEnumSet"},
                {"array", "[Ljava.lang.Object;"},
                {"String[]", "[Ljava.lang.String;"},
                {"Double[]", "[Ljava.lang.Double;"},
                {"Float[]", "[Ljava.lang.Float;"},
                {"double[]", "[D"},
                {"float[]", "[F"}
        });
        res.registerSerializer( BigDecimal.class, new FSTJSonSerializers.BigDecSerializer(), true );
        return res;
    }

    /**
     * @return a configuration encoding to JSon without support for reference sharing (=> NO cyclic object graphs)
     */
    public static FSTConfiguration createJsonNoRefConfiguration() {
        return createJsonConfiguration(false, false);
    }

    /**
     * create a json conf with given attributes. Note that shared refs = true for jason might be not as stable as for binary encodings
     * as fst relies on stream positions to identify objects within a given input, so any inbetween formatting will break proper reference
     * resolution.
     *
     * WARNING: use of sharedrefs = true is Deprecated as its flakey
     *
     * @param prettyPrint
     * @param shareReferences
     * @return
     */
    public static FSTConfiguration createJsonConfiguration(boolean prettyPrint, boolean shareReferences ) {
        if ( shareReferences && prettyPrint ) {
            throw new RuntimeException("unsupported flag combination");
        }
        return createJsonConfiguration(prettyPrint,shareReferences,null);
    }

    public static FSTConfiguration createJsonConfiguration() {
        return createJsonConfiguration(false,false);
    }

    protected static FSTConfiguration createJsonConfiguration(boolean prettyPrint, boolean shareReferences, ConcurrentHashMap<FieldKey,FSTClazzInfo.FSTFieldInfo> shared ) {
        if ( prettyPrint && shareReferences ) {
            throw new RuntimeException("cannot use prettyPrint with shared refs to 'true'. Set shareRefs to false.");
        }
        return constructJsonConf(prettyPrint, shareReferences, shared);
    }

    public static class JacksonAccessWorkaround extends UTF8JsonGenerator {
        public JacksonAccessWorkaround(IOContext ctxt, int features, ObjectCodec codec, OutputStream out) {
            super(ctxt, features, codec, out);
        }

        public JacksonAccessWorkaround(IOContext ctxt, int features, ObjectCodec codec, OutputStream out, byte[] outputBuffer, int outputOffset, boolean bufferRecyclable) {
            super(ctxt, features, codec, out, outputBuffer, outputOffset, bufferRecyclable);
        }

        public int getOutputTail() {
            return _outputTail;
        }
    }

    private static FSTConfiguration constructJsonConf(boolean prettyPrint, boolean shareReferences, ConcurrentHashMap<FieldKey, FSTClazzInfo.FSTFieldInfo> shared) {
        final FSTConfiguration conf = createMinBinConfiguration(shared);

        FSTSerializerRegistry reg = conf.serializationInfoRegistry.getSerializerRegistry();
        reg.putSerializer(FSTJSonUnmodifiableCollectionSerializer.UNMODIFIABLE_COLLECTION_CLASS, new FSTJSonUnmodifiableCollectionSerializer(), true);
        reg.putSerializer(FSTJSonUnmodifiableMapSerializer.UNMODIFIABLE_MAP_CLASS, new FSTJSonUnmodifiableMapSerializer(), false);

        conf.type = prettyPrint ? ConfType.JSONPRETTY : ConfType.JSON;
        JsonFactory fac;
        if ( prettyPrint ) {
            fac = new JsonFactory() {
                protected JsonGenerator _createUTF8Generator(OutputStream out, IOContext ctxt) throws IOException {
                    UTF8JsonGenerator gen = new JacksonAccessWorkaround(ctxt,
                            _generatorFeatures, _objectCodec, out);
                    if (_characterEscapes != null) {
                        gen.setCharacterEscapes(_characterEscapes);
                    }
                    SerializableString rootSep = _rootValueSeparator;
                    if (rootSep != DefaultPrettyPrinter.DEFAULT_ROOT_VALUE_SEPARATOR) {
                        gen.setRootValueSeparator(rootSep);
                    }
                    return gen;
                }
                @Override
                public JsonGenerator createGenerator(OutputStream out) throws IOException {
                    return super.createGenerator(out).setPrettyPrinter(new DefaultPrettyPrinter());
                }
            }
            .disable(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM)
            .disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
        } else {
            fac = new JsonFactory() {
                protected JsonGenerator _createUTF8Generator(OutputStream out, IOContext ctxt) throws IOException {
                    UTF8JsonGenerator gen = new JacksonAccessWorkaround(ctxt,
                            _generatorFeatures, _objectCodec, out);
                    if (_characterEscapes != null) {
                        gen.setCharacterEscapes(_characterEscapes);
                    }
                    SerializableString rootSep = _rootValueSeparator;
                    if (rootSep != DefaultPrettyPrinter.DEFAULT_ROOT_VALUE_SEPARATOR) {
                        gen.setRootValueSeparator(rootSep);
                    }
                    return gen;
                }
            };
            fac.disable(JsonGenerator.Feature.FLUSH_PASSED_TO_STREAM)
               .disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
        }
        conf.setCoderSpecific(fac);
        conf.setStreamCoderFactory(new JSonStreamCoderFactory(conf));
        conf.setShareReferences(shareReferences);
        conf.setLastResortResolver(new LastResortClassResolver() {
            @Override
            public Class getClass(String clName) {
                return Unknown.class;
            }
        });
        return conf;
    }

    /**
     * debug only, very slow (creates config with each call). Creates new conf so custom serializers are ignored.
     *
     * @param o
     */
    public static void prettyPrintJson(Object o) {
        FSTConfiguration conf = constructJsonConf(true, true, null);
        System.out.println(conf.asJsonString(o));
    }
    /**
     *
     * Configuration for use on Android. Its binary compatible with getDefaultConfiguration().
     * So one can write on server with getDefaultConf and read on mobile client with getAndroidConf().
     *
     * @return
     */
     public static FSTConfiguration createAndroidDefaultConfiguration() {
        return createAndroidDefaultConfiguration(null);
     }

     protected static FSTConfiguration createAndroidDefaultConfiguration(ConcurrentHashMap<FieldKey,FSTClazzInfo.FSTFieldInfo> shared) {
        final Objenesis genesis = new ObjenesisStd();
        FSTConfiguration conf = new FSTConfiguration(shared) {
            @Override
            public FSTClassInstantiator getInstantiator(Class clazz) {
                return new FSTObjenesisInstantiator(genesis,clazz);
            }
        };
        initDefaultFstConfigurationInternal(conf);
        if ( isAndroid ) {
            try {
                conf.registerSerializer(Class.forName("com.google.gson.internal.LinkedTreeMap"), new FSTMapSerializer(), true);
            } catch (ClassNotFoundException e) {
                //silent
            }
            try {
                conf.registerSerializer(Class.forName("com.google.gson.internal.LinkedHashTreeMap"), new FSTMapSerializer(), true);
            } catch (ClassNotFoundException e) {
                //silent
            }
        }
        return conf;
    }

    public static FSTConfiguration createConfiguration(ConfType ct, boolean shareRefs) {
        return createConfiguration(ct,shareRefs);
    }

    protected static FSTConfiguration createConfiguration(ConfType ct, boolean shareRefs,ConcurrentHashMap<FieldKey, FSTClazzInfo.FSTFieldInfo> shared ) {
        FSTConfiguration res;
        switch (ct) {
            case DEFAULT:
                res = createDefaultConfiguration(shared);
                break;
            case MINBIN:
                res = createMinBinConfiguration(shared);
                break;
            case UNSAFE:
                res = createUnsafeBinaryConfiguration(shared);
                break;
            case JSON:
                res = createJsonConfiguration( false, shareRefs, shared);
                break;
            case JSONPRETTY:
                res = createJsonConfiguration( true, shareRefs, shared);
                break;
            default:
                throw new RuntimeException("unsupported conftype for factory method");
        }
        res.setShareReferences(shareRefs);
        return res;
    }
    /**
     * the standard FSTConfiguration.
     * - safe (no unsafe r/w)
     * - platform independent byte order
     * - moderate compression
     *
     * note that if you are just read/write from/to byte arrays, its faster
     * to use DefaultCoder.
     *
     * This should be used most of the time.
     *
     * @return
     */
    public static FSTConfiguration createDefaultConfiguration() {
        return createDefaultConfiguration(null);
    }

    protected static FSTConfiguration createDefaultConfiguration(ConcurrentHashMap<FieldKey,FSTClazzInfo.FSTFieldInfo> shared) {
        if (isAndroid) {
            return createAndroidDefaultConfiguration(shared);
        }
        FSTConfiguration conf = new FSTConfiguration(shared);
        return initDefaultFstConfigurationInternal(conf);
    }

    protected static FSTConfiguration initDefaultFstConfigurationInternal(FSTConfiguration conf) {
        conf.registerIntToObjectMapFactory(new DefaultFSTInt2ObjectMapFactory());
        conf.addDefaultClazzes();
        // serializers
        FSTSerializerRegistry reg = conf.getCLInfoRegistry().getSerializerRegistry();
        reg.putSerializer(Class.class, new FSTClassSerializer(), false);
        reg.putSerializer(String.class, new FSTStringSerializer(), false);
        reg.putSerializer(Byte.class, new FSTBigNumberSerializers.FSTByteSerializer(), false);
        reg.putSerializer(Character.class, new FSTBigNumberSerializers.FSTCharSerializer(), false);
        reg.putSerializer(Short.class, new FSTBigNumberSerializers.FSTShortSerializer(), false);
        reg.putSerializer(Float.class, new FSTBigNumberSerializers.FSTFloatSerializer(), false);
        reg.putSerializer(Double.class, new FSTBigNumberSerializers.FSTDoubleSerializer(), false);
        reg.putSerializer(Date.class, new FSTDateSerializer(), false);
        reg.putSerializer(StringBuffer.class, new FSTStringBufferSerializer(), true);
        reg.putSerializer(StringBuilder.class, new FSTStringBuilderSerializer(), true);
        reg.putSerializer(EnumSet.class, new FSTEnumSetSerializer(), true);

        // for most cases don't register for subclasses as in many cases we'd like to fallback to JDK implementation
        // (e.g. TreeMap) in order to guarantee complete serialization
        reg.putSerializer(ArrayList.class, new FSTArrayListSerializer(), false);
        reg.putSerializer(Vector.class, new FSTCollectionSerializer(), true);
        reg.putSerializer(LinkedList.class, new FSTCollectionSerializer(), false); // subclass should register manually
        reg.putSerializer(HashSet.class, new FSTCollectionSerializer(), false); // subclass should register manually
        reg.putSerializer(HashMap.class, new FSTMapSerializer(), false); // subclass should register manually
        reg.putSerializer(LinkedHashMap.class, new FSTMapSerializer(), false); // subclass should register manually
        reg.putSerializer(Hashtable.class, new FSTMapSerializer(), true);
        reg.putSerializer(ConcurrentHashMap.class, new FSTMapSerializer(), true);
        reg.putSerializer(FSTStruct.class, new FSTStructSerializer(), true);
        reg.putSerializer(Throwable.class, new FSTThrowableSerializer(),true);

        reg.putSerializer(BitSet.class, new FSTBitSetSerializer(),true);
        reg.putSerializer(Timestamp.class, new FSTTimestampSerializer(),true);

        // serializers for classes failing in fst JDK emulation (e.g. Android<=>JDK)
        reg.putSerializer(BigInteger.class, new FSTBigIntegerSerializer(), true);

        return conf;
    }

    public void registerIntToObjectMapFactory(FSTInt2ObjectMapFactory intToObjectMapFactory){
        if (intToObjectMapFactory != null) {
            this.intToObjectMapFactory = intToObjectMapFactory;
        }
    }

    /**
     * Returns a configuration using Unsafe to read write data.
     * - platform dependent byte order
     * - no value compression attempts
     * - makes heavy use of Unsafe, which can be dangerous in case
     *   of version conflicts
     *
     * Use only in case it makes a significant difference and you absolutely need the performance gain.
     * Performance gains depend on data. There are cases where this is even slower,
     * in some scenarios (many native arrays) it can be several times faster.
     * see also OffHeapCoder, OnHeapCoder.
     *
     */
    public static FSTConfiguration createUnsafeBinaryConfiguration() {
        return createUnsafeBinaryConfiguration(null);
    }

    protected static FSTConfiguration createUnsafeBinaryConfiguration(ConcurrentHashMap<FieldKey, FSTClazzInfo.FSTFieldInfo> shared) {
        if ( isAndroid )
            throw new RuntimeException("not supported under android platform, use default configuration");
        final FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration(shared);
        conf.type = ConfType.UNSAFE;
        conf.setStreamCoderFactory(new FBinaryStreamCoderFactory(conf));
        return conf;
    }

    /**
     * register a custom serializer for a given class or the class and all of its subclasses.
     * Serializers must be configured identical on read/write side and should be set before
     * actually making use of the Configuration.
     *
     * @param clazz
     * @param ser
     * @param alsoForAllSubclasses
     */
    public void registerSerializer(Class clazz, FSTObjectSerializer ser, boolean alsoForAllSubclasses ) {
        serializationInfoRegistry.getSerializerRegistry().putSerializer(clazz, ser, alsoForAllSubclasses);
    }

    public boolean isForceClzInit() {
        return forceClzInit;
    }

    public LastResortClassResolver getLastResortResolver() {
        return lastResortResolver;
    }

    public void setLastResortResolver(LastResortClassResolver lastResortResolver) {
        this.lastResortResolver = lastResortResolver;
    }

    /**
     * always execute default fields init, even if no transients (so would get overwritten anyway)
     * required for lossy codecs (kson)
     *
     * @param forceClzInit
     * @return
     */
    public FSTConfiguration setForceClzInit(boolean forceClzInit) {
        this.forceClzInit = forceClzInit;
        return this;
    }

    public FSTClassInstantiator getInstantiator(Class clazz) {
        return instantiator;
    }

    public void setInstantiator(FSTClassInstantiator instantiator) {
        this.instantiator = instantiator;
    }

    public <T> T getCoderSpecific() {
        return (T) coderSpecific;
    }

    public void setCoderSpecific(Object coderSpecific) {
        this.coderSpecific = coderSpecific;
    }

    public void setClassLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    /**
     * special configuration used internally for struct emulation
     * @return
     */
    public static FSTConfiguration createStructConfiguration() {
        FSTConfiguration conf = new FSTConfiguration(null);
        conf.setStructMode(true);
        return conf;
    }

    protected FSTConfiguration(ConcurrentHashMap<FieldKey,FSTClazzInfo.FSTFieldInfo> sharedFieldInfos) {
        this.fieldInfoCache = sharedFieldInfos;
    }

    public StreamCoderFactory getStreamCoderFactory() {
        return streamCoderFactory;
    }

    /**
     * allows to use subclassed stream codecs. Can also be used to change class loading behaviour, as
     * clasForName is part of a codec's interface.
     *
     * e.g. new StreamCoderFactory() {
     *   @Override
     *   public FSTEncoder createStreamEncoder() {
     *      return new FSTStreamEncoder(FSTConfiguration.this);
     *   }
     *
     *   @Override
     *   public FSTDecoder createStreamDecoder() {
     *      return new FSTStreamDecoder(FSTConfiguration.this) { public Class classForName(String name) { ... }  } ;
     *   }
     * };
     *
     * You need to work with thread locals most probably as the factory is ~global (assigned to fstconfiguration shared amongst
     * streams)
     *
     * @param streamCoderFactory
     */
    public void setStreamCoderFactory(StreamCoderFactory streamCoderFactory) {
        this.streamCoderFactory = streamCoderFactory;
    }

    /**
     * reuse heavy weight objects. If a FSTStream is closed, objects are returned and can be reused by new stream instances.
     * the objects are held in soft references, so there should be no memory issues. FIXME: point of contention !
     * @param cached
     */
    public void returnObject( Object cached ) {
        try {
            while (!cacheLock.compareAndSet(false, true)) {
                // empty
            }
            List<SoftReference> li = cachedObjects.get(cached.getClass());
            if ( li == null ) {
                li = new ArrayList<SoftReference>();
                cachedObjects.put(cached.getClass(),li);
            }
            if ( li.size() < 5 )
                li.add(new SoftReference(cached));
        } finally {
            cacheLock.set(false);
        }
    }

    /**
     * ignored currently
     * @return
     */
    public boolean isPreferSpeed() {
        return preferSpeed;
    }

    /**
     * this options lets FST favour speed of encoding over size of the encoded object.
     * Warning: this option alters the format of the written stream, so both reader and writer should have
     * the same setting, else exceptions will occur
     * @param preferSpeed
     */
    public void setPreferSpeed(boolean preferSpeed) {
        this.preferSpeed = preferSpeed;
    }

    /**
     * for optimization purposes, do not use to benchmark processing time or in a regular program as
     * this methods creates a temporary binaryoutputstream and serializes the object in order to measure the
     * size.
     */
    public int calcObjectSizeBytesNotAUtility( Object obj ) throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream(10000);
        FSTObjectOutput ou = new FSTObjectOutput(bout,this);
        ou.writeObject(obj, obj.getClass());
        ou.close();
        return bout.toByteArray().length;
    }

    /**
     * patch default serializer lookup. set to null to delete.
     * Should be set prior to any serialization going on (serializer lookup is cached).
     * @param del
     */
    public void setSerializerRegistryDelegate( FSTSerializerRegistryDelegate del ) {
        serializationInfoRegistry.setSerializerRegistryDelegate(del);
    }

    AtomicBoolean cacheLock = new AtomicBoolean(false);
    public Object getCachedObject( Class cl ) {
        try  {
            while (!cacheLock.compareAndSet(false, true)) {
                // empty
            }
            List<SoftReference> li = cachedObjects.get(cl);
            if ( li == null ) {
                return null;
            }
            for (int i = li.size()-1; i >= 0; i--) {
                SoftReference softReference = li.get(i);
                Object res = softReference.get();
                li.remove(i);
                if ( res != null ) {
                    return res;
                }
            }
        } finally {
            cacheLock.set(false);
        }
        return null;
    }

    public boolean isForceSerializable() {
        return forceSerializable;
    }

    /**
     * treat unserializable classes same as if they would be serializable.
     * @param forceSerializable
//     */
    public FSTConfiguration setForceSerializable(boolean forceSerializable) {
        this.forceSerializable = forceSerializable;
        return this;
    }

    /**
     * clear global deduplication caches. Useful for class reloading scenarios, else counter productive as
     * j.reflect.Fiwld + Construtors will be instantiated more than once per class.
     */
    public static void clearGlobalCaches() {
        FSTClazzInfo.sharedFieldSets.clear();
        FSTDefaultClassInstantiator.constructorMap.clear();
    }

    /**
     * clear cached softref's and ThreadLocal.
     */
    public void clearCaches() {
        try {
            FSTInputStream.cachedBuffer.set(null);
            while (!cacheLock.compareAndSet(false, true)) {
                // empty
            }
            cachedObjects.clear();
        } finally {
            cacheLock.set( false );
        }
    }

    public boolean isShareReferences() {
        return shareReferences;
    }

    /**
     * if false, identical objects will get serialized twice. Gains speed as long there are no double objects/cyclic references (typical for small snippets as used in e.g. RPC)
     *
     * Cycles and Objects referenced more than once will not be detected (if set to false).
     * Additionally JDK compatibility is not supported (read/writeObject and stuff). Use case is highperformance
     * serialization of plain cycle free data (e.g. messaging). Can perform significantly faster (20-40%).
     *
     * @param shareReferences
     *
     */
    public void setShareReferences(boolean shareReferences) {
        this.shareReferences = shareReferences;
    }

    /**
     *
     * Preregister a class (use at init time). This avoids having to write class names.
     * Its a very simple and effective optimization (frequently > 2 times faster for small objects).
     *
     * Read and write side need to have classes preregistered in the exact same order.
     *
     * The list does not have to be complete. Just add your most frequently serialized classes here
     * to get significant gains in speed and smaller serialized representation size.
     *
     */
    public void registerClass( Class ... c) {
        for (int i = 0; i < c.length; i++) {
            classRegistry.registerClass(c[i],this);
            try {
                Class ac = Class.forName("[L"+c[i].getName()+";");
                classRegistry.registerClass(ac,this);
            } catch (ClassNotFoundException e) {
                // silent
            }
        }
    }

    void addDefaultClazzes() {
        classRegistry.registerClass(String.class,this);
        classRegistry.registerClass(Byte.class,this);
        classRegistry.registerClass(Short.class,this);
        classRegistry.registerClass(Integer.class,this);
        classRegistry.registerClass(Long.class,this);
        classRegistry.registerClass(Float.class,this);
        classRegistry.registerClass(Double.class,this);
        classRegistry.registerClass(BigDecimal.class,this);
        classRegistry.registerClass(BigInteger.class,this);
        classRegistry.registerClass(Character.class,this);
        classRegistry.registerClass(Boolean.class,this);
        classRegistry.registerClass(TreeMap.class,this);
        classRegistry.registerClass(HashMap.class,this);
        classRegistry.registerClass(ArrayList.class,this);
        classRegistry.registerClass(ConcurrentHashMap.class,this);
        classRegistry.registerClass(URL.class,this);
        classRegistry.registerClass(Date.class,this);
        classRegistry.registerClass(java.sql.Date.class,this);
        classRegistry.registerClass(SimpleDateFormat.class,this);
        classRegistry.registerClass(TreeSet.class,this);
        classRegistry.registerClass(LinkedList.class,this);
        classRegistry.registerClass(SimpleTimeZone.class,this);
        classRegistry.registerClass(GregorianCalendar.class,this);
        classRegistry.registerClass(Vector.class,this);
        classRegistry.registerClass(Hashtable.class,this);
        classRegistry.registerClass(BitSet.class,this);
        classRegistry.registerClass(Timestamp.class, this);
        classRegistry.registerClass(Locale.class,this);

        classRegistry.registerClass(StringBuffer.class,this);
        classRegistry.registerClass(StringBuilder.class,this);

        classRegistry.registerClass(Object.class,this);
        classRegistry.registerClass(Object[].class,this);
        classRegistry.registerClass(Object[][].class,this);
        classRegistry.registerClass(Object[][][].class,this);

        classRegistry.registerClass(byte[].class,this);
        classRegistry.registerClass(byte[][].class,this);

        classRegistry.registerClass(char[].class,this);
        classRegistry.registerClass(char[][].class,this);

        classRegistry.registerClass(short[].class,this);
        classRegistry.registerClass(short[][].class,this);

        classRegistry.registerClass(int[].class,this);
        classRegistry.registerClass(int[][].class,this);

        classRegistry.registerClass(float[].class,this);
        classRegistry.registerClass(float[][].class,this);

        classRegistry.registerClass(double[].class,this);
        classRegistry.registerClass(double[][].class,this);

        classRegistry.registerClass(long[].class,this);
        classRegistry.registerClass(long[][].class,this);

    }

    public FSTClazzNameRegistry getClassRegistry() {
        return classRegistry;
    }

    public FSTClazzInfoRegistry getCLInfoRegistry() {
        return serializationInfoRegistry;
    }

    public ClassLoader getClassLoader() {
        return classLoader;
    }

    public FSTClazzInfo getClassInfo(Class type) {
        return serializationInfoRegistry.getCLInfo(type, this);
    }

    /**
     * utility for thread safety and reuse. Do not close the resulting stream. However you should close
     * the given InputStream 'in'
     * @param in
     * @return
     */
    public FSTObjectInput getObjectInput( InputStream in ) {
        FSTObjectInput fstObjectInput = getIn();
        try {
            fstObjectInput.resetForReuse(in);
            return fstObjectInput;
        } catch (IOException e) {
            FSTUtil.<RuntimeException>rethrow(e);
        }
        return null;
    }

    public FSTObjectInput getObjectInput() {
        return getObjectInput((InputStream)null);
    }

    /**
     * take the given array as input. the array is NOT copied.
     *
     * WARNING: the input streams takes over ownership and might overwrite content
     * of this array in subsequent IO operations.
     *
     * @param arr
     * @return
     */
    public FSTObjectInput getObjectInput( byte arr[]) {
        return getObjectInput(arr, arr.length);
    }

    /**
     * take the given array as input. the array is NOT copied.
     *
     * WARNING: the input streams takes over ownership and might overwrite content
     * of this array in subsequent IO operations.
     *
     * @param arr
     * @param len
     * @return
     */
    public FSTObjectInput getObjectInput( byte arr[], int len ) {
        FSTObjectInput fstObjectInput = getIn();
        try {
            fstObjectInput.resetForReuseUseArray(arr,len);
            return fstObjectInput;
        } catch (IOException e) {
            FSTUtil.<RuntimeException>rethrow(e);
        }
        return null;
    }

    /**
     * take the given array and copy it to input. the array IS copied
     * @param arr
     * @param len
     * @return
     */
    public FSTObjectInput getObjectInputCopyFrom( byte arr[],int off, int len ) {
        FSTObjectInput fstObjectInput = getIn();
        try {
            fstObjectInput.resetForReuseCopyArray(arr, off, len);
            return fstObjectInput;
        } catch (IOException e) {
            FSTUtil.<RuntimeException>rethrow(e);
        }
        return null;
    }

    protected FSTObjectInput getIn() {
        FSTObjectInput fstObjectInput = (FSTObjectInput) streamCoderFactory.getInput().get();
        if ( fstObjectInput != null && fstObjectInput.isClosed() )
            fstObjectInput = null;
        if ( fstObjectInput == null ) {
            streamCoderFactory.getInput().set(new FSTObjectInput(this));
            return getIn();
        }
        fstObjectInput.conf = this;
        fstObjectInput.getCodec().setConf(this);
        return fstObjectInput;
    }

    protected FSTObjectOutput getOut() {
        FSTObjectOutput fstOut = (FSTObjectOutput) streamCoderFactory.getOutput().get();
        if ( fstOut == null || fstOut.closed ) {
            streamCoderFactory.getOutput().set(new FSTObjectOutput(this));
            return getOut();
        }
        fstOut.conf = this;
        fstOut.getCodec().setConf(this);
        return fstOut;
    }

    /**
     * utility for thread safety and reuse. Do not close the resulting stream. However you should close
     * the given OutputStream 'out'
     * @param out - can be null (temp bytearrays stream is created then)
     * @return
     */
    public FSTObjectOutput getObjectOutput(OutputStream out) {
        FSTObjectOutput fstObjectOutput = getOut();
        fstObjectOutput.resetForReUse(out);
        return fstObjectOutput;
    }

    /**
     * @return a recycled outputstream reusing its last recently used byte[] buffer
     */
    public FSTObjectOutput getObjectOutput() {
        return getObjectOutput((OutputStream)null);
    }

    public FSTObjectOutput getObjectOutput(byte[] outByte) {
        FSTObjectOutput fstObjectOutput = getOut();
        fstObjectOutput.resetForReUse(outByte);
        return fstObjectOutput;
    }

    /**
     * ignores all serialization related interfaces (Serializable, Externalizable) and serializes all classes using the
     * default scheme. Warning: this is a special mode of operation which fail serializing/deserializing many standard
     * JDK classes.
     *
     * @param ignoreSerialInterfaces
     */
    public void setStructMode(boolean ignoreSerialInterfaces) {
        serializationInfoRegistry.setStructMode(ignoreSerialInterfaces);
    }

    /**
     * special for structs
     * @return
     */
    public boolean isStructMode() {
        return serializationInfoRegistry.isStructMode();
    }

    public FSTClazzInfo getClazzInfo(Class rowClass) {
        return getCLInfoRegistry().getCLInfo(rowClass, this);
    }

    public void setCrossPlatform(boolean crossPlatform) {
        this.crossPlatform = crossPlatform;
    }

    public boolean isCrossPlatform() {
        return crossPlatform;
    }

    public <T> T deepCopy(T metadata) {
        return (T) asObject(asByteArray(metadata));
    }

    public static interface StreamCoderFactory {
        FSTEncoder createStreamEncoder();
        FSTDecoder createStreamDecoder();
        ThreadLocal getInput();
        ThreadLocal getOutput();
    }

    public FSTEncoder createStreamEncoder() {
        return streamCoderFactory.createStreamEncoder();
    }

    public FSTDecoder createStreamDecoder() {
        return streamCoderFactory.createStreamDecoder();
    }

    AtomicBoolean cplock = new AtomicBoolean(false);
    public void registerCrossPlatformClassBinaryCache( String fulLQName, byte[] binary ) {
        try {
            while (cplock.compareAndSet(false, true)) { } // spin
            minBinNamesBytez.put(fulLQName, binary);
        } finally {
            cplock.set(false);
        }
    }

    public byte[] getCrossPlatformBinaryCache(String symbolicName) {
        try {
            while ( cplock.compareAndSet(false, true)) { } // spin
            return minBinNamesBytez.get(symbolicName);
        } finally {
            cplock.set(false);
        }
    }

    /**
     * init right after creation of configuration, not during operation as it is not threadsafe regarding mutation
     * currently only for minbin serialization
     *
     * @param keysAndVals { { "symbolicName", "fullQualifiedClazzName" }, .. }
     */
    public FSTConfiguration registerCrossPlatformClassMapping( String[][] keysAndVals ) {
        for (int i = 0; i < keysAndVals.length; i++) {
            String[] keysAndVal = keysAndVals[i];
            registerCrossPlatformClassMapping(keysAndVal[0], keysAndVal[1]);
        }
        return this;
    }

    public FSTConfiguration registerCrossPlatformClassMapping( String shortName,  String fqName ) {
        minbinNames.put(shortName, fqName);
        minbinNamesReverse.put(fqName, shortName);
        return this;
    }

    /**
     * shorthand for registerCrossPlatformClassMapping(_,_)
     * @param shortName - class name in json type field
     * @param clz - class
     * @return
     */
    public FSTConfiguration cpMap(String shortName, Class clz) {
        return registerCrossPlatformClassMapping(shortName,clz.getName());
    }

    /**
     * init right after creation of configuration, not during operation as it is not threadsafe regarding mutation
     */
    public FSTConfiguration registerCrossPlatformClassMappingUseSimpleName( Class ... classes ) {
        registerCrossPlatformClassMappingUseSimpleName(Arrays.asList(classes));
        return this;
    }

    public FSTConfiguration registerCrossPlatformClassMappingUseSimpleName( List<Class> classes ) {
        for (int i = 0; i < classes.size(); i++) {
            Class clz = classes.get(i);
            minbinNames.put(clz.getSimpleName(), clz.getName());
            minbinNamesReverse.put(clz.getName(), clz.getSimpleName());
            try {
                if (!clz.isArray() ) {
                    Class ac = Class.forName("[L"+clz.getName()+";");
                    minbinNames.put(clz.getSimpleName()+"[]", ac.getName());
                    minbinNamesReverse.put(ac.getName(), clz.getSimpleName()+"[]");
                }
            } catch (ClassNotFoundException e) {
                FSTUtil.<RuntimeException>rethrow(e);
            }
        }
        return this;
    }

    /**
     * get cross platform symbolic class identifier
     * @param cl
     * @return
     */
    public String getCPNameForClass( Class cl ) {
        String res = minbinNamesReverse.get(cl.getName());
        if (res == null) {
            if (cl.isAnonymousClass()) {
                return getCPNameForClass(cl.getSuperclass());
            }
            return cl.getName();
        }
        return res;
    }

    public String getClassForCPName( String name ) {
        String res = minbinNames.get(name);
        if (res == null) {
            return name;
        }
        return res;
    }

    /**
     * convenience
     */
    public Object asObject( byte b[] ) {
        try {
            return getObjectInput(b).readObject();
        } catch (Exception e) {
            System.out.println("unable to decode:" +new String(b,0,0,Math.min(b.length,100)) );
            try {
                String debug = new String(b, "UTF-8");
            } catch (UnsupportedEncodingException e1) {
                //
            }
            try {
                getObjectInput(b).readObject();
            } catch (Exception e1) {
                // debug hook
            }
            FSTUtil.<RuntimeException>rethrow(e);
        }
        return null;
    }

    /**
     * convenience. (object must be serializable)
     */
    public byte[] asByteArray( Object object ) {
        FSTObjectOutput objectOutput = getObjectOutput();
        try {
            objectOutput.writeObject(object);
            return objectOutput.getCopyOfWrittenBuffer();
        } catch (IOException e) {
            try {
//                FSTConfiguration.prettyPrintJson(object); endless cycle !
            } catch (Exception ee) {
                //
            }
            FSTUtil.<RuntimeException>rethrow(e);
        }
        return null;
    }

    /**
     * Warning: avoids allocation + copying.
     * The returned byteArray is a direct pointer to underlying buffer.
     * the int length[] is expected to have at least on element.
     * The buffer can be larger than written data, therefore length[0] will contain written length.
     *
     * The buffer content must be used (e.g. sent to network, copied to offheap) before doing another
     * asByteArray on the current Thread.
     */
    public byte[] asSharedByteArray( Object object, int length[] ) {
        FSTObjectOutput objectOutput = getObjectOutput();
        try {
            objectOutput.writeObject(object);
            length[0] = objectOutput.getWritten();
            return objectOutput.getBuffer();
        } catch (IOException e) {
            FSTUtil.<RuntimeException>rethrow(e);
        }
        return null;
    }

    /**
     * utility/debug method. Use "asByteArray" for programmatic use as the
     * byte array will already by UTF-8 and ready to be sent on network.
     *
     * @param o
     * @return
     */
    public String asJsonString(Object o) {
        if ( getCoderSpecific() instanceof JsonFactory == false ) {
            return "can be called on JsonConfiguration only";
        } else {
            return new String(asByteArray(o), StandardCharsets.UTF_8);
        }
    }

    /**
     * helper to write series of objects to streams/files > Integer.MAX_VALUE.
     * it
     *  - serializes the object
     *  - writes the length of the serialized object to the stream
     *  - the writes the serialized object data
     *
     * on reader side (e.g. from a blocking socketstream, the reader then
     *  - reads the length
     *  - reads [length] bytes from the stream
     *  - deserializes
     *
     * @see decodeFromStream
     *
     * @param out
     * @param toSerialize
     * @throws IOException
     */
    public void encodeToStream( OutputStream out, Object toSerialize ) throws IOException {
        FSTObjectOutput objectOutput = getObjectOutput(); // could also do new with minor perf impact
        objectOutput.writeObject(toSerialize);
        int written = objectOutput.getWritten();
        out.write((written >>> 0) & 0xFF);
        out.write((written >>> 8) & 0xFF);
        out.write((written >>> 16) & 0xFF);
        out.write((written >>> 24) & 0xFF);

        // copy internal buffer to bufferedoutput
        out.write(objectOutput.getBuffer(), 0, written);
        objectOutput.flush();
    }

    /**
     * @see encodeToStream
     *
     * @param in
     * @return
     * @throws Exception
     */
    public Object decodeFromStream( InputStream in ) throws Exception {
        int read = in.read();
        if ( read < 0 )
            throw new EOFException("stream is closed");
        int ch1 = (read + 256) & 0xff;
        int ch2 = (in.read()+ 256) & 0xff;
        int ch3 = (in.read() + 256) & 0xff;
        int ch4 = (in.read() + 256) & 0xff;
        int len = (ch4 << 24) + (ch3 << 16) + (ch2 << 8) + (ch1 << 0);
        if ( len <= 0 )
            throw new EOFException("stream is corrupted");
        byte buffer[] = new byte[len]; // this could be reused !
        while (len > 0) {
            len -= in.read(buffer, buffer.length - len, len);
        }
        return getObjectInput(buffer).readObject();
    }

    /**
     * @return a configzration sharing as much as possible state with the callee. Its valid to register
     * different serializers at the derived configzration obtained.
     */
    public FSTConfiguration deriveConfiguration() {
        if ( fieldInfoCache == null ) {
            fieldInfoCache = new ConcurrentHashMap<>();
        }
        FSTConfiguration derived = createConfiguration(type, shareReferences, fieldInfoCache);
        // try to share as much as possible to save memory
        // note: the creation of those objects in createConfiguration() is unnecessary,
        // would need a specials lean creation method to avoid that (+init overhead)
        //
        // still no good test coverage. Problematic distribution of state and references all across the
        // code (to reduce pointer chasing) makes it problematic to implement stuff like this (errors might occur on nasty edge cases)
        derived.fieldInfoCache = fieldInfoCache;

        // sharing does not work. need manual clean up
//        derived.output = output;
//        derived.input = input;

//        cannot derive => hard link to conf in anonymous
//        derived.streamCoderFactory = streamCoderFactory;
//        derived.instantiator = instantiator;
//        derived.lastResortResolver = lastResortResolver;

        // avoid concurrent registering later on !
        derived.minbinNames = minbinNames;
        derived.minBinNamesBytez = minBinNamesBytez;
        derived.minbinNamesReverse = minbinNamesReverse;

        // errors with websockets ..
//        derived.classRegistry = classRegistry;
        return derived;
    }

    @Override
    public String toString() {
        return "FSTConfiguration{" +
                   "name='" + name + '\'' +
                   '}';
    }

    protected static class MinBinStreamCoderFactory implements StreamCoderFactory {
        private final FSTConfiguration conf;

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

        @Override
        public FSTEncoder createStreamEncoder() {
            return new FSTMinBinEncoder(conf);
        }

        @Override
        public FSTDecoder createStreamDecoder() {
            return new FSTMinBinDecoder(conf);
        }

        static ThreadLocal input = new ThreadLocal();
        static ThreadLocal output = new ThreadLocal();

        @Override
        public ThreadLocal getInput() {
            return input;
        }

        @Override
        public ThreadLocal getOutput() {
            return output;
        }
    }

    protected static class FSTDefaultStreamCoderFactory implements FSTConfiguration.StreamCoderFactory {
        private FSTConfiguration fstConfiguration;

        public FSTDefaultStreamCoderFactory(FSTConfiguration fstConfiguration) {this.fstConfiguration = fstConfiguration;}

        @Override
        public FSTEncoder createStreamEncoder() {
            return new FSTStreamEncoder(fstConfiguration);
        }

        @Override
        public FSTDecoder createStreamDecoder() {
            return new FSTStreamDecoder(fstConfiguration);
        }

        static ThreadLocal input = new ThreadLocal();
        static ThreadLocal output = new ThreadLocal();

        @Override
        public ThreadLocal getInput() {
            return input;
        }

        @Override
        public ThreadLocal getOutput() {
            return output;
        }

    }

    protected static class JSonStreamCoderFactory implements StreamCoderFactory {
        protected final FSTConfiguration conf;

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

        @Override
        public FSTEncoder createStreamEncoder() {
            return new FSTJsonEncoder(conf);
        }

        @Override
        public FSTDecoder createStreamDecoder() {
            return new FSTJsonDecoder(conf);
        }

        static ThreadLocal input = new ThreadLocal();
        static ThreadLocal output = new ThreadLocal();

        @Override
        public ThreadLocal getInput() {
            return input;
        }

        @Override
        public ThreadLocal getOutput() {
            return output;
        }
    }

    protected static class FBinaryStreamCoderFactory implements StreamCoderFactory {
        protected final FSTConfiguration conf;

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

        @Override
        public FSTEncoder createStreamEncoder() {
            return new FSTBytezEncoder(conf, new HeapBytez(new byte[4096]));
        }

        @Override
        public FSTDecoder createStreamDecoder() {
            return new FSTBytezDecoder(conf);
        }

        static ThreadLocal input = new ThreadLocal();
        static ThreadLocal output = new ThreadLocal();

        @Override
        public ThreadLocal getInput() {
            return input;
        }

        @Override
        public ThreadLocal getOutput() {
            return output;
        }

    }
}


/*
 * 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.offheap.structs.Align;
import org.nustaq.serialization.annotations.*;
import org.nustaq.serialization.util.FSTMap;
import org.nustaq.serialization.util.FSTUtil;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created with IntelliJ IDEA.
 * User: Möller
 * Date: 03.11.12
 * Time: 13:08
 * To change this template use File | Settings | File Templates.
 */
public final class FSTClazzInfo {

    // cache constructor per class (big saving in permspace)
    public static boolean BufferConstructorMeta = true;
    // cache and share class.getDeclaredFields amongst all fstconfigs
    public static boolean BufferFieldMeta = true;

    /**
     * cache + share j.reflect.Field. This can be cleared in case it gets too fat/leaks mem (e.g. class reloading)
     */
    public static ConcurrentHashMap<Class,Field[]> sharedFieldSets = new ConcurrentHashMap<>();

    public static final Comparator<FSTFieldInfo> defFieldComparator = new Comparator<FSTFieldInfo>() {
        @Override
        public int compare(FSTFieldInfo o1, FSTFieldInfo o2) {
            int res = 0;

            if ( o1.getVersion() != o2.getVersion() ) {
                return o1.getVersion() < o2.getVersion() ? -1 : 1;
            }

            // order: version, boolean, primitives, conditionals, object references
            if (o1.getType() == boolean.class && o2.getType() != boolean.class) {
                return -1;
            }
            if (o1.getType() != boolean.class && o2.getType() == boolean.class) {
                return 1;
            }

            if (o1.isConditional() && !o2.isConditional()) {
                res = 1;
            } else if (!o1.isConditional() && o2.isConditional()) {
                res = -1;
            } else if (o1.isPrimitive() && !o2.isPrimitive()) {
                res = -1;
            } else if (!o1.isPrimitive() && o2.isPrimitive())
                res = 1;
//                if (res == 0) // 64 bit / 32 bit issues
//                    res = (int) (o1.getMemOffset() - o2.getMemOffset());
            if (res == 0)
                res = o1.getType().getSimpleName().compareTo(o2.getType().getSimpleName());
            if (res == 0)
                res = o1.getName().compareTo(o2.getName());
            if (res == 0) {
                return o1.getField().getDeclaringClass().getName().compareTo(o2.getField().getDeclaringClass().getName());
            }
            return res;
        }
    };
    Class[] predict;
    private boolean ignoreAnn;
    FSTMap<String, FSTFieldInfo> fieldMap;
    Method writeReplaceMethod, readResolveMethod;
    FSTMap<Class, FSTCompatibilityInfo> compInfo;

    Object decoderAttached; // for decoders

    public Object getDecoderAttached() {
        return decoderAttached;
    }

    public void setDecoderAttached(Object decoderAttached) {
        this.decoderAttached = decoderAttached;
    }

    boolean requiresCompatibleMode;
    boolean externalizable;
    boolean flat; // never share instances of this class
    boolean isAsciiNameShortString = false;
    boolean requiresInit = false;
    boolean hasTransient;
    FSTObjectSerializer ser;
    FSTFieldInfo fieldInfo[]; // serializable fields

    Class clazz;
    Object[] enumConstants;
    Constructor cons;
    int clzId = -1;
    int structSize = 0;

    FSTConfiguration conf;
    protected FSTClassInstantiator instantiator; // initialized from FSTConfiguration in constructor
    boolean crossPlatform;

    public FSTClazzInfo(FSTConfiguration conf, Class clazz, FSTClazzInfoRegistry infoRegistry, boolean ignoreAnnotations) {
        this.conf = conf; // fixme: historically was not bound to conf but now is. Remove redundant state + refs (note: may still be useful because of less pointerchasing)
        crossPlatform = conf.isCrossPlatform();
        this.clazz = clazz;
        enumConstants = clazz.getEnumConstants();
        ignoreAnn = ignoreAnnotations;
        createFields(clazz);

        instantiator = conf.getInstantiator(clazz);
        if (Externalizable.class.isAssignableFrom(clazz)) {
            externalizable = true;
            cons = instantiator.findConstructorForExternalize(clazz);
        } else if (Serializable.class.isAssignableFrom(clazz) || clazz == Object.class) {
            externalizable = false;
            cons = instantiator.findConstructorForSerializable(clazz);
        } else {
            if (!conf.isStructMode()) {
                if ( conf.isForceSerializable() || getSer() != null ) {
                    externalizable = false;
                    cons = instantiator.findConstructorForSerializable(clazz);
                } else {
                    throw new RuntimeException("Class " + clazz.getName() + " does not implement Serializable or externalizable");
                }
            } else {
                cons = instantiator.findConstructorForSerializable(clazz);
            }
        }
        if (!ignoreAnnotations) {
            Predict annotation = (Predict) clazz.getAnnotation(Predict.class);
            if (annotation != null) {
                predict = annotation.value();
            }
            flat = clazz.isAnnotationPresent(Flat.class);
        }

        if (cons != null) {
            cons.setAccessible(true);
        }

        final String name = clazz.getName();
        if (name.length() < 127) {
            isAsciiNameShortString = true;
            for (int i = 0; i < name.length(); i++) {
                if (name.charAt(i) > 127) {
                    isAsciiNameShortString = false;
                    break;
                }
            }
        }

        requiresInit = isExternalizable() || useCompatibleMode() || hasTransient || conf.isForceClzInit();

        if (useCompatibleMode() && crossPlatform && getSer() == null && !clazz.isEnum())
            throw new RuntimeException("cannot support legacy JDK serialization methods in crossplatform mode. Define a serializer for this class " + clazz.getName());
    }

    byte[] bufferedName;

    public byte[] getBufferedName() {
        if (bufferedName == null) {
            bufferedName = getClazz().getName().getBytes();
        }
        return bufferedName;
    }

    @Override
    public String toString() {
        return "FSTClazzInfo{" +
                "clazz=" + clazz +
                '}';
    }

    public boolean isAsciiNameShortString() {
        return isAsciiNameShortString;
    }

    public int getClzId() {
        return clzId;
    }

    public void setClzId(int clzId) {
        this.clzId = clzId;
    }

    public int getNumBoolFields() {
        FSTFieldInfo[] fis = getFieldInfo();
        for (int i = 0; i < fis.length; i++) {
            FSTFieldInfo fstFieldInfo = fis[i];
            if (fstFieldInfo.getType() != boolean.class) {
                return i;
            }
        }
        return fis.length;
    }

    public boolean isExternalizable() {
        return externalizable;
    }

    public final boolean isFlat() {
        return flat;
    }

    public final Class[] getPredict() {
        return predict;
    }

    public final Object newInstance(boolean doesRequireInit) {
        return instantiator.newInstance(clazz, cons, doesRequireInit || requiresInit, conf.isForceSerializable() );
    }

    /**
     * Sideeffect: sets hasTransient
     *
     * @param c
     * @param res
     * @return
     */
    static ReentrantLock shareLock = new ReentrantLock(false);
    public final List<Field> getAllFields(Class c, List<Field> res) {
        try {
            if ( BufferFieldMeta ) {
                shareLock.lock();
            }
            if (res == null) {
                res = new ArrayList<Field>();
            }
            if (c == null) {
                return res;
            }
            Field[] declaredFields = BufferFieldMeta && !conf.isStructMode() ? sharedFieldSets.get(c) : null;
            if (declaredFields == null) {
                declaredFields = c.getDeclaredFields();
                if (BufferFieldMeta && !conf.isStructMode())
                    sharedFieldSets.put(c, declaredFields);
            }
            List<Field> c1 = Arrays.asList(declaredFields);
            Collections.reverse(c1);
            for (int i = 0; i < c1.size(); i++) {
                Field field = c1.get(i);
                res.add(0, field);
            }
            for (int i = 0; i < res.size(); i++) {
                Field field = res.get(i);
                if (Modifier.isStatic(field.getModifiers()) || isTransient(c, field)) {
                    if (isTransient(c, field)) {
                        hasTransient = true;
                    }
                    res.remove(i);
                    i--;
                }
            }
            List<Field> allFields = getAllFields(c.getSuperclass(), res);
            return new ArrayList<>(allFields);
        } finally {
            if ( BufferFieldMeta )
                shareLock.unlock();
        }
    }

    private boolean isTransient(Class c, Field field) {
        if (Modifier.isTransient(field.getModifiers()))
            return true;
        while (c.getName().indexOf("$") >= 0) {
            c = c.getSuperclass(); // patch fuer reallive queries, kontraktor spore
        }
        if ( field.getName().startsWith("this$") && c.getAnnotation(AnonymousTransient.class) != null )
            return true;
        return (c.getAnnotation(Transient.class) != null && field.getAnnotation(Serialize.class) == null);
    }

    public final FSTFieldInfo[] getFieldInfo() {
        return fieldInfo;
    }

    public final FSTFieldInfo[] getFieldInfoFiltered(Class... toRemove) {
        FSTFieldInfo[] fis = getFieldInfo();
        int count = 0;
        for (int i = 0; i < fis.length; i++) {
            FSTFieldInfo fi = fis[i];
            boolean skip = false;
            for (int j = 0; j < toRemove.length; j++) {
                Class aClass = toRemove[j];
                if (fi.getField().getDeclaringClass() == aClass) {
                    skip = true;
                    break;
                }
            }
            if (!skip) {
                count++;
            }
        }
        FSTFieldInfo res[] = new FSTFieldInfo[count];
        count = 0;
        for (int i = 0; i < fis.length; i++) {
            FSTFieldInfo fi = fis[i];
            boolean skip = false;
            for (int j = 0; j < toRemove.length; j++) {
                Class aClass = toRemove[j];
                if (fi.getField().getDeclaringClass() == aClass) {
                    skip = true;
                    break;
                }
            }
            if (!skip) {
                res[count++] = fis[i];
            }
        }
        return res;
    }

    public final FSTFieldInfo getFieldInfo(String name, Class declaringClass) {
        if ( fieldMap != null ) {
            if (declaringClass == null) {
                return fieldMap.get(name);
            }
            return fieldMap.get(declaringClass.getName() + "#" + name); //FIXME: THIS IS VERY SLOW (only used by JSON / compatibility mode)
        } else {
            synchronized (this) {
                fieldMap = buildFieldMap();
                return getFieldInfo(name,declaringClass);
            }
        }
    }

    private FSTMap buildFieldMap() {
        FSTMap res = new FSTMap<>(fieldInfo.length);
        for (int i = 0; i < fieldInfo.length; i++) {
            Field field = fieldInfo[i].getField();
            if ( field != null ) {
                res.put(field.getDeclaringClass().getName() + "#" + field.getName(), fieldInfo[i]);
                res.put(field.getName(), fieldInfo[i]);
            }
        }
        return res;
    }

    private void createFields(Class c) {
        if (c.isInterface() || c.isPrimitive()) {
            return;
        }
        List<Field> fields = getAllFields(c, null);
        fieldInfo = new FSTFieldInfo[fields.size()];
        for (int i = 0; i < fields.size(); i++) {
            Field field = fields.get(i);
            fieldInfo[i] = createFieldInfo(field);
        }

        // compatibility info sort order
        Comparator<FSTFieldInfo> infocomp = new Comparator<FSTFieldInfo>() {
            @Override
            public int compare(FSTFieldInfo o1, FSTFieldInfo o2) {
                int res = 0;
                res = o1.getType().getSimpleName().compareTo(o2.getType().getSimpleName());
                if (res == 0)
                    res = o1.getType().getName().compareTo(o2.getType().getName());
                if (res == 0) {
                    Class declaringClass = o1.getType().getDeclaringClass();
                    Class declaringClass1 = o2.getType().getDeclaringClass();
                    if (declaringClass == null && declaringClass1 == null) {
                        return 0;
                    }
                    if (declaringClass != null && declaringClass1 == null) {
                        return 1;
                    }
                    if (declaringClass == null && declaringClass1 != null) {
                        return -1;
                    }
                    if (res == 0) {
                        return declaringClass.getName().compareTo(declaringClass1.getName());
                    }
                }
                return res;
            }
        };

        // check if we actually need to build up compatibility info (memory intensive)
        boolean requiresCompatibilityData = false;
        if ( ! Externalizable.class.isAssignableFrom(c) && getSerNoStore() == null ) {
            Class tmpCls = c;
            while( tmpCls != Object.class ) {
                if ( FSTUtil.findPrivateMethod(tmpCls, "writeObject", new Class<?>[]{ObjectOutputStream.class}, Void.TYPE) != null ||
                     FSTUtil.findPrivateMethod(tmpCls, "readObject", new Class<?>[]{ObjectInputStream.class},Void.TYPE) != null ||
                     FSTUtil.findDerivedMethod(tmpCls, "writeReplace", null, Object.class) != null ||
                     FSTUtil.findDerivedMethod(tmpCls, "readResolve", null, Object.class) != null ) {
                     requiresCompatibilityData = true;
                     break;
                }
                tmpCls = tmpCls.getSuperclass();
            }
        }

        if (!conf.isStructMode() && requiresCompatibilityData ) {
            getCompInfo();
            fieldMap = buildFieldMap();
            Class curCl = c;
            fields.clear();
            while (curCl != Object.class) {
                ObjectStreamClass os = null;
                try {
                    os = ObjectStreamClass.lookup(curCl);
                } catch (Exception e) {
                    FSTUtil.<RuntimeException>rethrow(e);
                }
                if (os != null) {
                    final ObjectStreamField[] fi = os.getFields();
                    List<FSTFieldInfo> curClzFields = new ArrayList<FSTFieldInfo>();
                    if (fi != null) {
                        for (int i = 0; i < fi.length; i++) {
                            ObjectStreamField objectStreamField = fi[i];
                            String ff = objectStreamField.getName();
                            final FSTFieldInfo fstFieldInfo = fieldMap.get(curCl.getName() + "#" + ff);
                            if (fstFieldInfo != null && fstFieldInfo.getField() != null) {
                                curClzFields.add(fstFieldInfo);
                                fields.add(fstFieldInfo.getField());
                            } else {
                                FSTFieldInfo fake = new FSTFieldInfo(null, null, true );
                                fake.type = objectStreamField.getType();
                                fake.fakeName = objectStreamField.getName();
                                curClzFields.add(fake);
                            }
                        }
                    }
                    Collections.sort(curClzFields, infocomp);
                    FSTCompatibilityInfo info = new FSTCompatibilityInfo(curClzFields, curCl);
                    getCompInfo().put(curCl, info);
                    if (info.needsCompatibleMode()) {
                        requiresCompatibleMode = true;
                    }
                }
                curCl = curCl.getSuperclass();
            }
        }

        // default sort order
        Comparator<FSTFieldInfo> comp = defFieldComparator;
        if (!conf.isStructMode())
            Arrays.sort(fieldInfo, comp);
        int off = 8; // object header: length + clzId
        for (int i = 0; i < fieldInfo.length; i++) {
            FSTFieldInfo fstFieldInfo = fieldInfo[i];
            Align al = fstFieldInfo.getField().getAnnotation(Align.class);
            if (al != null) {
                fstFieldInfo.align = al.value();
                int alignOff = fstFieldInfo.align(off);
                fstFieldInfo.alignPad = alignOff - off;
                off = alignOff;
            }
            fstFieldInfo.setStructOffset(off);
            off += fstFieldInfo.getStructSize();
        }
        structSize = off;
        writeReplaceMethod = FSTUtil.findDerivedMethod(
            c, "writeReplace", null, Object.class);
        readResolveMethod = FSTUtil.findDerivedMethod(
            c, "readResolve", null, Object.class);
        if (writeReplaceMethod != null) {
            writeReplaceMethod.setAccessible(true);
        }
        if (readResolveMethod != null) {
            readResolveMethod.setAccessible(true);
        }
        for (int i = 0; i < fieldInfo.length; i++) {
            FSTFieldInfo fstFieldInfo = fieldInfo[i];
            fstFieldInfo.indexId = i;
        }
    }

    public int getStructSize() {
        return structSize;
    }

    public boolean useCompatibleMode() {
        return requiresCompatibleMode || writeReplaceMethod != null || readResolveMethod != null;
    }

    static AtomicInteger fiCount = new AtomicInteger(0);
    static AtomicInteger missCount = new AtomicInteger(0);
    protected FSTFieldInfo createFieldInfo(Field field) {
        FSTConfiguration.FieldKey key = null;
        if ( conf.fieldInfoCache != null ) {
            key = new FSTConfiguration.FieldKey(field.getDeclaringClass(), field.getName());
            FSTFieldInfo res = conf.fieldInfoCache.get(key);
            if ( res != null ) {
                fiCount.incrementAndGet();
                return res;
            }
        }
        field.setAccessible(true);
        Predict predict = crossPlatform ? null : field.getAnnotation(Predict.class); // needs to be iognored cross platform
        FSTFieldInfo result = new FSTFieldInfo(predict != null ? predict.value() : null, field, ignoreAnn);
        if ( conf.fieldInfoCache != null && key != null ) {
            conf.fieldInfoCache.put(key,result);
        }
        missCount.incrementAndGet();
        return result;
    }

    public final Method getReadResolveMethod() {
        return readResolveMethod;
    }

    public final Method getWriteReplaceMethod() {
        return writeReplaceMethod;
    }

    public final Class getClazz() {
        return clazz;
    }

    public Object[] getEnumConstants() {
        return enumConstants;
    }

    public FSTMap<Class, FSTCompatibilityInfo> getCompInfo() {
        if (compInfo == null)
            compInfo = new FSTMap<>(3); // just avoid edge case NPE's
        return compInfo;
    }

    public final static class FSTFieldInfo {

        final public static int BOOL = 1;
        final public static int BYTE = 2;
        final public static int CHAR = 3;
        final public static int SHORT = 4;
        final public static int INT = 5;
        final public static int LONG = 6;
        final public static int FLOAT = 7;
        final public static int DOUBLE = 8;

        Class possibleClasses[];
        FSTClazzInfo lastInfo; // cache last class stored (can save a hash lookup)
        String oneOf[] = null;

        int arrayDim;
        Class arrayType;
        boolean flat = false;
        boolean isConditional = false;

        final Field field;
        Class type;
        boolean integral = false;
        boolean primitive = false;
        boolean isArr = false;
        byte version;
        int integralType;
        long memOffset = -1;
        boolean isAndroid = FSTConfiguration.isAndroid; // hope for better locality

        int structOffset = 0;
        int indexId; // position in serializable fields array
        int align = 0;
        int alignPad = 0;
        Object bufferedName; // cache byte rep of field name (used for cross platform)

        // hack required for compatibility with ancient JDK mechanics (cross JDK, e.g. Android <=> OpenJDK ).
        // in rare cases, a field used in putField is not present as a real field
        // in this case only these of a fieldinfo are set
        public String fakeName;

        public FSTFieldInfo(Class[] possibleClasses, Field fi, boolean ignoreAnnotations) {
            this.possibleClasses = possibleClasses;
            field = fi;
            if (fi == null) {
                isArr = false;
            } else {
                isArr = field.getType().isArray();
                type = fi.getType();
                primitive = type.isPrimitive();
                if (FSTUtil.unFlaggedUnsafe != null ) {
                    fi.setAccessible(true);
                    if (!Modifier.isStatic(fi.getModifiers())) {
                        try {
                            memOffset = (int) FSTUtil.unFlaggedUnsafe.objectFieldOffset(fi);
                        } catch (Throwable th) {
                            //throw FSTUtil.rethrow(th);
                        }
                    }
                }
            }
            if (isArray()) {
                String clName = field.getType().getName();
                arrayDim = 1 + clName.lastIndexOf('[');
                arrayType = calcComponentType(field.getType());
            }
            calcIntegral();
            if (fi != null && !ignoreAnnotations) {
                version = (byte) (fi.isAnnotationPresent(Version.class) ? fi.getAnnotation(Version.class).value() : 0);
                flat = fi.isAnnotationPresent(Flat.class);
                isConditional = fi.isAnnotationPresent(Conditional.class);
                if (isIntegral()) {
                    isConditional = false;
                }
                OneOf annotation = fi.getAnnotation(OneOf.class);
                if (annotation != null) {
                    oneOf = annotation.value();
                }
            }

        }

        public byte getVersion() {
            return version;
        }

        public Object getBufferedName() {
            return bufferedName;
        }

        public void setBufferedName(Object bufferedName) {
            this.bufferedName = bufferedName;
        }

        public int align(int off) {
            while ((off / align) * align != off)
                off++;
            return off;
        }

        public int getIndexId() {
            return indexId;
        }

        public int getStructOffset() {
            return structOffset;
        }

        public void setStructOffset(int structOffset) {
            this.structOffset = structOffset;
        }

        public String[] getOneOf() {
            return oneOf;
        }

        public long getMemOffset() {
            return memOffset;
        }

        public int getAlign() {
            return align;
        }

        public int getAlignPad() {
            return alignPad;
        }

        public boolean isConditional() {
            return isConditional;
        }

        public FSTClazzInfo getLastInfo() {
            return lastInfo;
        }

        public void setLastInfo(FSTClazzInfo lastInfo) {
            this.lastInfo = lastInfo;
        }

        Class calcComponentType(Class c) {
            if (c.isArray()) {
                return calcComponentType(c.getComponentType());
            }
            return c;
        }

        public boolean isVolatile() {
            return Modifier.isVolatile(getField().getModifiers());
        }

        public final Class getType() {
            return type;
        }

        public boolean isArray() {
            return isArr;
        }

        public int getArrayDepth() {
            return arrayDim;
        }

        public Class getArrayType() {
            return arrayType;
        }

        public Class[] getPossibleClasses() {
            return possibleClasses;
        }

        void setPossibleClasses(Class[] possibleClasses) {
            this.possibleClasses = possibleClasses;
        }

        public Field getField() {
            return field;
        }

        public void calcIntegral() {
            if (field == null) {
                return;
            }
            if (isArray()) {
                integral = isIntegral(getArrayType());
            } else {
                integral = isIntegral(field.getType());

                Class type = field.getType();
                integralType = getIntegralCode(type);
            }
        }

        public static int getIntegralCode(Class type) {
            if (type == boolean.class) {
                return BOOL;
            } else if (type == byte.class) {
                return BYTE;
            } else if (type == char.class) {
                return CHAR;
            } else if (type == short.class) {
                return SHORT;
            } else if (type == int.class) {
                return INT;
            } else if (type == long.class) {
                return LONG;
            } else if (type == float.class) {
                return FLOAT;
            } else if (type == double.class) {
                return DOUBLE;
            }
            return 0;
        }

        /**
         * only set if is not an array, but a direct native field type
         *
         * @return
         */
        public int getIntegralType() {
            return integralType;
        }

        public boolean isIntegral(Class type) {
            return type.isPrimitive();
        }

        /**
         * @return wether this is primitive or an array of primitives
         */
        public boolean isIntegral() {
            return integral;
        }

        public String getDesc() {
            return field != null ? "<" + field.getName() + " of " + field.getDeclaringClass().getSimpleName() + ">" : "<undefined referencee>";
        }

        public String toString() {
            return getDesc();
        }

        public boolean isFlat() {
            return flat;
        }

        public int getComponentStructSize() {
            if (arrayType == boolean.class || arrayType == byte.class)
                return 1;
            if (arrayType == char.class || arrayType == short.class)
                return 2;
            if (arrayType == int.class || arrayType == float.class)
                return 4;
            if (arrayType == long.class || arrayType == double.class)
                return 8;
            return 0; // object => cannot decide
        }

        public int getStructSize() {
            if (type == boolean.class || type == byte.class)
                return 1;
            if (type == char.class || type == short.class)
                return 2;
            if (type == int.class || type == float.class)
                return 4;
            if (type == long.class || type == double.class)
                return 8;
            if (isArray()) {
                if (isIntegral())
                    return 8; // pointer+length
                else // object array
                    return 16; // pointer+length+elemsiz+pointertype
            }
            return 4;
        }

        public boolean isPrimitive() {
            return primitive;
        }

        public final int getByteValue(Object obj) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                return FSTUtil.unFlaggedUnsafe.getByte(obj, memOffset);
            }
            return field.getByte(obj);
        }

        public final int getCharValue(Object obj) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                return FSTUtil.unFlaggedUnsafe.getChar(obj, memOffset);
            }
            return field.getChar(obj);
        }

        public final int getShortValue(Object obj) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                return FSTUtil.unFlaggedUnsafe.getShort(obj, memOffset);
            }
            return field.getShort(obj);
        }

        public final int getIntValueUnsafe(Object obj) throws IllegalAccessException {
            return FSTUtil.unFlaggedUnsafe.getInt(obj, memOffset);
        }

        public final long getLongValueUnsafe(Object obj) throws IllegalAccessException {
            return FSTUtil.unFlaggedUnsafe.getLong(obj, memOffset);
        }

        public final boolean getBooleanValue(Object obj) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                return FSTUtil.unFlaggedUnsafe.getBoolean(obj, memOffset);
            }
            return field.getBoolean(obj);
        }

        /**
         * Warning: crashes if not an object ref !
         * use getField().get() for a safe version ..
         *
         * @param obj
         * @return
         * @throws IllegalAccessException
         */
        public final Object getObjectValue(Object obj) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                return FSTUtil.unFlaggedUnsafe.getObject(obj, memOffset);
            }
            return field.get(obj);
        }

        public final float getFloatValue(Object obj) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                return FSTUtil.unFlaggedUnsafe.getFloat(obj, memOffset);
            }
            return field.getFloat(obj);
        }

        public final void setCharValue(Object newObj, char c) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                FSTUtil.unFlaggedUnsafe.putChar(newObj, memOffset, c);
                return;
            }
            field.setChar(newObj, c);
        }

        public final void setShortValue(Object newObj, short i1) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                FSTUtil.unFlaggedUnsafe.putShort(newObj, memOffset, i1);
                return;
            }
            field.setShort(newObj, i1);
        }

        public final void setObjectValue(Object target, Object value) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                FSTUtil.unFlaggedUnsafe.putObject(target, memOffset, value);
                return;
            }
            field.set(target, value);
        }

        public final void setFloatValue(Object newObj, float l) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                FSTUtil.unFlaggedUnsafe.putFloat(newObj, memOffset, l);
                return;
            }
            field.setFloat(newObj, l);
        }

        public final void setDoubleValue(Object newObj, double l) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                FSTUtil.unFlaggedUnsafe.putDouble(newObj, memOffset, l);
                return;
            }
            field.setDouble(newObj, l);
        }

        public final void setLongValue(Object newObj, long i1) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                FSTUtil.unFlaggedUnsafe.putLong(newObj, memOffset, i1);
                return;
            }
            field.setLong(newObj, i1);
        }

        public final long getLongValue(Object obj) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                return FSTUtil.unFlaggedUnsafe.getLong(obj, memOffset);
            }
            return field.getLong(obj);
        }

        public final double getDoubleValue(Object obj) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                return FSTUtil.unFlaggedUnsafe.getDouble(obj, memOffset);
            }
            return field.getDouble(obj);
        }

        public final void setIntValue(Object newObj, int i1) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                FSTUtil.unFlaggedUnsafe.putInt(newObj, memOffset, i1);
                return;
            }
            field.setInt(newObj, i1);
        }

        public final int getIntValue(Object obj) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                return FSTUtil.unFlaggedUnsafe.getInt(obj, memOffset);
            }
            return field.getInt(obj);
        }

        public final void setBooleanValue(Object newObj, boolean i1) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                FSTUtil.unFlaggedUnsafe.putBoolean(newObj, memOffset, i1);
                return;
            }
            field.setBoolean(newObj, i1);
        }

        public final void setByteValue(Object newObj, byte b) throws IllegalAccessException {
            if (!isAndroid && memOffset >= 0) {
                FSTUtil.unFlaggedUnsafe.putByte(newObj, memOffset, b);
                return;
            }
            field.setByte(newObj, b);
        }

        public String getName() {
            return field != null ? field.getName() : fakeName;
        }
    }

    /**
     * sideeffecting: if no ser is found, next lookup will return null immediate
     * @return
     */
    public FSTObjectSerializer getSer() {
        if (ser == null) {
            if (clazz == null) {
                return null;
            }
            ser = getSerNoStore();
            if (ser == null) {
                ser = FSTSerializerRegistry.NULL;
            }
        }
        if (ser == FSTSerializerRegistry.NULL) {
            return null;
        }
        return ser;
    }

    // no sideffecting lookup
    public FSTObjectSerializer getSerNoStore() {
        return conf.getCLInfoRegistry().getSerializerRegistry().getSerializer(clazz);
    }

    static class FSTCompatibilityInfo {
        Method writeMethod, readMethod;
        ObjectStreamClass objectStreamClass;
        List<FSTFieldInfo> infos;
        Class clazz;
        FSTFieldInfo infoArr[];

        public FSTCompatibilityInfo(List<FSTFieldInfo> inf, Class c) {
            readClazz(c);
            infos = inf;
            clazz = c;
        }

        public List<FSTFieldInfo> getFields() {
            return infos;
        }

        public FSTFieldInfo[] getFieldArray() {
            if (infoArr == null) {
                List<FSTClazzInfo.FSTFieldInfo> fields = getFields();
                final FSTFieldInfo[] fstFieldInfos = new FSTFieldInfo[fields.size()];
                fields.toArray(fstFieldInfos);
                Arrays.sort(fstFieldInfos, defFieldComparator);
                infoArr = fstFieldInfos;
            }
            return infoArr;
        }

        public Class getClazz() {
            return clazz;
        }

        public boolean needsCompatibleMode() {
            return writeMethod != null || readMethod != null;
        }

        public void readClazz(Class c) {
            writeMethod = FSTUtil.findPrivateMethod(c, "writeObject",
                                                    new Class<?>[]{ObjectOutputStream.class},
                                                    Void.TYPE);
            readMethod = FSTUtil.findPrivateMethod(c, "readObject",
                                                   new Class<?>[]{ObjectInputStream.class},
                                                   Void.TYPE);
            if (writeMethod != null) {
                writeMethod.setAccessible(true);
            }
            if (readMethod != null) {
                readMethod.setAccessible(true);
            }
        }

        public Method getReadMethod() {
            return readMethod;
        }

        public void setReadMethod(Method readMethod) {
            this.readMethod = readMethod;
        }

        public Method getWriteMethod() {
            return writeMethod;
        }

        public void setWriteMethod(Method writeMethod) {
            this.writeMethod = writeMethod;
        }

        public boolean isAsymmetric() {
            return (getReadMethod() == null && getWriteMethod() != null) || (getWriteMethod() == null && getReadMethod() != null);
        }

    }

}


/*
 * 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.minbin;

import java.lang.reflect.Array;

/**
* Date: 12.04.2014
* Time: 22:11
* To change this template use File | Settings | File Templates.
*/
public class MBOut {
    byte bytez[] = new byte[500];
    int pos = 0;

    MinBin mb;

    public MBOut() {
        this(MinBin.DefaultInstance);
    }

    public MBOut(MinBin mb) {
        this.mb = mb;
    }

    /**
     * write single byte, grow byte array if needed
     * @param b
     */
    void writeOut(byte b) {
        if (pos == bytez.length - 1) {
            resize();
        }
        bytez[pos++] = b;
    }

    private void resize() {
        byte tmp[] = new byte[Math.min(bytez.length + 50 * 1000 * 1000, bytez.length * 2)];
        System.arraycopy(bytez, 0, tmp, 0, pos);
        bytez = tmp;
    }

    /**
     * write an int type with header
     * @param type
     * @param data
     */
    public void writeInt(byte type, long data) {
        if (!MinBin.isPrimitive(type) || MinBin.isArray(type))
            throw new RuntimeException("illegal type code");
        writeOut(type);
        writeRawInt(type, data);
    }
    /**
     * encode int without header tag
     * @param data
     */
    protected void writeRawInt(byte type, long data) {
        int numBytes = MinBin.extractNumBytes(type);
        for (int i = 0; i < numBytes; i++) {
            writeOut((byte) (data & 0xff));
            data = data >>> 8;
        }
    }
    /**
     * encode int using only as much bytes as needed to represent it
     * @param data
     */
    public void writeIntPacked(long data) {
        if (data <= Byte.MAX_VALUE && data >= Byte.MIN_VALUE)            writeInt(MinBin.INT_8,  data);
        else if (data <= Short.MAX_VALUE && data >= Short.MIN_VALUE)     writeInt(MinBin.INT_16, data);
        else if (data <= Integer.MAX_VALUE && data >= Integer.MIN_VALUE) writeInt(MinBin.INT_32, data);
        else if (data <= Long.MAX_VALUE && data >= Long.MIN_VALUE)       writeInt(MinBin.INT_64, data);
    }

    /**
     * write primitive array + header. no floating point or object array allowed. Just int based types
     * @param primitiveArray
     * @param start
     * @param len
     */
    public void writeArray(Object primitiveArray, int start, int len) {
        byte type = MinBin.ARRAY_MASK;
        Class<?> componentType = primitiveArray.getClass().getComponentType();
        if (componentType == boolean.class)    type |= MinBin.INT_8;
        else if (componentType == byte.class)  type |= MinBin.INT_8;
        else if (componentType == short.class) type |= MinBin.INT_16;
        else if (componentType == char.class)  type |= MinBin.INT_16 | MinBin.UNSIGN_MASK;
        else if (componentType == int.class)   type |= MinBin.INT_32;
        else if (componentType == long.class)  type |= MinBin.INT_64;
        else throw new RuntimeException("unsupported type " + componentType.getName());
        writeOut(type);
        writeIntPacked(len);
        switch (type) {
            case MinBin.INT_8|MinBin.ARRAY_MASK: {
                if ( componentType == boolean.class ) {
                    boolean[] arr = (boolean[]) primitiveArray;
                    for (int i = start; i < start + len; i++) {
                        writeRawInt(type, arr[i] ? 1 : 0);
                    }
                } else {
                    byte[] arr = (byte[]) primitiveArray;
                    for (int i = start; i < start + len; i++) {
                        writeRawInt(type, arr[i]);
                    }
                }
            }
            break;
            case MinBin.CHAR|MinBin.ARRAY_MASK: {
                char[] charArr = (char[]) primitiveArray;
                for (int i = start; i < start + len; i++) {
                    writeRawInt(type, charArr[i]);
                }
            }
            break;
            case MinBin.INT_32|MinBin.ARRAY_MASK: {
                int[] arr = (int[]) primitiveArray;
                for (int i = start; i < start + len; i++) {
                    writeRawInt(type, arr[i]);
                }
            }
            break;
            case MinBin.INT_64|MinBin.ARRAY_MASK: {
                long[] arr = (long[]) primitiveArray;
                for (int i = start; i < start + len; i++) {
                    writeRawInt(type, arr[i]);
                }
            }
            break;
            default: {
                for (int i = start; i < start + len; i++) {
                    if (componentType == boolean.class)
                        writeRawInt(type, Array.getBoolean(primitiveArray, i) ? 1 : 0);
                    else
                        writeRawInt(type, Array.getLong(primitiveArray, i));
                }
            }
        }
    }

    public void writeTagHeader(byte tagId) {
        writeOut(MinBin.getTagCode(tagId));
    }

    public void writeTag( Object obj ) {
        if (MinBin.END_MARKER == obj) {
            writeOut(MinBin.END);
            return;
        }
        MinBin.TagSerializer tagSerializer = mb.getSerializerFor(obj);
        if ( tagSerializer == null ) {
            throw new RuntimeException("no tag serializer found for "+obj.getClass().getName());
        }
        writeTagHeader((byte) tagSerializer.getTagId());
        tagSerializer.writeTag(obj,this);
    }

    public int getWritten() {  return pos; }
    public byte[] getBytez() { return bytez; }
    /**
     * completely reset state
     */
    public void reset() { pos = 0; }
    /**
     * completely reset and use given bytearray as buffer
     * @param bytez
     */
    public void reset(byte[] bytez) {
        pos = 0;
        this.bytez = bytez;
    }
    /**
     * only reset position
     */
    public void resetPosition() {
        pos = 0;
    }

    public void writeObject(Object o) {
        if ( o == null ) {
            writeTag(o);
        } else if ( o.getClass().isPrimitive() && o.getClass() != float.class && o.getClass() != double.class ) {
            writeIntPacked(((Number) o).longValue());
        } else if ( o.getClass().isArray() && o.getClass().getComponentType() != float.class && o.getClass().getComponentType() != double.class ) {
            writeArray(o,0,Array.getLength(o));
        } else if (o.getClass() == Byte.class){
            writeInt(MinBin.INT_8, ((Number)o).longValue());
        } else if (o.getClass() == Short.class){
            writeInt(MinBin.INT_16, ((Number)o).longValue());
        } else if (o.getClass() == Character.class){
            writeInt(MinBin.CHAR, ((Character)o).charValue());
        } else if (o.getClass() == Integer.class){
            writeInt(MinBin.INT_32, ((Number)o).longValue());
        } else if (o.getClass() == Long.class){
            writeInt(MinBin.INT_64, ((Number)o).longValue());
        } else {
            writeTag(o);
        }
    }

    /**
     * allow write through to underlying byte for performance reasons
     * @param bufferedName
     * @param i
     * @param length
     */
    public void writeRaw(byte[] bufferedName, int i, int length) {
        if (pos+length >= bytez.length - 1) {
            resize();
        }
        System.arraycopy(bufferedName,i,bytez,pos,length);
        pos += length;
    }
}