package act.db;

/*-
 * #%L
 * ACT Framework
 * %%
 * Copyright (C) 2014 - 2017 ActFramework
 * %%
 * 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.
 * #L%
 */

import act.Act;
import act.app.App;
import act.plugin.AppServicePlugin;
import act.util.SimpleBean;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.osgl.$;
import org.osgl.Osgl;
import org.osgl.exception.NotAppliedException;
import org.osgl.inject.BeanSpec;
import org.osgl.inject.Injector;
import org.osgl.util.C;
import org.osgl.util.E;
import org.osgl.util.S;
import org.osgl.util.ValueObject;

import java.beans.Transient;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * The `AdaptiveRecord` interface specifies a special {@link Model} in that
 * the fields/columns could be implicitly defined by database
 */
public interface AdaptiveRecord<ID_TYPE, MODEL_TYPE extends AdaptiveRecord> extends Model<ID_TYPE, MODEL_TYPE> {

    Map<String, Object> internalMap();

    /**
     * Add or replace a key/val pair into the active record
     *
     * @param key the key
     * @param val the value
     * @return the active record instance
     */
    MODEL_TYPE putValue(String key, Object val);

    /**
     * Merge a key/val pair in the active record.
     *
     * If the key specified does not exists then insert the key/val pair into the record.
     *
     * If there are existing key/val pair then merge it with the new one:
     *
     * 1. if the val is simple type or cannot be merged, then replace the existing value with new value
     * 2. if the val can be merged, e.g. it is a POJO or another adaptive record, then merge the new value into the old value. Merge shall happen recursively
     *
     * @param key the key
     * @param val the value
     * @return the active record instance
     */
    MODEL_TYPE mergeValue(String key, Object val);

    /**
     * Add all key/val pairs from specified kv map into this active record
     *
     * @param kvMap the key/value pairs
     * @return this active record instance
     */
    MODEL_TYPE putValues(Map<String, Object> kvMap);

    /**
     * Merge all key/val pairs from specified kv map into this active record
     *
     * @param kvMap the key/value pairs
     * @return this active record instance
     * @see #mergeValue(String, Object)
     */
    MODEL_TYPE mergeValues(Map<String, Object> kvMap);

    /**
     * Get value from the active record by key specified
     *
     * @param key the key
     * @param <T> the generic type of the value
     * @return the value or `null` if not found
     */
    <T> T getValue(String key);

    /**
     * Export the key/val pairs from this active record into a map
     *
     * @return the exported map contains all key/val pairs stored in this active record
     */
    Map<String, Object> toMap();

    /**
     * Get the size of the data stored in the active record
     *
     * @return the active record size
     */
    int size();

    /**
     * Check if the active records has a value associated with key specified
     *
     * @param key the key
     * @return `true` if there is value associated with the key in the record, or `false` otherwise
     */
    boolean containsKey(String key);

    /**
     * Returns a set of keys that has value stored in the active record
     *
     * @return the key set
     */
    Set<String> keySet();

    /**
     * Returns a set of entries stored in the active record
     *
     * @return the entry set
     */
    Set<Map.Entry<String, Object>> entrySet();

    /**
     * Returns a set of entries stored in the active record. For
     * field entries, use the field filter specified to check
     * if it needs to be added into the return set
     *
     * @param fieldFilter the function that returns `true` or `false` for
     *                    bean spec of a certain field declared in the class
     * @return the entry set with field filter applied
     */
    Set<Map.Entry<String, Object>> entrySet($.Function<BeanSpec, Boolean> fieldFilter);

    /**
     * Returns a Map typed object backed by this active record
     *
     * @return a Map backed by this active record
     */
    Map<String, Object> asMap();

    /**
     * Returns the meta info of this AdaptiveRecord
     *
     * @return AdaptiveRecord meta info
     */
    @Transient
    MetaInfo metaInfo();

    class Util {

        public static <MODEL_TYPE extends AdaptiveRecord> MODEL_TYPE putValue(MODEL_TYPE ar, String key, Object val) {
            Map<String, Object> kv = ar.internalMap();
            $.Func2 setter = ar.metaInfo().fieldSetters.get(key);
            if (null != setter) {
                setter.apply(ar, val);
            } else {
                kv.put(key, val);
            }
            return ar;
        }

        public static <MODEL_TYPE extends AdaptiveRecord> MODEL_TYPE mergeValue(MODEL_TYPE ar, String key, Object val) {
            Map<String, Object> kv = ar.internalMap();
            $.Func2 merger = ar.metaInfo().fieldMergers.get(key);
            if (null != merger) {
                merger.apply(ar, val);
            } else {
                Object v0 = kv.get(key);
                kv.put(key, AdaptiveRecord.MetaInfo.merge(v0, val));
            }
            return ar;
        }

        public static <MODEL_TYPE extends AdaptiveRecord, T> T getValue(MODEL_TYPE ar, String key) {
            Map<String, Object> kv = ar.internalMap();
            $.Function getter = ar.metaInfo().fieldGetters.get(key);
            if (null != getter) {
                return (T) getter.apply(ar);
            }
            return (T) kv.get(key);
        }

        public static <MODEL_TYPE extends AdaptiveRecord> MODEL_TYPE putValues(MODEL_TYPE ar, Map<String, Object> map) {
            for (Map.Entry<String, Object> entry: map.entrySet()) {
                String key = entry.getKey();
                if ("id".equals(key)) {
                    continue;
                }
                ar.putValue(entry.getKey(), entry.getValue());
            }
            return ar;
        }

        public static <MODEL_TYPE extends AdaptiveRecord> MODEL_TYPE mergeValues(MODEL_TYPE ar, Map<String, Object> map) {
            for (Map.Entry<String, Object> entry: map.entrySet()) {
                ar.mergeValue(entry.getKey(), entry.getValue());
            }
            return ar;
        }

        public static <MODEL_TYPE extends AdaptiveRecord, T> boolean containsKey(MODEL_TYPE ar, String key) {
            Map<String, Object> kv = ar.internalMap();
            return kv.containsKey(key) || ar.metaInfo().getterFieldSpecs.containsKey(key);
        }

        public static Map<String, Object> toMap(final AdaptiveRecord ar) {
            Map<String, Object> kv = ar.internalMap();
            Map<String, Object> map = new HashMap<>(kv);
            for (Map.Entry<String, $.Function> entry : ar.metaInfo().fieldGetters.entrySet()) {
                map.put(entry.getKey(), entry.getValue().apply(ar));
            }
            return map;
        }

        private static int fieldsSize(final AdaptiveRecord ar) {
            return ar.metaInfo().getterFieldSpecs.size();
        }

        public static int size(final AdaptiveRecord ar) {
            Map<String, Object> kv = ar.internalMap();
            return kv.size() + fieldsSize(ar);
        }

        private static boolean hasFields(AdaptiveRecord ar) {
            return !ar.metaInfo().getterFieldSpecs.isEmpty();
        }


        public static Set<String> keySet(AdaptiveRecord ar) {
            Map<String, Object> kv = ar.internalMap();
            if (!hasFields(ar)) {
                return kv.keySet();
            }
            Set<String> set = new HashSet<String>(ar.metaInfo().getterFieldSpecs.keySet());
            set.addAll(kv.keySet());
            return set;
        }

        public static Set<Map.Entry<String, Object>> entrySet(AdaptiveRecord ar, Osgl.Function<BeanSpec, Boolean> function) {
            Map<String, Object> kv = ar.internalMap();
            if (!hasFields(ar)) {
                return kv.entrySet();
            }
            Set<Map.Entry<String, Object>> set = new HashSet<Map.Entry<String, Object>>(kv.entrySet());
            AdaptiveRecord.MetaInfo metaInfo = ar.metaInfo();
            boolean filter = null != function;
            for (Map.Entry<String, $.Function> entry: metaInfo.fieldGetters.entrySet()) {
                String fieldName = entry.getKey();
                if ("kv".equals(fieldName)) {
                    continue;
                }
                if (filter) {
                    BeanSpec field = metaInfo.getterFieldSpecs.get(fieldName);
                    if (!function.apply(field)) {
                        continue;
                    }
                }
                $.Function getter = entry.getValue();
                set.add(new C.Map.Entry(fieldName, getter.apply(ar)));
            }
            return set;
        }


        public static Map<String, Object> asMap(final AdaptiveRecord ar) {
            final Map<String, Object> kv = ar.internalMap();
            // TODO: should we check the field value on size, remove, containsXxx etc methods?
            return new Map<String, Object>() {
                @Override
                public int size() {
                    return ar.size();
                }

                @Override
                public boolean isEmpty() {
                    return ar.size() == 0;
                }

                @Override
                public boolean containsKey(Object key) {
                    return kv.containsKey(key) || ar.metaInfo().getterFieldSpecs.containsKey(key);
                }

                @Override
                public boolean containsValue(Object value) {
                    return kv.containsValue(value);
                }

                @Override
                public Object get(Object key) {
                    $.Function getter = ar.metaInfo().fieldGetters.get(key);
                    return null != getter ? getter.apply(ar) : kv.get((String)key);
                }

                @Override
                public Object put(String key, Object value) {
                    $.Func2 setter = ar.metaInfo().fieldSetters.get(key);
                    if (null != setter) {
                        Object o = get(key);
                        setter.apply(ar, value);
                        return o;
                    }
                    return kv.put(key, value);
                }

                @Override
                public Object remove(Object key) {
                    $.Function getter = ar.metaInfo().fieldGetters.get(key);
                    if (null != getter) {
                        return null;
                    } else {
                        return kv.remove(key);
                    }
                }

                @Override
                public void putAll(Map<? extends String, ?> m) {
                    ar.putValues((Map)m);
                }

                @Override
                public void clear() {
                    kv.clear();
                    // TODO: should we clear field values?
                }

                @Override
                public Set<String> keySet() {
                    return ar.keySet();
                }

                @Override
                public Collection<Object> values() {
                    List<Object> list = new ArrayList<Object>();
                    list.addAll(kv.values());
                    for ($.Function getter : ar.metaInfo().fieldGetters.values()) {
                        list.add(getter.apply(ar));
                    }
                    return list;
                }

                @Override
                public Set<Entry<String, Object>> entrySet() {
                    return ar.entrySet();
                }
            };
        }

        public static AdaptiveRecord.MetaInfo generateMetaInfo(AdaptiveRecord ar) {
            AdaptiveRecord.MetaInfo.Repository r = Act.appServicePluginManager().get(AdaptiveRecord.MetaInfo.Repository.class);
            return r.get(ar.getClass(), new $.Transformer<Class<? extends AdaptiveRecord>, AdaptiveRecord.MetaInfo>() {
                @Override
                public AdaptiveRecord.MetaInfo transform(Class<? extends AdaptiveRecord> aClass) {
                    return new AdaptiveRecord.MetaInfo(aClass);
                }
            });
        }
    }

    class MetaInfo {
        private Class<? extends AdaptiveRecord> arClass;
        public String className;
        public Map<String, BeanSpec> getterFieldSpecs;
        public Map<String, Class> getterFieldClasses;
        public Map<String, BeanSpec> setterFieldSpecs;
        public Map<String, Class> setterFieldClasses;
        public Map<String, $.Function> fieldGetters;
        public Map<String, $.Func2> fieldSetters;
        public Map<String, $.Func2> fieldMergers;
        private SimpleBean.MetaInfo metaInfo;

        public MetaInfo(Class<? extends AdaptiveRecord> clazz) {
            this.className = clazz.getName();
            this.metaInfo = Act.app().classLoader().simpleBeanInfoManager().get(className);
            this.arClass = clazz;
            this.discoverProperties(clazz);
        }

        @Deprecated
        public Class fieldClass(String fieldName) {
            Class clazz = setterFieldClasses.get(fieldName);
            return null == clazz ? getterFieldClasses.get(fieldName) : clazz;
        }

        public Class getterFieldClass(String fieldName) {
            return getterFieldClasses.get(fieldName);
        }

        public Class setterFieldClass(String fieldName) {
            return setterFieldClasses.get(fieldName);
        }

        public Type getterFieldType(String fieldName) {
            BeanSpec spec = getterFieldSpecs.get(fieldName);
            return null == spec ? null : spec.type();
        }

        public Type setterFieldType(String fieldName) {
            BeanSpec spec = setterFieldSpecs.get(fieldName);
            return null == spec ? null : spec.type();
        }

        private void discoverProperties(Class<? extends AdaptiveRecord> clazz) {
            getterFieldSpecs = new HashMap<>();
            getterFieldClasses = new HashMap<>();
            setterFieldSpecs = new HashMap<>();
            setterFieldClasses = new HashMap<>();
            fieldGetters = new HashMap<>();
            fieldSetters = new HashMap<>();
            fieldMergers = new HashMap<>();
            Injector injector = Act.app().injector();
            for (final Method m : clazz.getMethods()) {
                String name = propertyName(m);
                String alias;
                final boolean hasAlias;
                if (S.blank(name)) {
                    continue;
                } else {
                    name = S.lowerFirst(name);
                    alias = null == metaInfo ? name : metaInfo.aliasOf(name);
                    hasAlias = name != alias;
                    if ("idAsStr".equals(name)) {
                        // special case for MorphiaModel
                        continue;
                    }
                }
                final Class returnClass = m.getReturnType();
                Type returnType = m.getGenericReturnType();
                Class paramClass = null;
                Type paramType = null;
                Class[] params = m.getParameterTypes();
                Type[] paramTypes = m.getGenericParameterTypes();
                if (null != params && params.length == 1) {
                    paramClass = params[0];
                    paramType = paramTypes[0];
                }
                Class fieldClass = null == paramClass ? returnClass : paramClass;
                Type fieldType = null == paramType ? returnType : paramType;
                if (!(fieldType instanceof ParameterizedType)) {
                    fieldType = fieldClass;
                }
                if (null == paramClass) {
                    BeanSpec spec = BeanSpec.of(fieldType, m.getDeclaredAnnotations(), name, injector);
                    getterFieldSpecs.put(name, spec);
                    getterFieldClasses.put(name, fieldClass);
                    if (hasAlias) {
                        getterFieldSpecs.put(alias, spec);
                        getterFieldClasses.put(alias, fieldClass);
                    }
                } else {
                    BeanSpec existingSpec = setterFieldSpecs.get(name);
                    if (null == existingSpec && hasAlias) {
                        existingSpec = setterFieldSpecs.get(alias);
                    }
                    if (null != existingSpec) {
                        // we need to infer the type from field in this case
                        Field field = $.fieldOf(clazz, name, true);
                        if (null != field) {
                            BeanSpec spec = BeanSpec.of(field, injector);
                            setterFieldSpecs.put(name, spec);
                            setterFieldClasses.put(name, field.getType());
                            if (hasAlias) {
                                setterFieldSpecs.put(alias, spec);
                                setterFieldClasses.put(alias, field.getType());
                            }
                        } else {
                            if (fieldClass == Object.class) {
                                // ignore
                            } else if (existingSpec.rawType() == Object.class) {
                                BeanSpec spec = BeanSpec.of(fieldType, m.getDeclaredAnnotations(), name, injector);
                                setterFieldSpecs.put(name, spec);
                                setterFieldClasses.put(name, fieldClass);
                                if (hasAlias) {
                                    setterFieldSpecs.put(alias, spec);
                                    setterFieldClasses.put(alias, fieldClass);
                                }
                            }
                        }
                    } else {
                        BeanSpec spec = BeanSpec.of(fieldType, m.getDeclaredAnnotations(), name, injector);
                        setterFieldSpecs.put(name, spec);
                        setterFieldClasses.put(name, fieldClass);
                        if (hasAlias) {
                            setterFieldSpecs.put(alias, spec);
                            setterFieldClasses.put(alias, fieldClass);
                        }
                    }
                }
                if (null != paramClass) {
                    final String fieldName = name;
                    $.Func2 fn = new Osgl.Func2() {
                        @Override
                        public Object apply(Object host, Object value) throws NotAppliedException, Osgl.Break {
                            BeanSpec spec = setterFieldSpecs.get(fieldName);
                            if (null != value && !spec.isInstance(value)) {
                                if (value instanceof String) {
                                    value = Act.app().resolverManager().resolve((String)value, spec.rawType());
                                } else if (value instanceof JSONObject) {
                                    value = JSON.parseObject(((JSONObject) value).toJSONString(), spec.rawType());
                                }
                            }
                            $.invokeVirtual(host, m, value);
                            return null;
                        }
                    };
                    fieldSetters.put(name, fn);
                    if (hasAlias) {
                        fieldSetters.put(alias, fn);
                    }
                    fn = new Osgl.Func2() {
                        @Override
                        public Object apply(Object host, Object value) throws NotAppliedException, Osgl.Break {
                            BeanSpec spec = setterFieldSpecs.get(fieldName);
                            if (null != value && !spec.isInstance(value)) {
                                if (value instanceof String) {
                                    value = Act.app().resolverManager().resolve((String)value, spec.rawType());
                                }
                            }
                            $.Function getter = fieldGetters.get(fieldName);
                            if (null == getter) {
                                $.invokeVirtual(host, m, value);
                                return null;
                            }
                            Object value0 = getter.apply(host);
                            value = merge(value0, value);
                            $.invokeVirtual(host, m, value);
                            return null;
                        }
                    };
                    fieldMergers.put(name, fn);
                    if (hasAlias) {
                        fieldMergers.put(alias, fn);
                    }
                } else {
                    $.F1 fn = new Osgl.F1() {
                        @Override
                        public Object apply(Object host) throws NotAppliedException, Osgl.Break {
                            return $.invokeVirtual(host, m);
                        }
                    };
                    fieldGetters.put(name, fn);
                    if (hasAlias) {
                        fieldGetters.put(alias, fn);
                    }
                }
            }
        }

        private String propertyName(Method m) {
            String name = m.getName();
            if ("getClass".equals(name)) {
                return null;
            }
            Type[] paramTypes = m.getGenericParameterTypes();
            if (name.startsWith("set") && void.class == m.getReturnType() && null != paramTypes && paramTypes.length == 1) {
                return name.substring(3);
            }
            boolean isGet = name.startsWith("get");
            boolean isIs = name.startsWith("is");
            if ((isGet || isIs) && void.class != m.getReturnType() && (null == paramTypes || paramTypes.length == 0)) {
                return isGet ? name.substring(3) : name.substring(2);
            }
            return null;
        }

        public static Object merge(Object to, Object from) {
            if (null == to) {
                return from;
            }
            if (null == from) {
                return to;
            }
            if (canBeMerged(to.getClass())) {
                return _merge(to, from);
            }
            return from;
        }

        private static Object _merge(Object to, Object from) {
            if (to instanceof ValueObject) {
                if (from instanceof ValueObject) {
                    return ValueObject.of(merge(((ValueObject) to).value(), ((ValueObject) from).value()));
                }
                return ValueObject.of(merge(((ValueObject) to).value(), from));
            }
            if (to instanceof AdaptiveRecord) {
                AdaptiveRecord ar = (AdaptiveRecord) to;
                return mergeIntoAdaptiveRecord(ar, from);
            }
            if (to instanceof Map) {
                Map map = (Map) to;
                return mergeIntoMap(map, from);
            }
            if (to instanceof Set) {
                Set set = (Set) to;
                return mergeIntoSet(set, from);
            }
            if (to instanceof List) {
                List list = (List) to;
                return mergeIntoList(list, from);
            }
            if (to.getClass().isArray()) {
                List list = new ArrayList();
                int len = Array.getLength(to);
                for (int i = 0; i < len; ++i) {
                    list.add(Array.get(to, i));
                }
                List list1 = mergeIntoList(list, from);
                int sz1 = list1.size();
                Object a1 = Array.newInstance(to.getClass().getComponentType(), sz1);
                for (int i = 0; i < sz1; ++i) {
                    Array.set(a1, i, list1.get(i));
                }
                return a1;
            }
            return mergeIntoPojo(to, from);
        }

        private static String getterName(Field field) {
            boolean isBoolean = field.getType() == Boolean.class || field.getType() == boolean.class;
            return (isBoolean ? "is" : "get") + S.capFirst(field.getName());
        }

        private static String setterName(Field field) {
            return "set" + S.capFirst(field.getName());
        }

        private static boolean canBeMerged(Class<?> c) {
            return !($.isSimpleType(c) || isDateType(c));
        }

        private static boolean isDateType(Class<?> c) {
            String name = c.getSimpleName();
            return (name.endsWith("Date") || name.endsWith("DateTime") || name.endsWith("Calendar"));
        }

        private static AdaptiveRecord mergeIntoAdaptiveRecord(AdaptiveRecord ar, Object value) {
            if (value instanceof Map) {
                return mergeMapIntoAdaptiveRecord(ar, (Map) value);
            }
            if (value instanceof AdaptiveRecord) {
                return mergeMapIntoAdaptiveRecord(ar, ((AdaptiveRecord) value).asMap());
            }
            List<Field> fields = $.fieldsOf(value.getClass(), true);
            for (Field f : fields) {
                f.setAccessible(true);
                try {
                    String fn = f.getName();
                    Object fv = f.get(value);
                    Object o0 = ar.getValue(fn);
                    ar.putValue(fn, merge(o0, fv));
                } catch (IllegalAccessException e) {
                    throw E.unexpected(e, "error merging into adaptive record");
                }
            }
            return ar;
        }

        private static AdaptiveRecord mergeMapIntoAdaptiveRecord(AdaptiveRecord ar, Map map) {
            return ar.putValues(map);
        }

        private static Map mergeIntoMap(Map map, Object value) {
            if (value instanceof Map) {
                return mergeMapIntoMap(map, (Map) value);
            }
            if (value instanceof AdaptiveRecord) {
                return mergeMapIntoMap(map, ((AdaptiveRecord) value).asMap());
            }
            Map retval = new HashMap(map);
            List<Field> fields = $.fieldsOf(value.getClass(), true);
            for (Field f : fields) {
                f.setAccessible(true);
                try {
                    String fn = f.getName();
                    Object fv = f.get(value);
                    Object o0 = map.get(fn);
                    retval.put(fn, merge(o0, fv));
                } catch (IllegalAccessException e) {
                    throw E.unexpected(e, "error merging into adaptive record");
                }
            }
            return retval;
        }

        private static Map mergeMapIntoMap(Map m0, Map<?, ?> m1) {
            Map retval = (Map) Act.injector().get(m0.getClass());
            retval.putAll(m0);
            for (Map.Entry entry : m1.entrySet()) {
                Object k = entry.getKey();
                Object v = entry.getValue();
                retval.put(k, merge(m0.get(k), v));
            }
            return retval;
        }

        private static Set mergeIntoSet(Set set, Object value) {
            if (value instanceof Collection) {
                return mergeCollectionIntoSet(set, (Collection) value);
            }
            if (value.getClass().isArray()) {
                List list = new ArrayList();
                int len = Array.getLength(value);
                for (int i = 0; i < len; ++i) {
                    list.add(Array.get(value, i));
                }
                return mergeCollectionIntoSet(set, list);
            }
            throw new IllegalArgumentException("Cannot merge " + value.getClass() + " into Set");
        }

        private static Set mergeCollectionIntoSet(Set set, Collection col) {
            Set set1 = (Set) Act.injector().get(set.getClass());
            set1.addAll(col);
            return set1;
        }

        private static List mergeIntoList(List list, Object value) {
            if (value instanceof Set) {
                return mergeSetIntoList(list, (Set) value);
            }
            if (value instanceof List) {
                return mergeListIntoList(list, (List) value);
            }
            if (value.getClass().isArray()) {
                List list0 = new ArrayList();
                int len = Array.getLength(value);
                for (int i = 0; i < len; ++i) {
                    list0.add(Array.get(value, i));
                }
                return mergeListIntoList(list, list0);
            }
            throw new IllegalArgumentException("Cannot merge " + value.getClass() + " into List");
        }

        private static List mergeSetIntoList(List list, Set set) {
            List retval = (List) Act.injector().get(list.getClass());
            retval.addAll(list);
            retval.addAll(set);
            return retval;
        }

        private static List mergeListIntoList(List to, List from) {
            List retval = (List) Act.injector().get(to.getClass());
            int szTo = to.size();
            int szFrom = from.size();
            int szMin = Math.min(szTo, szFrom);
            for (int i = 0; i < szMin; ++i) {
                Object o0 = to.get(i);
                Object o1 = from.get(i);
                retval.add(merge(o0, o1));
            }
            if (szTo > szFrom) {
                for (int i = szMin; i < szTo; ++i) {
                    retval.add(to.get(i));
                }
            } else if (szFrom > szTo) {
                for (int i = szMin; i < szFrom; ++i) {
                    retval.add(from.get(i));
                }
            }
            return retval;
        }

        private static Object mergeIntoPojo(Object o0, Object o1) {
            if (o1 instanceof Map) {
                return mergeMapIntoPojo(o0, (Map) o1);
            }
            if (o1 instanceof AdaptiveRecord) {
                return mergeMapIntoPojo(o0, ((AdaptiveRecord) o1).asMap());
            }
            if (o1 instanceof Collection || o1.getClass().isArray()) {
                throw E.unexpected("cannot merge " + o1.getClass() + " into " + o0.getClass());
            }
            Class<?> c0 = o0.getClass();
            List<Field> fields = $.fieldsOf(o1.getClass(), true);
            for (Field f : fields) {
                f.setAccessible(true);
                try {
                    String fn = f.getName();
                    Field f0 = $.fieldOf(c0, fn);
                    if (null == f0) {
                        continue;
                    }
                    Object fv = f.get(o1);
                    f0.setAccessible(true);
                    Object fv0 = merge(f0.get(o0), fv);
                    f0.set(o0, fv0);
                } catch (IllegalAccessException e) {
                    throw E.unexpected(e, "error merging into POJO");
                }
            }
            return o0;
        }

        private static Object mergeMapIntoPojo(Object o0, Map map) {
            List<Field> fields = $.fieldsOf(o0.getClass(), true);
            for (Field f : fields) {
                String fn = f.getName();
                if (map.containsKey(fn)) {
                    f.setAccessible(true);
                    try {
                        Object v = f.get(o0);
                        f.set(o0, merge(v, map.get(fn)));
                    } catch (IllegalAccessException e) {
                        throw E.unexpected(e, "error merging into POJO");
                    }
                }
            }
            return o0;
        }

        public static class Repository extends AppServicePlugin {
            @Override
            protected void applyTo(App app) {
            }

            private ConcurrentMap<Class<?>, MetaInfo> map = new ConcurrentHashMap<>();

            public MetaInfo get(Class<? extends AdaptiveRecord> clazz, $.Function<Class<? extends AdaptiveRecord>, MetaInfo> factory) {
                MetaInfo info = map.get(clazz);
                if (null == info) {
                    MetaInfo theInfo = factory.apply(clazz);
                    info = map.putIfAbsent(clazz, theInfo);
                    if (null == info) {
                        info = theInfo;
                    }
                }
                return info;
            }
        }
    }

}
