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

import org.apache.johnzon.mapper.access.AccessMode;
import org.apache.johnzon.mapper.converter.CharacterConverter;
import org.apache.johnzon.mapper.internal.AdapterKey;
import org.apache.johnzon.mapper.internal.ConverterAdapter;
import org.apache.johnzon.mapper.internal.JsonPointerTracker;
import org.apache.johnzon.mapper.number.Validator;
import org.apache.johnzon.mapper.reflection.JohnzonParameterizedType;

import javax.json.JsonArray;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonString;
import javax.json.JsonStructure;
import javax.json.JsonValue;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static javax.json.JsonValue.ValueType.ARRAY;
import static javax.json.JsonValue.ValueType.FALSE;
import static javax.json.JsonValue.ValueType.NULL;
import static javax.json.JsonValue.ValueType.NUMBER;
import static javax.json.JsonValue.ValueType.STRING;
import static javax.json.JsonValue.ValueType.TRUE;
import static org.apache.johnzon.mapper.Mappings.getPrimitiveDefault;

/**
 * This class is not concurrently usable as it contains state.
 */
public class MappingParserImpl implements MappingParser {

    private static final JohnzonParameterizedType ANY_LIST = new JohnzonParameterizedType(List.class, Object.class);
    private static final CharacterConverter CHARACTER_CONVERTER = new CharacterConverter(); // this one is particular, share the logic

    protected final ConcurrentMap<Class<?>, Method> valueOfs = new ConcurrentHashMap<Class<?>, Method>();

    private final MapperConfig config;
    private final Mappings mappings;

    private final JsonReader jsonReader;

    /**
     * Used for de-referencing JsonPointers during deserialisation.
     * key: JsonPointer
     * value: already deserialised Object
     */
    private Map<String, Object> jsonPointers;

    public MappingParserImpl(MapperConfig config, Mappings mappings, JsonReader jsonReader, Map<String, Object> jsonPointers) {
        this.config = config;
        this.mappings = mappings;
        this.jsonReader = jsonReader;
    }


    @Override
    public <T> T readObject(Type targetType) {
        try {
            return readObject(jsonReader.readValue(), targetType);
        } catch (final NoSuchMethodError noSuchMethodError) { // jsonp 1.0 fallback - mainly for tests
            return readObject(jsonReader.read(), targetType);
        }
    }

    @Override
    public <T> T readObject(final JsonValue jsonValue, final Type targetType) {
        return readObject(jsonValue, targetType, targetType instanceof Class || targetType instanceof ParameterizedType, null);
    }

    public <T> T readObject(final JsonValue jsonValue, final Type targetType, final boolean applyObjectConverter,
                            final Collection<Class<?>> skippedConverters) {
        final JsonValue.ValueType valueType = jsonValue != null ? jsonValue.getValueType() : null;

        if (JsonStructure.class == targetType || JsonObject.class == targetType || JsonValue.class == targetType) {
            return (T) jsonValue;
        }
        if (JsonObject.class.isInstance(jsonValue)) {
            return (T) buildObject(
                    targetType, JsonObject.class.cast(jsonValue), applyObjectConverter,
                    null, skippedConverters);
        }
        if (JsonString.class.isInstance(jsonValue)) {
            if ((targetType == String.class || targetType == Object.class)) {
                return (T) JsonString.class.cast(jsonValue).getString();
            }
            if (targetType == Character.class || targetType == char.class) {
                final CharSequence string = JsonString.class.cast(jsonValue).getChars();
                if (string.length() == 1) {
                    return (T) Character.valueOf(string.charAt(0));
                }
                throw new IllegalArgumentException("Invalid Character binding"); // don't log the value (pwd case)
            }

            final Mappings.ClassMapping classMapping = mappings.getClassMapping(targetType);
            if (classMapping != null && classMapping.adapter != null) {
                return (T) classMapping.adapter.to(JsonString.class.cast(jsonValue).getString());
            }

            final Adapter adapter = findAdapter(targetType);
            if (adapter != null && TypeAwareAdapter.class.isInstance(adapter)) {
                final TypeAwareAdapter typeAwareAdapter = TypeAwareAdapter.class.cast(adapter);
                if (typeAwareAdapter.getTo() == String.class) {
                    return (T) adapter.to(JsonString.class.cast(jsonValue).getString());
                }
                if (typeAwareAdapter.getTo() == JsonString.class) {
                    return (T) adapter.to(JsonString.class.cast(jsonValue));
                }
                if (typeAwareAdapter.getTo() == CharSequence.class) {
                    return (T) adapter.to(JsonString.class.cast(jsonValue).getChars());
                }
            }
        }
        if (JsonNumber.class.isInstance(jsonValue)) {
            final JsonNumber number = JsonNumber.class.cast(jsonValue);
            if (targetType == int.class || targetType == Integer.class) {
                return (T) Integer.valueOf(number.intValue());
            }
            if (targetType == long.class || targetType == Long.class) {
                return (T) Long.valueOf(number.longValue());
            }
            if (targetType == double.class || targetType == Double.class || targetType == Object.class) {
                return (T) Double.valueOf(number.doubleValue());
            }
            if (targetType == float.class || targetType == Float.class) {
                return (T) Float.valueOf((float) number.doubleValue());
            }
            if (targetType == byte.class || targetType == Byte.class) {
                final int intValue = number.intValue();
                Validator.validateByte(intValue);
                return (T) Byte.valueOf((byte) intValue);
            }
            if (targetType == short.class || targetType == Short.class) {
                return (T) Short.valueOf((short) number.intValue());
            }
            if (targetType == BigDecimal.class || Number.class == targetType) {
                return (T) number.bigDecimalValue();
            }
            if (targetType == BigInteger.class) {
                return (T) number.bigIntegerValue();
            }
        }
        if (JsonArray.class.isInstance(jsonValue)) {

            JsonArray jsonArray = (JsonArray) jsonValue;

            if (Class.class.isInstance(targetType)) {
                final Class<?> asClass = (Class) targetType;
                if (asClass.isArray()) {
                    final Class componentType = asClass.getComponentType();
                    return (T) buildArrayWithComponentType(jsonArray, componentType, config.findAdapter(componentType),
                            isDedup() ? JsonPointerTracker.ROOT : null, Object.class);
                }
                if (Collection.class.isAssignableFrom(asClass)) {
                    return readObject(jsonValue, new JohnzonParameterizedType(asClass, Object.class), applyObjectConverter, skippedConverters);
                }
            }
            if (ParameterizedType.class.isInstance(targetType)) {

                final ParameterizedType pt = (ParameterizedType) targetType;
                final Mappings.CollectionMapping mapping = mappings.findCollectionMapping(pt, Object.class);
                if (mapping == null) {
                    throw new UnsupportedOperationException("type " + targetType + " not supported");
                }

                final Type arg = pt.getActualTypeArguments()[0];
                return (T) mapCollection(mapping, jsonArray, Class.class.isInstance(arg) ? config.findAdapter(Class.class.cast(arg)) : null,
                        null, isDedup() ? JsonPointerTracker.ROOT : null, Object.class);
            }
            if (Object.class == targetType) {
                return (T) new ArrayList(asList(Object[].class.cast(buildArrayWithComponentType(jsonArray, Object.class, null,
                        isDedup() ? JsonPointerTracker.ROOT : null, Object.class))));
            }
        }
        if (NULL == valueType) {
            return null;
        }
        if (TRUE == valueType && (Boolean.class == targetType || boolean.class == targetType || Object.class == targetType)) {
            return (T) Boolean.TRUE;
        }
        if (FALSE == valueType && (Boolean.class == targetType || boolean.class == targetType || Object.class == targetType)) {
            return (T) Boolean.FALSE;
        }

        final String snippet = config.getSnippet().of(jsonValue);
        final String description = ExceptionMessages.description(valueType);
        throw new IllegalArgumentException(targetType + " does not support " + description + ": " + snippet);
    }

    private boolean isDedup() {
        return jsonPointers != Collections.<String, Object>emptyMap();
    }

    private Object buildObject(final Type inType, final JsonObject object, final boolean applyObjectConverter,
                               final JsonPointerTracker jsonPointer, final Collection<Class<?>> skippedConverters) {
        final Type type = inType == Object.class ? new JohnzonParameterizedType(Map.class, String.class, Object.class) : inType;
        if (applyObjectConverter && !(type instanceof ParameterizedType)) {
            if (!(type instanceof Class)) {
                throw new MapperException("ObjectConverters are only supported for Classes not Types");
            }

            final Class clazz = (Class) type;
            if (skippedConverters == null || !skippedConverters.contains(clazz)) {
                ObjectConverter.Reader objectConverter = config.findObjectConverterReader(clazz);
                if (objectConverter != null) {
                    final Collection<Class<?>> skipped = skippedConverters == null ? new ArrayList<>() : skippedConverters;
                    skipped.add(clazz);
                    return objectConverter.fromJson(
                            object, type,
                            new SuppressConversionMappingParser(this, object, skipped));
                }
            }
        }

        if (config.getDeserializationPredicate() != null && Class.class.isInstance(inType)) {
            final Class<?> clazz = Class.class.cast(inType);
            if (config.getDeserializationPredicate().test(clazz) && object.containsKey(config.getDiscriminator())) {
                final String discriminator = object.getString(config.getDiscriminator());
                final Class<?> nestedType = config.getTypeLoader().apply(discriminator);
                if (nestedType != null && nestedType != inType) {
                    return buildObject(nestedType, object, applyObjectConverter, jsonPointer, skippedConverters);
                }
            }
        }
        final Mappings.ClassMapping classMapping = mappings.findOrCreateClassMapping(type);

        if (classMapping == null) {
            if (ParameterizedType.class.isInstance(type)) {
                final ParameterizedType aType = ParameterizedType.class.cast(type);
                final Type[] fieldArgTypes = aType.getActualTypeArguments();
                if (fieldArgTypes.length >= 2) {
                    final Class<?> raw = Class.class.cast(aType.getRawType());
                    final Map map;
                    if (SortedMap.class.isAssignableFrom(raw) || NavigableMap.class == raw || TreeMap.class == raw) {
                        map = config.getAttributeOrder() == null ? new TreeMap() : new TreeMap(config.getAttributeOrder());
                    } else if (ConcurrentMap.class.isAssignableFrom(raw)) {
                        map = new ConcurrentHashMap(object.size());
                    } else if (EnumMap.class.isAssignableFrom(raw)) {
                        if (!config.isSupportEnumContainerDeserialization()) {
                            throw new MapperException("JSON-B forbids EnumMap deserialization, " +
                                    "set supportEnumMapDeserialization=true to disable that arbitrary limitation");
                        }
                        map = new EnumMap(Class.class.cast(fieldArgTypes[0]));
                    } else if (Map.class.isAssignableFrom(raw)) {
                        map = new LinkedHashMap(object.size()); // todo: configurable from config.getNewDefaultMap()?
                    } else {
                        map = null;
                    }

                    if (map != null) {
                        final Type keyType = fieldArgTypes[0];
                        final boolean any = fieldArgTypes.length < 2 || fieldArgTypes[1] == Object.class;
                        for (final Map.Entry<String, JsonValue> value : object.entrySet()) {
                            final JsonValue jsonValue = value.getValue();
                            if (JsonNumber.class.isInstance(jsonValue) && any) {
                                map.put(value.getKey(), config.isUseBigDecimalForObjectNumbers() ?
                                        JsonNumber.class.cast(jsonValue).bigDecimalValue() : toNumberValue(JsonNumber.class.cast(jsonValue)));
                            } else if (JsonString.class.isInstance(jsonValue) && any) {
                                map.put(value.getKey(), JsonString.class.cast(jsonValue).getString());
                            } else {
                                map.put(convertTo(keyType, value.getKey()), toObject(null, jsonValue, fieldArgTypes[1], null, jsonPointer, Object.class));
                            }
                        }
                        return map;
                    }
                }
            } else if (Map.class == type || HashMap.class == type || LinkedHashMap.class == type) {
                final Map<String, Object> map = new LinkedHashMap<String, Object>();
                for (final Map.Entry<String, JsonValue> value : object.entrySet()) {
                    map.put(value.getKey(), toObject(null, value.getValue(), Object.class, null, jsonPointer, Object.class));
                }
                return map;
            }
        }
        if (classMapping == null) {
            final String snippet = config.getSnippet().of(object);
            final String description = ExceptionMessages.description(object);
            throw new MapperException("Unable to map " + description + " to " + type + ": " + snippet);
        }

        if (applyObjectConverter && classMapping.reader != null && (skippedConverters == null || !skippedConverters.contains(type))) {
            final Collection<Class<?>> skipped = skippedConverters == null ? new ArrayList<>() : skippedConverters;
            if (Class.class.isInstance(type)) { // more than likely, drop this check?
                skipped.add(Class.class.cast(type));
            }
            return classMapping.reader.fromJson(object, type, new SuppressConversionMappingParser(this, object, skipped));
        }
        /* doesn't work yet
        if (classMapping.adapter != null) {
            return classMapping.adapter.from(t);
        }
        */

        if (classMapping.factory == null) {
            throw new MissingFactoryException(classMapping.clazz, object, config.getSnippet().of(object));
        }
        if (config.isFailOnUnknown()) {
            if (!classMapping.setters.keySet().containsAll(object.keySet())) {
                throw new MapperException("(fail on unknown properties): " +
                        object.keySet().stream().filter(it -> !classMapping.setters.containsKey(it)).collect(joining(", ", "[", "]")));
            }
        }

        Object t;
        try {
            if (classMapping.factory.getParameterTypes() == null || classMapping.factory.getParameterTypes().length == 0) {
                t = classMapping.factory.create(null);
            } else {
                t = classMapping.factory.create(createParameters(classMapping, object, jsonPointer, e -> {
                    if (FactoryCreateException.class.isInstance(e)) {
                        throw FactoryCreateException.class.cast(e);
                    }
                    throw new FactoryCreateException(type, object, config.getSnippet().of(object), e);
                }));
            }
        } catch (final FactoryCreateException e){
            throw e;
        } catch (final Exception e) {
            throw new FactoryCreateException(type, object, config.getSnippet().of(object), e);
        }

        // store the new object under it's jsonPointer in case it gets referenced later
        if (jsonPointers == null) {
            if (classMapping.deduplicateObjects || config.isDeduplicateObjects()) {
                jsonPointers = new HashMap<>();
                jsonPointers.put(jsonPointer == null ? "/" : jsonPointer.toString(), t);
            } else {
                jsonPointers = Collections.emptyMap();
            }
        } else if (isDedup()) {
            jsonPointers.put(jsonPointer == null ? "/" : jsonPointer.toString(), t);
        }

        for (final Map.Entry<String, JsonValue> jsonEntry : object.entrySet()) {
            final Mappings.Setter value = classMapping.setters.get(jsonEntry.getKey());
            if (value == null) {
                continue;
            }

            final JsonValue jsonValue = jsonEntry.getValue();
            final JsonValue.ValueType valueType = jsonValue != null ? jsonValue.getValueType() : null;
            try {
                if (JsonValue.class == value.paramType) {
                    value.writer.write(t, jsonValue);
                    continue;
                }
                if (jsonValue == null) {
                    continue;
                }

                final AccessMode.Writer setterMethod = value.writer;
                if (NULL == valueType) { // forced
                    setterMethod.write(t, null);
                } else {
                    Object existingInstance = null;
                    if (config.isReadAttributeBeforeWrite()) {
                        final Mappings.Getter getter = classMapping.getters.get(jsonEntry.getKey());
                        if (getter != null) {
                            try {
                                existingInstance = getter.reader.read(t);
                            } catch (final RuntimeException re) {
                                // backward compatibility
                            }
                        }
                    }
                    final Object convertedValue = toValue(
                            existingInstance, jsonValue, value.converter, value.itemConverter,
                            value.paramType, value.objectConverter,
                            isDedup() ? new JsonPointerTracker(jsonPointer, jsonEntry.getKey()) : null, inType,
                            e -> {
                                if (SetterMappingException.class.isInstance(e)) {
                                    throw SetterMappingException.class.cast(e);
                                }
                                final String snippet = config.getSnippet().of(jsonValue);
                                throw new SetterMappingException(
                                        classMapping.clazz, jsonEntry.getKey(), value.writer.getType(), valueType, snippet, e);
                            });
                    if (convertedValue != null) {
                        setterMethod.write(t, convertedValue);
                    }
                }
            } catch (final SetterMappingException alreadyHandled) {
                throw alreadyHandled;
            } catch (final Exception e) {
                final String snippet = jsonValue == null? "null": config.getSnippet().of(jsonValue);
                throw new SetterMappingException(classMapping.clazz, jsonEntry.getKey(), value.writer.getType(), valueType, snippet, e);
            }
        }
        if (classMapping.anySetter != null) {
            for (final Map.Entry<String, JsonValue> entry : object.entrySet()) {
                final String key = entry.getKey();
                if (!classMapping.setters.containsKey(key)) {
                    try {
                        classMapping.anySetter.invoke(t, key,
                                toValue(null, entry.getValue(), null, null,
                                        classMapping.anySetter.getGenericParameterTypes()[1], null,
                                        isDedup() ? new JsonPointerTracker(jsonPointer, entry.getKey()) : null, type,
                                        MapperException::new));
                    } catch (final IllegalAccessException e) {
                        throw new IllegalStateException(e);
                    } catch (final InvocationTargetException e) {
                        throw new MapperException(e.getCause());
                    }
                }
            }
        } else if (classMapping.anyField != null) {
            try {
                classMapping.anyField.set(t, object.entrySet().stream()
                    .filter(it -> !classMapping.setters.containsKey(it.getKey()))
                    .collect(toMap(Map.Entry::getKey, e -> toValue(null, e.getValue(), null, null,
                            ParameterizedType.class.cast(classMapping.anyField.getGenericType()).getActualTypeArguments()[1], null,
                            isDedup() ? new JsonPointerTracker(jsonPointer, e.getKey()) : null, type,
                            MapperException::new))));
            } catch (final IllegalAccessException e) {
                throw new IllegalStateException(e);
            }
        }
        if (classMapping.mapAdder != null) {
            object.entrySet().stream()
                .filter(it -> !classMapping.setters.containsKey(it.getKey()))
                .filter(it -> it.getValue().getValueType() != NULL)
                .forEach(e -> {
                    final Object convertedValue = toValue(
                            null, e.getValue(), null, null,
                            classMapping.mapAdderType, null,
                            new JsonPointerTracker(jsonPointer, e.getKey()), inType,
                            MapperException::new);
                    if (convertedValue != null) {
                        try {
                            classMapping.mapAdder.invoke(t, e.getKey(), convertedValue);
                        } catch (final IllegalAccessException ex) {
                            throw new IllegalStateException(ex);
                        } catch (final InvocationTargetException ex) {
                            throw new MapperException(ex.getCause());
                        }
                    }
                });
        }
        return t;
    }

    private Number toNumberValue(JsonNumber jsonNumber) {
        if (jsonNumber.isIntegral()) {
            final int intValue = jsonNumber.intValue();
            final long longValue = jsonNumber.longValue();
            if (intValue == longValue) {
                return intValue;
            }
            return longValue;
        }
        if (config.isUseBigDecimalForFloats()) {
            return jsonNumber.bigDecimalValue();
        }
        return jsonNumber.doubleValue();
    }

    private Object convertTo(final Adapter converter, final JsonValue jsonValue, final JsonPointerTracker jsonPointer,
                             final Type targetType) {
        final JsonValue.ValueType valueType = jsonValue != null ? jsonValue.getValueType() : null;

        final AdapterKey key = getAdapterKey(converter);
        if (key != null && JsonValue.class == key.getTo()) {
            return converter.to(jsonValue);
        }

        if (JsonValue.ValueType.OBJECT == valueType) {
            if (JsonObject.class == key.getTo() || JsonStructure.class == key.getTo()) {
                return converter.to(jsonValue.asJsonObject());
            }
            final Object param;
            try {
                Type to = key.getTo();
                param = buildObject(to, JsonObject.class.cast(jsonValue), to instanceof Class, jsonPointer, getSkippedConverters());
            } catch (final Exception e) {
                throw new MapperException(e);
            }
            return converter.to(param);
        }

        if (NULL.equals(valueType)) {
            return null;
        }
        if (STRING.equals(valueType)) {
            if (key.getTo() == JsonString.class) {
                return converter.to(JsonString.class.cast(jsonValue));
            }
            return converter.to(JsonString.class.cast(jsonValue).getString());
        }
        if (TRUE.equals(valueType) || FALSE.equals(valueType)) {
            if (key != null) {
                if (boolean.class == key.getTo() || Boolean.class == key.getTo()) {
                    return converter.to(Boolean.parseBoolean(jsonValue.toString()));
                }
            }
        }
        if (NUMBER.equals(valueType)) {
            if (key != null) {
                if (Long.class == key.getTo() || long.class == key.getTo()) {
                    return converter.to(JsonNumber.class.cast(jsonValue).longValue());
                } else if (Integer.class == key.getTo() || int.class == key.getTo()) {
                    return converter.to(JsonNumber.class.cast(jsonValue).intValue());
                } else if (Double.class == key.getTo() || double.class == key.getTo()) {
                    return converter.to(JsonNumber.class.cast(jsonValue).doubleValue());
                } else if (Float.class == key.getTo() || float.class == key.getTo()) {
                    return converter.to(JsonNumber.class.cast(jsonValue).doubleValue());
                } else if (BigInteger.class == key.getTo()) {
                    return converter.to(JsonNumber.class.cast(jsonValue).bigIntegerValue());
                } else if (BigDecimal.class == key.getTo() ||  Number.class == key.getTo()) {
                    return converter.to(JsonNumber.class.cast(jsonValue).bigDecimalValue());
                } else if (JsonNumber.class == key.getTo()) {
                    return converter.to(JsonNumber.class.cast(jsonValue));
                }
            }
        }
        if (ARRAY.equals(valueType)) {
            if (JsonArray.class == key.getTo() || JsonStructure.class == key.getTo()) {
                return converter.to(jsonValue.asJsonObject());
            }
            if (TypeAwareAdapter.class.isInstance(converter)) {
                final TypeAwareAdapter adapter = TypeAwareAdapter.class.cast(converter);
                if (adapter.getFrom().equals(targetType)) {
                    return converter.to(readObject(jsonValue, adapter.getTo()));
                }
            }
            return buildArray(key.getTo(), jsonValue.asJsonArray(), null, null, jsonPointer, null);
        }
        return converter.to(jsonValue.toString());

    }

    private AdapterKey getAdapterKey(final Adapter converter) {
        AdapterKey adapterKey = config.getReverseAdapters().get(converter);

        if (adapterKey == null) {
            if (converter instanceof TypeAwareAdapter) {
                return TypeAwareAdapter.class.cast(converter).getKey();

            } else {
                Class<?> current = converter.getClass();
                while (current != null && current != Object.class) {
                    final Type[] types = current.getGenericInterfaces();
                    for (final Type t : types) {
                        if (!ParameterizedType.class.isInstance(t)) {
                            continue;
                        }
                        final ParameterizedType pt = ParameterizedType.class.cast(t);
                        if (Adapter.class == pt.getRawType()) {
                            final Type[] actualTypeArguments = pt.getActualTypeArguments();
                            adapterKey = new AdapterKey(actualTypeArguments[0], actualTypeArguments[1]);
                            config.getReverseAdapters().putIfAbsent(converter, adapterKey);
                            return adapterKey;
                        }
                    }
                    current = current.getSuperclass();
                }
            }
        }
        return adapterKey;
    }


    private Object toObject(final Object baseInstance, final JsonValue jsonValue,
                            final Type type, final Adapter itemConverter, final JsonPointerTracker jsonPointer,
                            final Type rootType) {
        if (jsonValue == null) {
            return null;
        }

        JsonValue.ValueType valueType = jsonValue.getValueType();
        if (JsonValue.ValueType.NULL == valueType) {
            return null;
        }

        if (type == Boolean.class || type == boolean.class) {
            if (JsonValue.ValueType.TRUE == valueType) {
                return true;
            }
            if (JsonValue.ValueType.FALSE == valueType) {
                return false;
            }
            final String snippet = config.getSnippet().of(jsonValue);
            final String description = ExceptionMessages.description(valueType);
            throw new MapperException("Unable to parse " + description + " to boolean: " + snippet);
        }

        if (config.isTreatByteArrayAsBase64() && jsonValue.getValueType() == JsonValue.ValueType.STRING && (type == byte[].class /*|| type == Byte[].class*/)) {
            return Base64.getDecoder().decode(((JsonString) jsonValue).getString());
        }
        if (config.isTreatByteArrayAsBase64URL() && jsonValue.getValueType() == JsonValue.ValueType.STRING && (type == byte[].class /*|| type == Byte[].class*/)) {
            return Base64.getUrlDecoder().decode(((JsonString) jsonValue).getString());
        }

        if (Object.class == type) { // handling specific types here to keep exception in standard handling
            if (JsonValue.ValueType.TRUE == valueType) {
                return true;
            }
            if (JsonValue.ValueType.FALSE == valueType) {
                return false;
            }
            if (JsonNumber.class.isInstance(jsonValue)) {
                return toNumberValue(JsonNumber.class.cast(jsonValue));
            }
            if (JsonString.class.isInstance(jsonValue)) {
                return JsonString.class.cast(jsonValue).getString();
            }
        }

        if (type == Character.class || type == char.class) {
            return convertTo(Class.class.cast(type), (JsonString.class.cast(jsonValue).getString()));
        }

        if (JsonObject.class.isInstance(jsonValue)) {
            if (JsonObject.class == type || JsonStructure.class == type || JsonValue.class == type) {
                return jsonValue;
            }
            final boolean typedAdapter = !ConverterAdapter.class.isInstance(itemConverter) && TypeAwareAdapter.class.isInstance(itemConverter);
            final Object object = buildObject(
                    baseInstance != null ? baseInstance.getClass() : (
                            typedAdapter ? TypeAwareAdapter.class.cast(itemConverter).getTo() : type),
                    JsonObject.class.cast(jsonValue), type instanceof Class,
                    jsonPointer, getSkippedConverters());
            return typedAdapter ? itemConverter.to(object) : object;
        } else if (JsonArray.class.isInstance(jsonValue)) {
            if (JsonArray.class == type || JsonStructure.class == type || JsonValue.class == type) {
                return jsonValue;
            }
            return buildArray(type, JsonArray.class.cast(jsonValue), itemConverter, null, jsonPointer, rootType);
        } else if (JsonNumber.class.isInstance(jsonValue)) {
            if (JsonNumber.class == type || JsonValue.class == type) {
                return jsonValue;
            }

            final JsonNumber number = JsonNumber.class.cast(jsonValue);

            if (type == Long.class || type == long.class) {
                return number.longValueExact();
            }

            if (type == Float.class || type == float.class) {
                return (float) number.doubleValue();
            }

            if (type == Double.class || type == double.class) {
                return number.doubleValue();
            }

            if (type == BigInteger.class) {
                return number.bigIntegerValue();
            }

            if (type == BigDecimal.class ||  Number.class == type) {
                return number.bigDecimalValue();
            }

            if (type == Integer.class || type == int.class) {
                return number.intValueExact();
            }

            if (type == Short.class || type == short.class) {
                final int intValue = number.intValue();
                short shortVal = (short) intValue;
                if (intValue != shortVal) {
                    throw new java.lang.ArithmeticException("Overflow");
                }
                return shortVal;
            }

            if (type == Byte.class || type == byte.class) {
                final int intValue = number.intValueExact();
                Validator.validateByte(intValue);
                return (byte) intValue;
            }

        } else if (JsonString.class.isInstance(jsonValue)) {
            if (JsonString.class == type || JsonValue.class == type) {
                return jsonValue;
            }

            final String string = JsonString.class.cast(jsonValue).getString();
            if (itemConverter == null) {
                // check whether we have a jsonPointer to a previously deserialised object
                if (isDedup() && !String.class.equals(type)) {
                    Object o = jsonPointers == null ? null : jsonPointers.get(string);
                    if (o != null) {
                        return o;
                    }
                }
                return convertTo(type, string);
            }
            return itemConverter.to(string);
        }

        final String snippet = config.getSnippet().of(jsonValue);
        final String description = ExceptionMessages.description(valueType);
        throw new MapperException("Unable to parse " + description + " to " + type + ": " + snippet);
    }

    private Object buildArray(final Type type, final JsonArray jsonArray, final Adapter itemConverter,
                              final ObjectConverter.Reader objectConverter,
                              final JsonPointerTracker jsonPointer, final Type rootType) {
        if (Class.class.isInstance(type)) {
            final Class clazz = Class.class.cast(type);
            if (clazz.isArray()) {
                final Class<?> componentType = clazz.getComponentType();
                return buildArrayWithComponentType(jsonArray, componentType, itemConverter, jsonPointer, rootType);
            }
            if (Collection.class.isAssignableFrom(clazz)) {
                final Mappings.CollectionMapping mapping = mappings.findCollectionMapping(
                        new JohnzonParameterizedType(clazz, Object.class), rootType);
                if (mapping != null) {
                    return mapCollection(mapping, jsonArray, itemConverter, objectConverter, jsonPointer, rootType);
                }
            }
        }

        if (ParameterizedType.class.isInstance(type)) {
            final ParameterizedType genericType = ParameterizedType.class.cast(type);
            if (Stream.class == genericType.getRawType()) {
                return Stream.of(1).flatMap(seed -> Collection.class.cast(buildArray(
                        new JohnzonParameterizedType(List.class, genericType.getActualTypeArguments()),
                        jsonArray, itemConverter, objectConverter, jsonPointer, rootType)).stream());
            }

            final Mappings.CollectionMapping mapping = mappings.findCollectionMapping(genericType, rootType);
            if (mapping != null) {
                return mapCollection(mapping, jsonArray, itemConverter, objectConverter, jsonPointer, rootType);
            }
        }

        if (GenericArrayType.class.isInstance(type)) {
            Type genericComponentType = GenericArrayType.class.cast(type).getGenericComponentType();
            while (ParameterizedType.class.isInstance(genericComponentType)) {
                genericComponentType = ParameterizedType.class.cast(genericComponentType).getRawType();
            }
            if (Class.class.isInstance(genericComponentType)) {
                return buildArrayWithComponentType(jsonArray, Class.class.cast(genericComponentType), itemConverter, jsonPointer, rootType);
            } // else: fail for now
        }

        if (Object.class == type) {
            return buildArray(ANY_LIST, jsonArray, null, null, jsonPointer, rootType);
        }

        // guess we don't want to map stream impls - keep it lazy since it is the only advantage to have streams there
        if (IntStream.class == type) {
            return Stream.of(1).flatMapToInt(seed -> IntStream.of(int[].class.cast(
                    buildArray(int[].class, jsonArray, null, null, jsonPointer, rootType))));
        }
        if (LongStream.class == type) {
            return Stream.of(1).flatMapToLong(seed -> LongStream.of(long[].class.cast(
                    buildArray(long[].class, jsonArray, null, null, jsonPointer, rootType))));
        }
        if (DoubleStream.class == type) {
            return Stream.of(1).flatMapToDouble(seed -> DoubleStream.of(double[].class.cast(
                    buildArray(double[].class, jsonArray, null, null, jsonPointer, rootType))));
        }

        throw new UnsupportedOperationException("type " + type + " not supported");
    }

    private Object buildArrayWithComponentType(final JsonArray jsonArray, final Class<?> componentType, final Adapter itemConverter,
                                               final JsonPointerTracker jsonPointer, final Type rootType) {

        if (boolean.class == componentType) {
            boolean[] array = new boolean[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                final Object object = toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                if (object == null) {
                    throw new IllegalStateException("json array mapped to boolean[] has null value at index " + i);
                }
                array[i] = (boolean) object;
                i++;
            }
            return array;
        }
        if (byte.class == componentType) {
            byte[] array = new byte[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                final Object object = toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                if (object == null) {
                    throw new IllegalStateException("json array mapped to byte[] has null value at index " + i);
                }
                array[i] = (byte) object;
                i++;
            }
            return array;
        }
        if (char.class == componentType) {
            char[] array = new char[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                final Object object = toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                if (object == null) {
                    throw new IllegalStateException("json array mapped to char[] has null value at index " + i);
                }
                array[i] = (char) object;
                i++;
            }
            return array;
        }
        if (short.class == componentType) {
            short[] array = new short[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                final Object object = toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                if (object == null) {
                    throw new IllegalStateException("json array mapped to short[] has null value at index " + i);
                }
                array[i] = (short) object;
                i++;
            }
            return array;
        }
        if (int.class == componentType) {
            int[] array = new int[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                final Object object = toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                if (object == null) {
                    throw new IllegalStateException("json array mapped to int[] has null value at index " + i);
                }
                array[i] = (int) object;
                i++;
            }
            return array;
        }
        if (long.class == componentType) {
            long[] array = new long[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                final Object object = toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                if (object == null) {
                    throw new IllegalStateException("json array mapped to long[] has null value at index " + i);
                }
                array[i] = (long) object;
                i++;
            }
            return array;
        }
        if (float.class == componentType) {
            float[] array = new float[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                final Object object = toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                if (object == null) {
                    throw new IllegalStateException("json array mapped to float[] has null value at index " + i);
                }
                array[i] = (float) object;
                i++;
            }
            return array;
        }
        if (double.class == componentType) {
            double[] array = new double[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                final Object object = toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                if (object == null) {
                    throw new IllegalStateException("json array mapped to double[] has null value at index " + i);
                }
                array[i] = (double) object;
                i++;
            }
            return array;
        }

        // wrapper types
        if (Boolean.class == componentType) {
            Boolean[] array = new Boolean[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                array[i] = (Boolean) toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                i++;
            }
            return array;
        }
        if (Byte.class == componentType) {
            Byte[] array = new Byte[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                array[i] = (Byte) toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                i++;
            }
            return array;
        }
        if (Character.class == componentType) {
            Character[] array = new Character[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                array[i] = (Character) toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                i++;
            }
            return array;
        }
        if (Short.class == componentType) {
            Short[] array = new Short[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                array[i] = (Short) toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                i++;
            }
            return array;
        }
        if (Integer.class == componentType) {
            Integer[] array = new Integer[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                array[i] = (Integer) toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                i++;
            }
            return array;
        }
        if (Long.class == componentType) {
            Long[] array = new Long[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                array[i] = (Long) toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                i++;
            }
            return array;
        }
        if (Float.class == componentType) {
            Float[] array = new Float[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                array[i] = (Float) toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                i++;
            }
            return array;
        }
        if (Double.class == componentType) {
            Double[] array = new Double[jsonArray.size()];
            int i = 0;
            for (final JsonValue value : jsonArray) {
                array[i] = (Double) toObject(null, value, componentType, itemConverter,
                        isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType);
                i++;
            }
            return array;
        }

        // for all the rest we have to rely on reflection :(
        final Object array = Array.newInstance(componentType, jsonArray.size());
        int i = 0;
        for (final JsonValue value : jsonArray) {
            Array.set(array, i, toObject(null, value, componentType, itemConverter,
                    isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType));
            i++;
        }
        return array;
    }

    private <T> Collection<T> mapCollection(final Mappings.CollectionMapping mapping, final JsonArray jsonArray,
                                            final Adapter itemConverter, ObjectConverter.Reader objectConverter,
                                            final JsonPointerTracker jsonPointer, final Type rootType) {
        final Collection collection;

        if (SortedSet.class == mapping.raw || NavigableSet.class == mapping.raw || TreeSet.class == mapping.raw) {
            collection = new TreeSet<T>();
        } else if (Set.class == mapping.raw || HashSet.class == mapping.raw) {
            collection = new HashSet<T>(jsonArray.size());
        } else if (Queue.class == mapping.raw || ArrayBlockingQueue.class == mapping.raw) {
            collection = new ArrayBlockingQueue<T>(jsonArray.size());
        } else if (List.class == mapping.raw || Collection.class == mapping.raw || ArrayList.class == mapping.raw || EnumSet.class == mapping.raw) {
            collection = new ArrayList<T>(jsonArray.size());
        } else if (LinkedHashSet.class == mapping.raw) {
            collection = new LinkedHashSet<T>(jsonArray.size());
        } else if (LinkedList.class == mapping.raw) {
            collection = new LinkedList<T>();
        } else if (Deque.class == mapping.raw || ArrayDeque.class == mapping.raw) {
            collection = new ArrayDeque(jsonArray.size());
        } else if (PriorityQueue.class == mapping.raw) {
            collection = new PriorityQueue(jsonArray.size());
        } else {
            throw new IllegalStateException("not supported collection type: " + mapping.raw.getName());
        }

        int i = 0;
        for (final JsonValue value : jsonArray) {
            collection.add(JsonValue.NULL.equals(value)
                    ? null
                    : toValue(null, value, null, itemConverter, mapping.arg, objectConverter,
                    isDedup() ? new JsonPointerTracker(jsonPointer, i) : null, rootType, MapperException::new));
            i++;
        }

        if (EnumSet.class == mapping.raw) {
            if (!config.isSupportEnumContainerDeserialization()) {
                throw new MapperException("Enum container deserialization disabled, " +
                        "set supportEnumContainerDeserialization=true to enable it");
            }
            if (collection.isEmpty()) {
                return EnumSet.noneOf(Class.class.cast(mapping.arg));
            } else if (collection.size() == 1) {
                return Collection.class.cast(EnumSet.of(Enum.class.cast(collection.iterator().next())));
            } else {
                final List<Enum> list = List.class.cast(collection);
                return Collection.class.cast(EnumSet.of(list.get(0), list.subList(1, list.size()).toArray(new Enum[list.size() - 1])));
            }
        }

        return collection;
    }


    private Object[] createParameters(final Mappings.ClassMapping mapping, final JsonObject object, JsonPointerTracker jsonPointer,
                                      final Function<Exception, RuntimeException> onException) {
        final int length = mapping.factory.getParameterTypes().length;
        final Object[] objects = new Object[length];

        for (int i = 0; i < length; i++) {
            final String paramName = mapping.factory.getParameterNames()[i];
            final Type parameterType = mapping.factory.getParameterTypes()[i];
            objects[i] = toValue(null,
                    object.get(paramName),
                    mapping.factory.getParameterConverter()[i],
                    mapping.factory.getParameterItemConverter()[i],
                    parameterType,
                    mapping.factory.getObjectConverter()[i],
                    isDedup() ? new JsonPointerTracker(jsonPointer, paramName) : null,
                    mapping.clazz, //X TODO ObjectConverter in @JohnzonConverter with Constructors!
                    onException);
            if (objects[i] == null) {
                objects[i] = getPrimitiveDefault(parameterType);
            }
        }

        return objects;
    }

    private Object toValue(final Object baseInstance, final JsonValue jsonValue, final Adapter converter,
                           final Adapter itemConverter, final Type type, final ObjectConverter.Reader objectConverter,
                           final JsonPointerTracker jsonPointer, final Type rootType,
                           final Function<Exception, RuntimeException> onException) {

        if (objectConverter != null) {
            return objectConverter.fromJson(jsonValue, type, this);
        }

        try {
            return converter == null ?
                    toObject(baseInstance, jsonValue, type, itemConverter, jsonPointer, rootType) :
                    convertTo(converter, jsonValue, jsonPointer, type);
        } catch (final Exception e) {
            if (e instanceof MapperException) {
                throw e;
            }
            throw onException.apply(e);
        }
    }


    /**
     * @deprecated see MapperConfig
     */
    private Object convertTo(final Type aClass, final String text) {
        if (Object.class == aClass || String.class == aClass) {
            return text;
        }
        final Adapter converter = findAdapter(aClass);
        Method method = valueOfs.get(aClass);
        if (method == null && Class.class.isInstance(aClass)) { // handle primitives
            final Class cast = Class.class.cast(aClass);
            try {
                method = cast.getMethod("valueOf", String.class);
                if (Modifier.isPublic(method.getModifiers()) && Modifier.isStatic(method.getModifiers())) {
                    valueOfs.putIfAbsent(cast, method);
                } else {
                    method = null;
                }
            } catch (final NoSuchMethodException e) {
                // if a real primitive (very unlikely) try the wrapper
                if (char.class == aClass) {
                    return CHARACTER_CONVERTER.fromString(text);
                }
                try {
                    return convertTo(Class.class.cast(cast.getField("TYPE").get(null)), text);
                } catch (final Exception e1) {
                    // no-op
                }
                // no-op
            }
        }
        if (method != null) {
            try {
                return method.invoke(null, text);
            } catch (final IllegalAccessException e) {
                throw new IllegalStateException(e);
            } catch (final InvocationTargetException e) {
                throw new MapperException(e.getCause());
            }
        }
        if (converter == null) {
            if (ParameterizedType.class.isInstance(aClass)) {
                return convertTo(ParameterizedType.class.cast(aClass).getRawType(), text);
            }
            throw new MapperException("Missing a Converter for type " + aClass + " to convert the JSON String '" +
                    text + "' . Please register a custom converter for it.");
        }
        return converter.to(text);
    }

    /**
     * @deprecated see MapperConfig - it is acually reversed so maybe not deprecated after all?
     */
    private Adapter findAdapter(final Type aClass) {
        if (config.getNoParserAdapterTypes().contains(aClass)) {
            return null;
        }
        final Adapter<?, ?> converter = config.getAdapters().get(new AdapterKey(aClass, String.class, true));
        if (converter != null) {
            return converter;
        }
        if (Class.class.isInstance(aClass)) {
            final Class<?> clazz = Class.class.cast(aClass);
            if (Enum.class.isAssignableFrom(clazz)) {
                final Adapter<?, ?> enumConverter = new ConverterAdapter(config.getEnumConverterFactory().apply(clazz), clazz);
                config.getAdapters().putIfAbsent(new AdapterKey(String.class, aClass), enumConverter);
                return enumConverter;
            }
        }
        final List<AdapterKey> matched = config.getAdapters().adapterKeys().stream()
                .filter(k -> k.isAssignableFrom(aClass))
                .collect(toList());
        if (matched.size() == 1) {
            final Adapter<?, ?> adapter = config.getAdapters().get(matched.iterator().next());
            if (TypeAwareAdapter.class.isInstance(adapter)) {
                config.getAdapters().put(new AdapterKey(aClass, TypeAwareAdapter.class.cast(adapter).getTo()), adapter);
            }
            return adapter;
        }
        config.getNoParserAdapterTypes().add(aClass);
        return null;
    }

    /**
     * Internal class to suppress {@link ObjectConverter} lookup if and only if
     * the {@link JsonValue} is the same refernece than the lookup was done before.
     */
    private static class SuppressConversionMappingParser implements MappingParser {
        private final MappingParserImpl delegate;
        private final JsonObject suppressConversionFor;
        private final Collection<Class<?>> skippedConverters;

        public SuppressConversionMappingParser(final MappingParserImpl delegate, final JsonObject suppressConversionFor,
                                               final Collection<Class<?>> skippedConverters) {
            this.delegate = delegate;
            this.suppressConversionFor = suppressConversionFor;
            this.skippedConverters = skippedConverters;
        }

        @Override
        public Collection<Class<?>> getSkippedConverters() {
            return skippedConverters;
        }

        @Override
        public <T> T readObject(final Type targetType) {
            return delegate.readObject(targetType);
        }

        @Override
        public <T> T readObject(final JsonValue jsonValue, final Type targetType) {
            final Collection<Class<?>> skippedConverters = getSkippedConverters();
            if (suppressConversionFor == jsonValue) {
                return delegate.readObject(jsonValue, targetType, false, skippedConverters);
            }
            final boolean useConverters = (Class.class.isInstance(targetType) &&
                    (skippedConverters == null || skippedConverters.stream().noneMatch(it -> it.isAssignableFrom(Class.class.cast(targetType))))) ||
                    ParameterizedType.class.isInstance(targetType);
            return delegate.readObject(jsonValue, targetType, useConverters, skippedConverters);
        }
    }
}
