/*
 *  Copyright 2001-present Stephen Colebourne
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.joda.beans.ser;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.joda.beans.JodaBeanUtils;
import org.joda.beans.MetaProperty;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;
import com.google.common.collect.Ordering;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.SortedMultiset;
import com.google.common.collect.Table;
import com.google.common.collect.Table.Cell;
import com.google.common.collect.TreeMultiset;

/**
 * Guava factory used to create wrappers around collection-like objects.
 */
public class GuavaSerIteratorFactory extends SerIteratorFactory {

    /**
     * Creates an iterator wrapper for a meta-property value.
     * 
     * @param value  the possible collection-like object, not null
     * @param prop  the meta-property defining the value, not null
     * @param beanClass  the class of the bean, not the meta-property, for better generics, not null
     * @return the iterator, null if not a collection-like type
     */
    @Override
    public SerIterator create(final Object value, final MetaProperty<?> prop, Class<?> beanClass) {
        Class<?> declaredType = prop.propertyType();
        if (value instanceof BiMap) {
            Class<?> keyType = JodaBeanUtils.mapKeyType(prop, beanClass);
            Class<?> valueType = JodaBeanUtils.mapValueType(prop, beanClass);
            List<Class<?>> valueTypeTypes = JodaBeanUtils.mapValueTypeTypes(prop, beanClass);
            return biMap((BiMap<?, ?>) value, declaredType, keyType, valueType, valueTypeTypes);
        }
        if (value instanceof Multiset) {
            Class<?> valueType = JodaBeanUtils.collectionType(prop, beanClass);
            List<Class<?>> valueTypeTypes = JodaBeanUtils.collectionTypeTypes(prop, beanClass);
            return multiset((Multiset<?>) value, declaredType, valueType, valueTypeTypes);
        }
        if (value instanceof Multimap) {
            Class<?> keyType = JodaBeanUtils.mapKeyType(prop, beanClass);
            Class<?> valueType = JodaBeanUtils.mapValueType(prop, beanClass);
            List<Class<?>> valueTypeTypes = JodaBeanUtils.mapValueTypeTypes(prop, beanClass);
            return multimap((Multimap<?, ?>) value, declaredType, keyType, valueType, valueTypeTypes);
        }
        if (value instanceof Table) {
            Class<?> rowType = JodaBeanUtils.extractTypeClass(prop, beanClass, 3, 0);
            Class<?> colType = JodaBeanUtils.extractTypeClass(prop, beanClass, 3, 1);
            Class<?> valueType = JodaBeanUtils.extractTypeClass(prop, beanClass, 3, 2);
            return table((Table<?, ?, ?>) value, declaredType, rowType, colType, valueType, EMPTY_VALUE_TYPES);
        }
        return super.create(value, prop, beanClass);
    }

    /**
     * Creates an iterator wrapper for a value retrieved from a parent iterator.
     * <p>
     * Allows the parent iterator to define the child iterator using generic type information.
     * This handles cases such as a {@code List} as the value in a {@code Map}.
     * 
     * @param value  the possible collection-like object, not null
     * @param parent  the parent iterator, not null
     * @return the iterator, null if not a collection-like type
     */
    @Override
    public SerIterator createChild(final Object value, final SerIterator parent) {
        Class<?> declaredType = parent.valueType();
        List<Class<?>> childGenericTypes = parent.valueTypeTypes();
        if (value instanceof BiMap) {
            if (childGenericTypes.size() == 2) {
                return biMap((BiMap<?, ?>) value, declaredType, childGenericTypes.get(0), childGenericTypes.get(1), EMPTY_VALUE_TYPES);
            }
            return biMap((BiMap<?, ?>) value, Object.class, Object.class, Object.class, EMPTY_VALUE_TYPES);
        }
        if (value instanceof Multimap) {
            if (childGenericTypes.size() == 2) {
                return multimap((Multimap<?, ?>) value, declaredType, childGenericTypes.get(0), childGenericTypes.get(1), EMPTY_VALUE_TYPES);
            }
            return multimap((Multimap<?, ?>) value, Object.class, Object.class, Object.class, EMPTY_VALUE_TYPES);
        }
        if (value instanceof Multiset) {
            if (childGenericTypes.size() == 1) {
                return multiset((Multiset<?>) value, declaredType, childGenericTypes.get(0), EMPTY_VALUE_TYPES);
            }
            return multiset((Multiset<?>) value, Object.class, Object.class, EMPTY_VALUE_TYPES);
        }
        if (value instanceof Table) {
            if (childGenericTypes.size() == 3) {
                return table((Table<?, ?, ?>) value, declaredType,
                        childGenericTypes.get(0), childGenericTypes.get(1),
                        childGenericTypes.get(2), EMPTY_VALUE_TYPES);
            }
            return table((Table<?, ?, ?>) value, Object.class, Object.class, Object.class, Object.class, EMPTY_VALUE_TYPES);
        }
        return super.createChild(value, parent);
    }

    //-----------------------------------------------------------------------
    /**
     * Creates an iterator wrapper for a meta-property value.
     * 
     * @param metaTypeDescription  the description of the collection type, not null
     * @param settings  the settings object, not null
     * @param knownTypes  the known types map, null if not using known type shortening
     * @return the iterator, null if not a collection-like type
     */
    @Override
    public SerIterable createIterable(final String metaTypeDescription, final JodaBeanSer settings, final Map<String, Class<?>> knownTypes) {
        if (metaTypeDescription.equals("BiMap")) {
            return biMap(Object.class, Object.class, EMPTY_VALUE_TYPES);
        }
        if (metaTypeDescription.equals("SetMultimap")) {
            return setMultimap(Object.class, Object.class, EMPTY_VALUE_TYPES);
        }
        if (metaTypeDescription.equals("ListMultimap")) {
            return listMultimap(Object.class, Object.class, EMPTY_VALUE_TYPES);
        }
        if (metaTypeDescription.equals("Multimap")) {
            return listMultimap(Object.class, Object.class, EMPTY_VALUE_TYPES);
        }
        if (metaTypeDescription.equals("Multiset")) {
            return multiset(Object.class, EMPTY_VALUE_TYPES);
        }
        if (metaTypeDescription.equals("Table")) {
            return table(Object.class, Object.class, Object.class, EMPTY_VALUE_TYPES);
        }
        return super.createIterable(metaTypeDescription, settings, knownTypes);
    }

    /**
     * Creates an iterator wrapper for a meta-property value.
     * 
     * @param prop  the meta-property defining the value, not null
     * @param beanClass  the class of the bean, not the meta-property, for better generics, not null
     * @return the iterator, null if not a collection-like type
     */
    @Override
    public SerIterable createIterable(final MetaProperty<?> prop, Class<?> beanClass) {
        if (BiMap.class.isAssignableFrom(prop.propertyType())) {
            Class<?> keyType = defaultToObjectClass(JodaBeanUtils.mapKeyType(prop, beanClass));
            Class<?> valueType = defaultToObjectClass(JodaBeanUtils.mapValueType(prop, beanClass));
            List<Class<?>> valueTypeTypes = JodaBeanUtils.mapValueTypeTypes(prop, beanClass);
            return biMap(keyType, valueType, valueTypeTypes);
        }
        if (SortedMultiset.class.isAssignableFrom(prop.propertyType())) {
            Class<?> valueType = defaultToObjectClass(JodaBeanUtils.collectionType(prop, beanClass));
            List<Class<?>> valueTypeTypes = JodaBeanUtils.collectionTypeTypes(prop, beanClass);
            return sortedMultiset(valueType, valueTypeTypes);
        }
        if (Multiset.class.isAssignableFrom(prop.propertyType())) {
            Class<?> valueType = defaultToObjectClass(JodaBeanUtils.collectionType(prop, beanClass));
            List<Class<?>> valueTypeTypes = JodaBeanUtils.collectionTypeTypes(prop, beanClass);
            return multiset(valueType, valueTypeTypes);
        }
        if (SetMultimap.class.isAssignableFrom(prop.propertyType())) {
            Class<?> keyType = defaultToObjectClass(JodaBeanUtils.mapKeyType(prop, beanClass));
            Class<?> valueType = defaultToObjectClass(JodaBeanUtils.mapValueType(prop, beanClass));
            List<Class<?>> valueTypeTypes = JodaBeanUtils.mapValueTypeTypes(prop, beanClass);
            return setMultimap(keyType, valueType, valueTypeTypes);
        }
        if (ListMultimap.class.isAssignableFrom(prop.propertyType())) {
            Class<?> keyType = defaultToObjectClass(JodaBeanUtils.mapKeyType(prop, beanClass));
            Class<?> valueType = defaultToObjectClass(JodaBeanUtils.mapValueType(prop, beanClass));
            List<Class<?>> valueTypeTypes = JodaBeanUtils.mapValueTypeTypes(prop, beanClass);
            return listMultimap(keyType, valueType, valueTypeTypes);
        }
        if (Multimap.class.isAssignableFrom(prop.propertyType())) {
            Class<?> keyType = defaultToObjectClass(JodaBeanUtils.mapKeyType(prop, beanClass));
            Class<?> valueType = defaultToObjectClass(JodaBeanUtils.mapValueType(prop, beanClass));
            List<Class<?>> valueTypeTypes = JodaBeanUtils.mapValueTypeTypes(prop, beanClass);
            return listMultimap(keyType, valueType, valueTypeTypes);
        }
        if (Table.class.isAssignableFrom(prop.propertyType())) {
            Class<?> rowType = defaultToObjectClass(JodaBeanUtils.extractTypeClass(prop, beanClass, 3, 0));
            Class<?> colType = defaultToObjectClass(JodaBeanUtils.extractTypeClass(prop, beanClass, 3, 1));
            Class<?> valueType = defaultToObjectClass(JodaBeanUtils.extractTypeClass(prop, beanClass, 3, 2));
            return table(rowType, colType, valueType, EMPTY_VALUE_TYPES);
        }
        if (ImmutableList.class.isAssignableFrom(prop.propertyType())) {
            Class<?> valueType = JodaBeanUtils.collectionType(prop, beanClass);
            List<Class<?>> valueTypeTypes = JodaBeanUtils.collectionTypeTypes(prop, beanClass);
            return immutableList(valueType, valueTypeTypes);
        }
        if (ImmutableSortedSet.class.isAssignableFrom(prop.propertyType())) {
            Class<?> valueType = JodaBeanUtils.collectionType(prop, beanClass);
            List<Class<?>> valueTypeTypes = JodaBeanUtils.collectionTypeTypes(prop, beanClass);
            return immutableSortedSet(valueType, valueTypeTypes);
        }
        if (ImmutableSet.class.isAssignableFrom(prop.propertyType())) {
            Class<?> valueType = JodaBeanUtils.collectionType(prop, beanClass);
            List<Class<?>> valueTypeTypes = JodaBeanUtils.collectionTypeTypes(prop, beanClass);
            return immutableSet(valueType, valueTypeTypes);
        }
        if (ImmutableSortedMap.class.isAssignableFrom(prop.propertyType())) {
            Class<?> keyType = JodaBeanUtils.mapKeyType(prop, beanClass);
            Class<?> valueType = JodaBeanUtils.mapValueType(prop, beanClass);
            List<Class<?>> valueTypeTypes = JodaBeanUtils.mapValueTypeTypes(prop, beanClass);
            return immutableSortedMap(keyType, valueType, valueTypeTypes);
        }
        if (ImmutableMap.class.isAssignableFrom(prop.propertyType())) {
            Class<?> keyType = JodaBeanUtils.mapKeyType(prop, beanClass);
            Class<?> valueType = JodaBeanUtils.mapValueType(prop, beanClass);
            List<Class<?>> valueTypeTypes = JodaBeanUtils.mapValueTypeTypes(prop, beanClass);
            return immutableMap(keyType, valueType, valueTypeTypes);
        }
        return super.createIterable(prop, beanClass);
    }

    //-----------------------------------------------------------------------
    /**
     * Gets an iterable wrapper for {@code BiMap}.
     * 
     * @param keyType  the value type, not null
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterable, not null
     */
    public static final SerIterable biMap(final Class<?> keyType, final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        final BiMap<Object, Object> map = HashBiMap.create();
        return map(keyType, valueType, valueTypeTypes, map);
    }

    /**
     * Gets an iterable wrapper for {@code Multiset}.
     * 
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterable, not null
     */
    public static final SerIterable multiset(final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        final Multiset<Object> coll = HashMultiset.create();
        return multiset(valueType, valueTypeTypes, coll);
    }

    /**
     * Gets an iterable wrapper for {@code SortedMultiset}.
     * 
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterable, not null
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public static final SerIterable sortedMultiset(final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        Ordering natural = Ordering.natural();
        final SortedMultiset<Object> coll = TreeMultiset.create(natural);
        return multiset(valueType, valueTypeTypes, coll);
    }

    private static SerIterable multiset(final Class<?> valueType, final List<Class<?>> valueTypeTypes, final Multiset<Object> coll) {
        return new SerIterable() {
            @Override
            public SerIterator iterator() {
                return multiset(coll, Object.class, valueType, valueTypeTypes);
            }
            @Override
            public void add(Object key, Object column, Object value, int count) {
                if (key != null) {
                    throw new IllegalArgumentException("Unexpected key");
                }
                coll.add(value, count);
            }
            @Override
            public Object build() {
                return coll;
            }
            @Override
            public SerCategory category() {
                return SerCategory.COUNTED;
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
        };
    }

    /**
     * Gets an iterator wrapper for {@code Multiset}.
     * 
     * @param multiset  the collection, not null
     * @param declaredType  the declared type, not null
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterator, not null
     */
    @SuppressWarnings("rawtypes")
    public static final SerIterator multiset(
            final Multiset<?> multiset, final Class<?> declaredType, final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        return new SerIterator() {
            private final Iterator it = multiset.entrySet().iterator();
            private Multiset.Entry current;

            @Override
            public String metaTypeName() {
                return "Multiset";
            }
            @Override
            public boolean metaTypeRequired() {
                return Multiset.class.isAssignableFrom(declaredType) == false;
            }
            @Override
            public SerCategory category() {
                return SerCategory.COUNTED;
            }
            @Override
            public int size() {
                return multiset.entrySet().size();
            }
            @Override
            public boolean hasNext() {
                return it.hasNext();
            }
            @Override
            public void next() {
                current = (Multiset.Entry) it.next();
            }
            @Override
            public int count() {
                return current.getCount();
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
            @Override
            public Object value() {
                return current.getElement();
            }
        };
    }

    //-----------------------------------------------------------------------
    /**
     * Gets an iterable wrapper for {@code ListMultimap}.
     * 
     * @param keyType  the key type, not null
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterable, not null
     */
    public static final SerIterable listMultimap(final Class<?> keyType, final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        final ListMultimap<Object, Object> map = ArrayListMultimap.create();
        return new SerIterable() {
            @Override
            public SerIterator iterator() {
                return multimap(map, Object.class, keyType, valueType, valueTypeTypes);
            }
            @Override
            public void add(Object key, Object column, Object value, int count) {
                if (key == null) {
                    throw new IllegalArgumentException("Missing key");
                }
                if (count != 1) {
                    throw new IllegalArgumentException("Unexpected count");
                }
                map.put(key, value);
            }
            @Override
            public Object build() {
                return map;
            }
            @Override
            public SerCategory category() {
                return SerCategory.MAP;
            }
            @Override
            public Class<?> keyType() {
                return keyType;
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
        };
    }

    /**
     * Gets an iterable wrapper for {@code SetMultimap}.
     * 
     * @param keyType  the key type, not null
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterable, not null
     */
    public static final SerIterable setMultimap(final Class<?> keyType, final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        final SetMultimap<Object, Object> map = HashMultimap.create();
        return new SerIterable() {
            @Override
            public SerIterator iterator() {
                return multimap(map, Object.class, keyType, valueType, valueTypeTypes);
            }
            @Override
            public void add(Object key, Object column, Object value, int count) {
                if (key == null) {
                    throw new IllegalArgumentException("Missing key");
                }
                if (count != 1) {
                    throw new IllegalArgumentException("Unexpected count");
                }
                map.put(key, value);
            }
            @Override
            public Object build() {
                return map;
            }
            @Override
            public SerCategory category() {
                return SerCategory.MAP;
            }
            @Override
            public Class<?> keyType() {
                return keyType;
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
        };
    }

    /**
     * Gets an iterator wrapper for {@code Multimap}.
     * 
     * @param map  the collection, not null
     * @param declaredType  the declared type, not null
     * @param keyType  the key type, not null
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterator, not null
     */
    @SuppressWarnings("rawtypes")
    public static final SerIterator multimap(
            final Multimap<?, ?> map, final Class<?> declaredType,
            final Class<?> keyType, final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        return new SerIterator() {
            private final Iterator it = map.entries().iterator();
            private Map.Entry current;

            @Override
            public String metaTypeName() {
                if (map instanceof SetMultimap) {
                    return "SetMultimap";
                }
                if (map instanceof ListMultimap) {
                    return "ListMultimap";
                }
                return "Multimap";
            }
            @Override
            public boolean metaTypeRequired() {
                if (map instanceof SetMultimap) {
                    return SetMultimap.class.isAssignableFrom(declaredType) == false;
                }
                if (map instanceof ListMultimap) {
                    return ListMultimap.class.isAssignableFrom(declaredType) == false;
                }
                return Multimap.class.isAssignableFrom(declaredType) == false;
            }
            @Override
            public SerCategory category() {
                return SerCategory.MAP;
            }
            @Override
            public int size() {
                return map.size();
            }
            @Override
            public boolean hasNext() {
                return it.hasNext();
            }
            @Override
            public void next() {
                current = (Map.Entry) it.next();
            }
            @Override
            public Class<?> keyType() {
                return keyType;
            }
            @Override
            public Object key() {
                return current.getKey();
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
            @Override
            public Object value() {
                return current.getValue();
            }
        };
    }

    //-----------------------------------------------------------------------
    /**
     * Gets an iterable wrapper for {@code Table}.
     * 
     * @param rowType  the row type, not null
     * @param colType  the column type, not null
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterable, not null
     */
    public static final SerIterable table(final Class<?> rowType, final Class<?> colType, final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        final Table<Object, Object, Object> table = HashBasedTable.create();
        return new SerIterable() {
            @Override
            public SerIterator iterator() {
                return table(table, Object.class, rowType, colType, valueType, valueTypeTypes);
            }
            @Override
            public void add(Object row, Object column, Object value, int count) {
                if (row == null) {
                    throw new IllegalArgumentException("Missing row");
                }
                if (column == null) {
                    throw new IllegalArgumentException("Missing column");
                }
                if (count != 1) {
                    throw new IllegalArgumentException("Unexpected count");
                }
                table.put(row, column, value);
            }
            @Override
            public Object build() {
                return table;
            }
            @Override
            public SerCategory category() {
                return SerCategory.TABLE;
            }
            @Override
            public Class<?> keyType() {
                return rowType;
            }
            @Override
            public Class<?> columnType() {
                return colType;
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
        };
    }

    /**
     * Gets an iterator wrapper for {@code Table}.
     * 
     * @param table  the collection, not null
     * @param declaredType  the declared type, not null
     * @param rowType  the row type, not null
     * @param colType  the col type, not null
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterator, not null
     */
    @SuppressWarnings("rawtypes")
    public static final SerIterator table(
            final Table<?, ?, ?> table, final Class<?> declaredType,
            final Class<?> rowType, final Class<?> colType, final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        return new SerIterator() {
            private final Iterator it = table.cellSet().iterator();
            private Cell current;

            @Override
            public String metaTypeName() {
                return "Table";
            }
            @Override
            public boolean metaTypeRequired() {
                return Table.class.isAssignableFrom(declaredType) == false;
            }
            @Override
            public SerCategory category() {
                return SerCategory.TABLE;
            }
            @Override
            public int size() {
                return table.size();
            }
            @Override
            public boolean hasNext() {
                return it.hasNext();
            }
            @Override
            public void next() {
                current = (Cell) it.next();
            }
            @Override
            public Class<?> keyType() {
                return rowType;
            }
            @Override
            public Object key() {
                return current.getRowKey();
            }
            @Override
            public Class<?> columnType() {
                return colType;
            }
            @Override
            public Object column() {
                return current.getColumnKey();
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
            @Override
            public Object value() {
                return current.getValue();
            }
        };
    }

    /**
     * Gets an iterator wrapper for {@code BiMap}.
     * 
     * @param map  the collection, not null
     * @param declaredType  the declared type, not null
     * @param keyType  the value type, not null
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterator, not null
     */
    @SuppressWarnings("rawtypes")
    public static final SerIterator biMap(
            final BiMap<?, ?> map, final Class<?> declaredType,
            final Class<?> keyType, final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        return new SerIterator() {
            private final Iterator it = map.entrySet().iterator();
            private Entry current;

            @Override
            public String metaTypeName() {
                return "BiMap";
            }
            @Override
            public boolean metaTypeRequired() {
                // hack around Guava annoyance by assuming that size 0 and 1 ImmutableBiMap
                // was actually meant to be an ImmutableMap
                if ((declaredType == Map.class || declaredType == ImmutableMap.class) && map.size() < 2) {
                    return false;
                }
                return BiMap.class.isAssignableFrom(declaredType) == false;
            }
            @Override
            public SerCategory category() {
                return SerCategory.MAP;
            }
            @Override
            public int size() {
                return map.size();
            }
            @Override
            public boolean hasNext() {
                return it.hasNext();
            }
            @Override
            public void next() {
                current = (Entry) it.next();
            }
            @Override
            public Class<?> keyType() {
                return keyType;
            }
            @Override
            public Object key() {
                return current.getKey();
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
            @Override
            public Object value() {
                return current.getValue();
            }
        };
    }

    /**
     * Gets an iterable wrapper for {@code ImmutableList}.
     *
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterable, not null
     */
    public static final SerIterable immutableList(
            final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        final List<Object> coll = new ArrayList<>();
        return new SerIterable() {
            @Override
            public SerIterator iterator() {
                return collection(coll, Object.class, valueType, valueTypeTypes);
            }
            @Override
            public void add(Object key, Object column, Object value, int count) {
                if (key != null) {
                    throw new IllegalArgumentException("Unexpected key");
                }
                for (int i = 0; i < count; i++) {
                    coll.add(value);
                }
            }
            @Override
            public Object build() {
                return ImmutableList.copyOf(coll);
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
        };
    }

    /**
     * Gets an iterable wrapper for {@code ImmutableSortedSet}.
     *
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterable, not null
     */
    public static final SerIterable immutableSortedSet(
            final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        final Set<Object> coll = new LinkedHashSet<>();
        return new SerIterable() {
            @Override
            public SerIterator iterator() {
                return collection(coll, Object.class, valueType, valueTypeTypes);
            }
            @Override
            public void add(Object key, Object column, Object value, int count) {
                if (key != null) {
                    throw new IllegalArgumentException("Unexpected key");
                }
                for (int i = 0; i < count; i++) {
                    coll.add(value);
                }
            }
            @Override
            public Object build() {
                return ImmutableSortedSet.copyOf(coll);
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
        };
    }

    /**
     * Gets an iterable wrapper for {@code ImmutableSet}.
     *
     * @param valueType  the value type, not null
     * @param valueTypeTypes  the generic parameters of the value type
     * @return the iterable, not null
     */
    public static final SerIterable immutableSet(
            final Class<?> valueType, final List<Class<?>> valueTypeTypes) {
        final Set<Object> coll = new LinkedHashSet<>();
        return new SerIterable() {
            @Override
            public SerIterator iterator() {
                return collection(coll, Object.class, valueType, valueTypeTypes);
            }
            @Override
            public void add(Object key, Object column, Object value, int count) {
                if (key != null) {
                    throw new IllegalArgumentException("Unexpected key");
                }
                for (int i = 0; i < count; i++) {
                    coll.add(value);
                }
            }
            @Override
            public Object build() {
                return ImmutableSet.copyOf(coll);
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
        };
    }

    static SerIterable immutableMap(
            final Class<?> keyType, final Class<?> valueType,
            final List<Class<?>> valueTypeTypes) {
        final Map<Object, Object> map = new LinkedHashMap<>();
        return new SerIterable() {
            @Override
            public SerIterator iterator() {
                return map(map, Object.class, keyType, valueType, valueTypeTypes);
            }
            @Override
            public void add(Object key, Object column, Object value, int count) {
                if (key == null) {
                    throw new IllegalArgumentException("Missing key");
                }
                if (count != 1) {
                    throw new IllegalArgumentException("Unexpected count");
                }
                map.put(key, value);
            }
            @Override
            public Object build() {
                return ImmutableMap.copyOf(map);
            }
            @Override
            public SerCategory category() {
                return SerCategory.MAP;
            }
            @Override
            public Class<?> keyType() {
                return keyType;
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
        };
    }

    static SerIterable immutableSortedMap(
            final Class<?> keyType, final Class<?> valueType,
            final List<Class<?>> valueTypeTypes) {
        final Map<Object, Object> map = new LinkedHashMap<>();
        return new SerIterable() {
            @Override
            public SerIterator iterator() {
                return map(map, Object.class, keyType, valueType, valueTypeTypes);
            }
            @Override
            public void add(Object key, Object column, Object value, int count) {
                if (key == null) {
                    throw new IllegalArgumentException("Missing key");
                }
                if (count != 1) {
                    throw new IllegalArgumentException("Unexpected count");
                }
                map.put(key, value);
            }
            @Override
            public Object build() {
                return ImmutableSortedMap.copyOf(map);
            }
            @Override
            public SerCategory category() {
                return SerCategory.MAP;
            }
            @Override
            public Class<?> keyType() {
                return keyType;
            }
            @Override
            public Class<?> valueType() {
                return valueType;
            }
            @Override
            public List<Class<?>> valueTypeTypes() {
                return valueTypeTypes;
            }
        };
    }

}
