All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.nextop.WireValue Maven / Gradle / Ivy

The newest version!
package io.nextop;

import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.collect.*;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import io.nextop.org.apache.commons.codec.binary.Base64OutputStream;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.TransformedMap;

import javax.annotation.Nullable;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.WritableByteChannel;
import java.util.*;


// TODO
// wire value v2:
// - fixed size index (23-bit)
// - incremental headers
// -- lru maintainenace to keep within a fixed size
// -- store the compressed value in the lru map
// -- never compress int* of float*
// - use ByteBuffer in API instead of byte[]

// more efficient codec than text, that allows a lossless (*or nearly, for floats) conversion to text when needed
// like protobufs/bson but focused on easy conversion to text
// conversion of text is important when constructing HTTP requests, which are isSend-based
// TODO do compression of values and objects keysets by having a header in the front. up to the 127 that save the most bytes to compress
// TODO long term the compression table can be stateful, so transferring the same objects repeatedly will have a savings, like a dynamic protobuf
// TODO wire value even in a simple form will increase speed between cloud and client, because binary will be less data and faster to parse on client
// TODO can always call toString to get JSON out (if type is array or object)

// FIXME look at sizes if the lut is stateful, and the lut didn't need to be resent each time

// FIXME memory and compressed wire values should have same hashcode and equals


// FIXME everywhere replace byte[] with ByteBuffer

public abstract class WireValue {
    // just use int constants here

    // FIXME MESSAGE, IMAGE(orientation=capture_front,capture_back,binary; representation as bytes+encoding) and NULL should be WireValue top-level types
    public static enum Type {
        UTF8,
        BLOB,
        INT32,
        INT64,
        FLOAT32,
        FLOAT64,
        BOOLEAN,
        MAP,
        LIST,

        // FIXME ID here

        // FIXME
        MESSAGE,
        // FIXME
        IMAGE,
        // FIXME
        NULL

    }





    // FIXME parse
//    public static WireValue of(byte[] bytes, int offset, int n) {
//
//        // expect compressed format here
//
//    }
//





    // FIXME rename to "fromBytes"
    public static WireValue valueOf(ByteBuffer bb) {
        int n = bb.remaining();
        byte[] bytes = new byte[n];
        bb.get(bytes, 0, n);
        return valueOf(bytes, 0);
    }

    // FIXME rename to "fromBytes"
    public static WireValue valueOf(byte[] bytes) {
        return valueOf(bytes, 0);
    }

    public static WireValue valueOf(byte[] bytes, int offset) {
        int h = 0xFF & bytes[offset];
        if ((h & H_COMPRESSED) == H_COMPRESSED) {
            int nb = h & ~H_COMPRESSED;
            int size = getint(bytes, offset + 1);
            // next 4 is size in bytes
            int[] offsets = new int[size + 1];
            offsets[0] = offset + 9;
            if (0 < size) {
                for (int i = 1; i <= size; ++i) {
                    offsets[i] = offsets[i - 1] + _byteSize(bytes, offsets[i - 1]);
                }
            }


            CompressionState cs = new CompressionState(bytes, offset, offsets, nb);
            return valueOf(bytes, offsets[size], cs);
        }
        return valueOf(bytes, offset, null);
    }


    static WireValue valueOf(byte[] bytes, int offset, CompressionState cs) {
        int h = 0xFF & bytes[offset];
        if ((h & H_COMPRESSED) == H_COMPRESSED) {
            int luti = h & ~H_COMPRESSED;
            for (int i = 1; i < cs.nb; ++i) {
                luti = (luti << 8) | (0xFF & bytes[offset + i]);
            }
            return valueOf(cs.header, cs.offsets[luti], cs);
        }

        switch (h) {
            case H_UTF8:
                return new CUtf8WireValue(bytes, offset, cs);
            case H_BLOB:
                return new CBlobWireValue(bytes, offset, cs);
            case H_INT32:
                return new CInt32WireValue(bytes, offset, cs);
            case H_INT64:
                return new CInt64WireValue(bytes, offset, cs);
            case H_FLOAT32:
                return new CFloat32WireValue(bytes, offset, cs);
            case H_FLOAT64:
                return new CFloat64WireValue(bytes, offset, cs);
            case H_TRUE_BOOLEAN:
                return new BooleanWireValue(true);
            case H_FALSE_BOOLEAN:
                return new BooleanWireValue(false);
            case H_MAP:
                return new CMapWireValue(bytes, offset, cs);
            case H_LIST:
                return new CListWireValue(bytes, offset, cs);
            case H_INT32_LIST:
//                return new CInt32ListWireValue(bytes, offset, cs);
                // FIXME see listh
                throw new IllegalArgumentException();
            case H_INT64_LIST:
//                return new CInt64ListWireValue(bytes, offset, cs);
                // FIXME see listh
                throw new IllegalArgumentException();
            case H_FLOAT32_LIST:
//                return new CFloat32ListWireValue(bytes, offset, cs);
                // FIXME see listh
                throw new IllegalArgumentException();
            case H_FLOAT64_LIST:
//                return new CFloat64ListWireValue(bytes, offset, cs);
                // FIXME see listh
                throw new IllegalArgumentException();
            case H_NULL:
                return new NullWireValue();
            case H_MESSAGE:
                return new CMessageWireValue(bytes, offset, cs);
            case H_IMAGE:
                return new CImageWireValue(bytes, offset, cs);
            default:
                throw new IllegalArgumentException("" + h);
        }
    }


    // based on byte[] and views into the byte[] (parsing does not expand into a bunch of objects in memory)
    private static abstract class CompressedWireValue extends WireValue {
        byte[] bytes;
        int offset;
        CompressionState cs;


        CompressedWireValue(Type type, byte[] bytes, int offset, CompressionState cs) {
            super(type);
            this.bytes = bytes;
            this.offset = offset;
            this.cs = cs;
        }


    }


    private static class CUtf8WireValue extends CompressedWireValue{
        CUtf8WireValue(byte[] bytes, int offset, CompressionState cs) {
            super(Type.UTF8, bytes, offset, cs);
        }

        @Override
        public String asString() {
            int length = getint(bytes, offset + 1);
            return new String(bytes, offset + 5, length, Charsets.UTF_8);
        }
        @Override
        public int asInt() {
            throw new UnsupportedOperationException();
        }
        @Override
        public long asLong() {
            throw new UnsupportedOperationException();
        }
        @Override
        public float asFloat() {
            throw new UnsupportedOperationException();
        }
        @Override
        public double asDouble() {
            throw new UnsupportedOperationException();
        }
        @Override
        public boolean asBoolean() {
            throw new UnsupportedOperationException();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class CBlobWireValue extends CompressedWireValue{
        CBlobWireValue(byte[] bytes, int offset, CompressionState cs) {
            super(Type.BLOB, bytes, offset, cs);
        }

        @Override
        public String asString() {
            throw new UnsupportedOperationException();
        }
        @Override
        public int asInt() {
            throw new UnsupportedOperationException();
        }
        @Override
        public long asLong() {
            throw new UnsupportedOperationException();
        }
        @Override
        public float asFloat() {
            throw new UnsupportedOperationException();
        }
        @Override
        public double asDouble() {
            throw new UnsupportedOperationException();
        }
        @Override
        public boolean asBoolean() {
            throw new UnsupportedOperationException();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            int length = getint(bytes, offset + 1);
            return ByteBuffer.wrap(bytes, offset + 5, length);
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }


    private static class CMapWireValue extends CompressedWireValue{
        CMapWireValue(byte[] bytes, int offset, CompressionState cs) {
            super(Type.MAP, bytes, offset, cs);
        }

        @Override
        public String asString() {
            throw new UnsupportedOperationException();
        }
        @Override
        public int asInt() {
            throw new UnsupportedOperationException();
        }
        @Override
        public long asLong() {
            throw new UnsupportedOperationException();
        }
        @Override
        public float asFloat() {
            throw new UnsupportedOperationException();
        }
        @Override
        public double asDouble() {
            throw new UnsupportedOperationException();
        }
        @Override
        public boolean asBoolean() {
            throw new UnsupportedOperationException();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            Map map = new AbstractMap() {
                int ds = offset + 5;
                List keys = valueOf(bytes, ds, cs).asList();
                List values = valueOf(bytes, ds + byteSize(bytes, ds, cs), cs).asList();
                int n = keys.size();

                @Override
                public Set> entrySet() {
                    return new AbstractSet>() {
                        @Override
                        public int size() {
                            return n;
                        }
                        @Override
                        public Iterator> iterator() {
                            return new Iterator>() {
                                int i = 0;

                                @Override
                                public boolean hasNext() {
                                    return i < n;
                                }

                                @Override
                                public Entry next() {
                                    int j = i++;
                                    return new SimpleImmutableEntry(keys.get(j), values.get(j));
                                }

                                @Override
                                public void remove() {
                                    throw new UnsupportedOperationException();
                                }
                            };
                        }
                    };
                }
            };
            return duckMap(map);
        }
        @Override
        public ByteBuffer asBlob() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class CListWireValue extends CompressedWireValue {
        CListWireValue(byte[] bytes, int offset, CompressionState cs) {
            super(Type.LIST, bytes, offset, cs);
        }

        @Override
        public String asString() {
            throw new UnsupportedOperationException();
        }
        @Override
        public int asInt() {
            throw new UnsupportedOperationException();
        }
        @Override
        public long asLong() {
            throw new UnsupportedOperationException();
        }
        @Override
        public float asFloat() {
            throw new UnsupportedOperationException();
        }
        @Override
        public double asDouble() {
            throw new UnsupportedOperationException();
        }
        @Override
        public boolean asBoolean() {
            throw new UnsupportedOperationException();
        }
        @Override
        public List asList() {
            List list = new AbstractList() {
                int n = getint(bytes, offset + 1);
                int[] offsets = null;
                int offseti = 0;

                @Override
                public int size() {
                    return n;
                }

                @Override
                public WireValue get(int index) {
                    if (index < 0 || n <= index) {
                        throw new IndexOutOfBoundsException();
                    }
                    fillOffsets(index);
                    return valueOf(bytes, offsets[index], cs);
                }


                void fillOffsets(int j) {
                    if (offseti <= j) {
                        if (null == offsets) {
                            offsets = new int[n];
                            offsets[0] = offset + 9;
                            offseti = 1;
                        }

                        int i = offseti;
                        for (; i <= j; ++i) {
                            offsets[i] = offsets[i - 1] + byteSize(bytes, offsets[i - 1], cs);
                        }
                        offseti = i;
                    }
                }
            };
            return duckList(list);
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }


    private static class CInt32WireValue extends CompressedWireValue {
        CInt32WireValue(byte[] bytes, int offset, CompressionState cs) {
            super(Type.INT32, bytes, offset, cs);
        }

        @Override
        public String asString() {
            return String.valueOf(asInt());
        }
        @Override
        public int asInt() {
            return getint(bytes, offset + 1);
        }
        @Override
        public long asLong() {
            return asInt();
        }
        @Override
        public float asFloat() {
            return (float) asInt();
        }
        @Override
        public double asDouble() {
            return (double) asInt();
        }
        @Override
        public boolean asBoolean() {
            return 0 != asInt();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class CInt64WireValue extends CompressedWireValue {
        CInt64WireValue(byte[] bytes, int offset, CompressionState cs) {
            super(Type.INT64, bytes, offset, cs);
        }

        @Override
        public String asString() {
            return String.valueOf(asLong());
        }
        @Override
        public int asInt() {
            return (int) asLong();
        }
        @Override
        public long asLong() {
            return getlong(bytes, offset + 1);
        }
        @Override
        public float asFloat() {
            return (float) asLong();
        }
        @Override
        public double asDouble() {
            return (double) asLong();
        }
        @Override
        public boolean asBoolean() {
            return 0 != asLong();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class CFloat32WireValue extends CompressedWireValue {
        CFloat32WireValue(byte[] bytes, int offset, CompressionState cs) {
            super(Type.FLOAT32, bytes, offset, cs);
        }

        @Override
        public String asString() {
            return String.valueOf(asFloat());
        }
        @Override
        public int asInt() {
            return (int) asFloat();
        }
        @Override
        public long asLong() {
            return (long) asFloat();
        }
        @Override
        public float asFloat() {
            return Float.intBitsToFloat(getint(bytes, offset + 1));
        }
        @Override
        public double asDouble() {
            return (double) asFloat();
        }
        @Override
        public boolean asBoolean() {
            return 0.f != asFloat();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class CFloat64WireValue extends CompressedWireValue {
        CFloat64WireValue(byte[] bytes, int offset, CompressionState cs) {
            super(Type.FLOAT64, bytes, offset, cs);
        }

        @Override
        public String asString() {
            return String.valueOf(asDouble());
        }
        @Override
        public int asInt() {
            return (int) asDouble();
        }
        @Override
        public long asLong() {
            return (long) asDouble();
        }
        @Override
        public float asFloat() {
            return (float) asDouble();
        }
        @Override
        public double asDouble() {
            return Double.longBitsToDouble(getlong(bytes, offset + 1));
        }
        @Override
        public boolean asBoolean() {
            return 0.0 != asDouble();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class CMessageWireValue extends CompressedWireValue {
        CMessageWireValue(byte[] bytes, int offset, CompressionState cs) {
            super(Type.MESSAGE, bytes, offset, cs);
        }

        @Override
        public String asString() {
            throw new UnsupportedOperationException();
        }
        @Override
        public int asInt() {
            throw new UnsupportedOperationException();
        }
        @Override
        public long asLong() {
            throw new UnsupportedOperationException();
        }
        @Override
        public float asFloat() {
            throw new UnsupportedOperationException();
        }
        @Override
        public double asDouble() {
            throw new UnsupportedOperationException();
        }
        @Override
        public boolean asBoolean() {
            throw new UnsupportedOperationException();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            // [0] is the header
            return MessageCodec.valueOf(bytes, offset + 1, cs);
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class CImageWireValue extends CompressedWireValue {
        CImageWireValue(byte[] bytes, int offset, CompressionState cs) {
            super(Type.IMAGE, bytes, offset, cs);
        }

        @Override
        public String asString() {
            throw new UnsupportedOperationException();
        }
        @Override
        public int asInt() {
            throw new UnsupportedOperationException();
        }
        @Override
        public long asLong() {
            throw new UnsupportedOperationException();
        }
        @Override
        public float asFloat() {
            throw new UnsupportedOperationException();
        }
        @Override
        public double asDouble() {
            throw new UnsupportedOperationException();
        }
        @Override
        public boolean asBoolean() {
            throw new UnsupportedOperationException();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            // [0] is the header
            return ImageCodec.valueOf(bytes, offset + 1);
        }
    }




    // LUT sizes: 2^7, 2^15
    // index maps to byte[]
    private static class CompressionState {
        byte[] header;
        int offset;
        int[] offsets;
        int nb;

        CompressionState(byte[] header, int offset, int[] offsets, int nb) {
            this.header = header;
            this.offset = offset;
            this.offsets = offsets;
            this.nb = nb;
        }
    }






    public static WireValue of(JsonElement e) {
        if (e.isJsonPrimitive()) {
            JsonPrimitive p = e.getAsJsonPrimitive();
            if (p.isBoolean()) {
                return of(p.getAsBoolean());
            } else if (p.isNumber()) {
                try {
                    long n = p.getAsLong();
                    if ((int) n == n) {
                        return of((int) n);
                    } else {
                        return of(n);
                    }
                } catch (NumberFormatException x) {
                    double d = p.getAsDouble();
                    if ((float) d == d) {
                        return of((float) d);
                    } else {
                        return of(d);
                    }
                }
            } else if (p.isString()) {
                return of(p.getAsString());
            } else {
                throw new IllegalArgumentException();
            }
        } else if (e.isJsonObject()) {
            JsonObject object = e.getAsJsonObject();
            Map m = new HashMap(4);
            for (Map.Entry oe : object.entrySet()) {
                m.put(of(oe.getKey()), of(oe.getValue()));
            }
            return of(m);
        } else if (e.isJsonArray()) {
            JsonArray array = e.getAsJsonArray();
            List list = new ArrayList(4);
            for (JsonElement ae : array) {
                list.add(of(ae));
            }
            return of(list);
        } else {
            throw new IllegalArgumentException();
        }
    }


    public static WireValue valueOfJson(String json) {
        try {
            return valueOfJson(new StringReader(json));
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    /** @param jsonIn json source. Closed by this method. */
    public static WireValue valueOfJson(Reader jsonIn) throws IOException {
        JsonReader r = new JsonReader(jsonIn);
        try {
            return parseJson(r);
        } finally {
            r.close();
        }
    }

    private static WireValue parseJson(JsonReader r) throws IOException {
        switch (r.peek()) {
            case BEGIN_OBJECT: {
                Map map = new HashMap(4);
                r.beginObject();
                while (!JsonToken.END_OBJECT.equals(r.peek())) {
                    WireValue key = WireValue.of(r.nextName());
                    WireValue value = parseJson(r);
                    map.put(key, value);
                }
                r.endObject();
                return WireValue.of(map);
            }
            case BEGIN_ARRAY: {
                List list = new ArrayList(4);
                r.beginArray();
                while (!JsonToken.END_ARRAY.equals(r.peek())) {
                    WireValue value = parseJson(r);
                    list.add(value);
                }
                r.endArray();
                return WireValue.of(list);
            }
            case STRING:
                return WireValue.of(r.nextString());
            case NUMBER: {
                try {
                    long n = r.nextLong();
                    if ((int) n == n) {
                        return WireValue.of((int) n);
                    } else {
                        return WireValue.of(n);
                    }
                } catch (NumberFormatException e) {
                    double d = r.nextDouble();
                    if ((float) d == d) {
                        return WireValue.of((float) d);
                    } else {
                        return WireValue.of(d);
                    }
                }
            }
            case BOOLEAN:
                return WireValue.of(r.nextBoolean());
            case NULL:
                // FIXME have a NULL WireValue Type
                throw new IllegalArgumentException();
            default:
            case END_DOCUMENT:
                throw new IllegalArgumentException();
        }
    }




    public static WireValue of(@Nullable Object value) {
        if (null == value) {
            return of();
        }
        if (value instanceof WireValue) {
            return (WireValue) value;
        }
        if (value instanceof CharSequence) {
            return of(value.toString());
        }
        if (value instanceof Number) {
            if (value instanceof Integer) {
                return of(((Integer) value).intValue());
            }
            if (value instanceof Long) {
                return of(((Long) value).longValue());
            }
            if (value instanceof Float) {
                return of(((Float) value).floatValue());
            }
            if (value instanceof Double) {
                return of(((Double) value).doubleValue());
            }
            // default int32
            return of(((Number) value).intValue());
        }
        if (value instanceof Boolean) {
            return of(((Boolean) value).booleanValue());
        }
        if (value instanceof byte[]) {
            return of((byte[]) value);
        }
        // FIXME bytebuffer
        if (value instanceof Map) {
            return of(asWireValueMap((Map) value));
        }
        if (value instanceof Collection) {
            if (value instanceof List) {
                return of(asWireValueList((List) value));
            }
            return of(asWireValueList((Collection) value));
        }
        if (value instanceof Message) {
            return of((Message) value);
        }
        if (value instanceof EncodedImage) {
            return of((EncodedImage) value);
        }
        if (value instanceof JsonElement) {
            return of((JsonElement) value);
        }
        // FIXME ID
        if (value instanceof Id) {
            return of(value.toString());
        }
        throw new IllegalArgumentException();
    }

    static WireValue of(byte[] value) {
        return of(value, 0, value.length);
    }
    static WireValue of(byte[] value, int offset, int length) {
        return new BlobWireValue(value, offset, length);
    }

    // FIXME of(ByteBuffer)

    static WireValue of(String value) {
        return new Utf8WireValue(value);
    }

    static WireValue of(int value) {
        return new NumberWireValue(value);
    }

    static WireValue of(long value) {
        return new NumberWireValue(value);
    }

    static WireValue of(float value) {
        return new NumberWireValue(value);
    }

    static WireValue of(double value) {
        return new NumberWireValue(value);
    }

    static WireValue of(boolean value) {
        return new BooleanWireValue(value);
    }

    static WireValue of(Map m) {
        return new MapWireValue(m);
    }

    static WireValue of(List list) {
        return new ListWireValue(list);
    }


    static WireValue of(Message m) {
        return new MessageWireValue(m);
    }

    static WireValue of(EncodedImage image) {
        return new ImageWireValue(image);
    }

    // null
    static WireValue of() {
        return new NullWireValue();
    }






    final Type type;
    boolean hashCodeSet = false;
    int hashCode;


    WireValue(Type type) {
        this.type = type;
    }


    // FIXME as* functions that represent the wirevalue as something



    public final Type getType() {
        return type;
    }


    @Override
    public final int hashCode() {
        if (!hashCodeSet) {
            hashCode = _hashCode(this);
            hashCodeSet = true;
        }
        return hashCode;
    }
    static int _hashCode(WireValue value) {
        switch (value.type) {
            case UTF8:
                return value.asString().hashCode();
            case BLOB:
                return value.asBlob().hashCode();
            case INT32:
                return value.asInt();
            case INT64:
                long n = value.asLong();
                return (int) (n ^ (n >>> 32));
            case FLOAT32:
                return Float.floatToIntBits(value.asFloat());
            case FLOAT64:
                long dn = Double.doubleToLongBits(value.asDouble());
                return (int) (dn ^ (dn >>> 32));
            case BOOLEAN:
                return value.asBoolean() ? 1231 : 1237;
            case MAP: {
                Map map = value.asMap();
//                List keys = stableKeys(map);
//                List values = stableValues(map, keys);
                // important: order should not affect hash code
                int c = 0;
                for (Map.Entry e : map.entrySet()) {
                    c += _hashCode(e.getKey());
                    c += _hashCode(e.getValue());
                }
                return c;
             }
            case LIST: {
                int c = 0;
                for (WireValue v : value.asList()) {
                    c = 31 * c + _hashCode(v);
                }
                return c;
            }
            case MESSAGE:
                return value.asMessage().hashCode();
            case IMAGE:
                return value.asImage().hashCode();
            case NULL:
                return 0;
            default:
                throw new IllegalArgumentException("" + value.type);
        }
    }

    @Override
    public final boolean equals(Object obj) {
        if (!(obj instanceof WireValue)) {
            return false;
        }
        WireValue b = (WireValue) obj;
        return _equals(this, b);
    }
    static boolean _equals(WireValue a, WireValue b) {
        if (!a.type.equals(b.type) || a.hashCode() != b.hashCode()) {
            return false;
        }
        switch (a.type) {
            case UTF8:
                return a.asString().equals(b.asString());
            case BLOB:
                return a.asBlob().equals(b.asBlob());
            case INT32:
                return a.asInt() == b.asInt();
            case INT64:
                return a.asLong() == b.asLong();
            case FLOAT32:
                return a.asFloat() == b.asFloat();
            case FLOAT64:
                return a.asDouble() == b.asDouble();
            case BOOLEAN:
                return a.asBoolean() == b.asBoolean();
            case MAP:
                return a.asMap().equals(b.asMap());
            case LIST:
                return a.asList().equals(b.asList());
            case MESSAGE:
                return a.asMessage().equals(b.asMessage());
            case IMAGE:
                return a.asImage().equals(b.asImage());
            case NULL:
                return true;
            default:
                throw new IllegalArgumentException();
        }
    }


    @Override
    public String toString() {
        return toText();
    }

    public String toDebugString() {
        return String.format("%s %s", type, toString());
    }


    public String toText() {
        switch (type) {
            case UTF8:
                return asString();
            case BLOB:
                return base64(asBlob());
            case INT32:
                return String.valueOf(asInt());
            case INT64:
                return String.valueOf(asLong());
            case FLOAT32:
                return String.valueOf(asFloat());
            case FLOAT64:
                return String.valueOf(asDouble());
            case BOOLEAN:
                return String.valueOf(asBoolean());
            case MAP:
                return toJson();
            case LIST:
                return toJson();
            case MESSAGE:
                // TODO
                return "[message]";
            case IMAGE:
                return base64(asImage().toBuffer());
            case NULL:
                // TODO
                return "";
            default:
                throw new IllegalArgumentException();
        }
    }



    public String toJson() {
        // TODO better size upper bound - will the better memory efficiency be worth a pre-scan?
        StringWriter sw = new StringWriter();
        try {
            toJson(sw);
            return sw.toString();
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }

    public void toJson(Writer jsonOut) throws IOException {
        JsonWriter w = new JsonWriter(jsonOut);
        try {
            toJson(this, w);
        } finally {
            w.close();
        }
    }


    static void toJson(WireValue value, JsonWriter w) throws IOException {
        switch (value.getType()) {
            case UTF8:
                w.value(value.asString());
                break;
            case BLOB:
                w.value(base64(value.asBlob()));
                break;
            case INT32:
                w.value(value.asInt());
                break;
            case INT64:
                w.value(value.asLong());
                break;
            case FLOAT32:
                w.value(value.asFloat());
                break;
            case FLOAT64:
                w.value(value.asDouble());
                break;
            case BOOLEAN:
                w.value(value.asBoolean());
                break;
            case MAP:
                w.beginObject();
                Map map = value.asMap();
                for (WireValue key : stableKeys(map)) {
                    w.name(key.toText());
                    toJson(map.get(key), w);
                }
                w.endObject();
                break;
            case LIST:
                w.beginArray();
                for (WireValue v : value.asList()) {
                    toJson(v, w);
                }
                w.endArray();
                break;
            case MESSAGE:
                // FIXME 0.1.1 write as object
                // TODO base64(asImage().toBuffer());
                w.value("[message]");
                break;
            case IMAGE:
                // FIXME 0.1.1 write as object
                w.value("[image]");
                break;
            case NULL:
                w.nullValue();
                break;
            default:
                throw new IllegalArgumentException();
        }
    }

    static String base64(ByteBuffer bytes) {
        byte[] b64;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream(bytes.remaining() * 4 / 3);
            Base64OutputStream b64os = new Base64OutputStream(baos, true, 0, null);

            WritableByteChannel channel = Channels.newChannel(b64os);
            channel.write(bytes);
            channel.close();

            b64 = baos.toByteArray();
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
        return new String(b64, Charsets.UTF_8);
    }




    static final int H_COMPRESSED = 0x80;
    static final int H_UTF8 = 1;
    static final int H_BLOB = 2;
    static final int H_INT32 = 3;
    static final int H_INT64 = 4;
    static final int H_FLOAT32 = 5;
    static final int H_FLOAT64 = 6;
    static final int H_TRUE_BOOLEAN = 7;
    static final int H_FALSE_BOOLEAN = 8;
    static final int H_MAP = 9;
    static final int H_LIST = 10;
    static final int H_INT32_LIST = 11;
    static final int H_INT64_LIST = 12;
    static final int H_FLOAT32_LIST = 13;
    static final int H_FLOAT64_LIST = 14;
    static final int H_NULL = 15;
    static final int H_MESSAGE = 16;
    static final int H_IMAGE = 17;


    static byte[] header(int nb, int v) {
        switch (nb) {
            case 1:
                return new byte[]{(byte) (v & 0xFF)};
            case 2:
                return new byte[]{(byte) ((v >>> 8) & 0xFF), (byte) (v & 0xFF)};
            case 3:
                return new byte[]{(byte) ((v >>> 16) & 0xFF), (byte) ((v >>> 8) & 0xFF), (byte) (v & 0xFF)};
            default:
                throw new IllegalArgumentException();
        }
    }

    static int listh(List list) {
        return H_LIST;
        // FIXME implement later
//        int n = list.size();
//        if (0 == n) {
//            return H_LIST;
//        }
//        Type t = list.get(0).getType();
//        for (int i = 0; i < n; ++i) {
//            if (!t.equals(list.get(1).getType())) {
//                return H_LIST;
//            }
//        }
//        switch (t) {
//            case INT32:
//                return H_INT32;
//            case INT64:
//                return H_INT64_LIST;
//            case FLOAT32:
//                return H_FLOAT32_LIST;
//            case FLOAT64:
//                return H_FLOAT64_LIST;
//            default:
//                return H_LIST;
//        }
    }




    // toByte should always compress
    static int byteSize(byte[] bytes, int offset, CompressionState cs) {
        int h = 0xFF & bytes[offset];
        if ((h & H_COMPRESSED) == H_COMPRESSED) {
            return cs.nb;
        }
        return _byteSize(bytes, offset);
    }

    static int _byteSize(byte[] bytes, int offset) {
        int h = 0xFF & bytes[offset];
//        System.out.printf("_byteSize %s\n", h);
        switch (h) {
            case H_UTF8:
                return 5 + getint(bytes, offset + 1);
            case H_BLOB:
                return 5 + getint(bytes, offset + 1);
            case H_INT32:
                return 5;
            case H_INT64:
                return 9;
            case H_FLOAT32:
                return 5;
            case H_FLOAT64:
                return 9;
            case H_TRUE_BOOLEAN:
                return 1;
            case H_FALSE_BOOLEAN:
                return 1;
            case H_MAP:
                return 5 + getint(bytes, offset + 1);
            case H_LIST:
                return 9 + getint(bytes, offset + 5);
            case H_INT32_LIST:
                // FIXME see listh
                throw new IllegalArgumentException();
            case H_INT64_LIST:
                // FIXME see listh
                throw new IllegalArgumentException();
            case H_FLOAT32_LIST:
                // FIXME see listh
                throw new IllegalArgumentException();
            case H_FLOAT64_LIST:
                // FIXME see listh
                throw new IllegalArgumentException();
            case H_MESSAGE:
                return 5 + getint(bytes, offset + 1);
            case H_IMAGE:
                return 5 + getint(bytes, offset + 1);
            case H_NULL:
                return 1;
            default:
                throw new IllegalArgumentException("" + h);
        }
    }



    public void toBytes(ByteBuffer bb) {

        Lb lb = new Lb();
        lb.init(this);
        if (lb.opt()) {
            // write the lut header
            bb.put((byte) (H_COMPRESSED | lb.lutNb));
            bb.putInt(lb.lut.size());
            bb.putInt(0);
            int i = bb.position();

            for (Lb.S s : lb.lut) {
                _toBytes(s.value, lb, bb);
            }

            int bytes = bb.position() - i;
            bb.putInt(i - 4, bytes);
//            System.out.printf("header %d bytes\n", bytes);
        }

        int i = bb.position();
        toBytes(this, lb, bb);
        int bytes = bb.position() - i;
//        System.out.printf("body %d bytes\n", bytes);

    }
    private static void toBytes(WireValue value, Lb lb, ByteBuffer bb) {
        int luti = lb.luti(value);
        if (0 <= luti) {
            byte[] header = header(lb.lutNb, luti);
            header[0] |= H_COMPRESSED;
            bb.put(header);
        } else {
            _toBytes(value, lb, bb);
        }
    }
    private static void _toBytes(WireValue value, Lb lb, ByteBuffer bb) {
        switch (value.getType()) {
            case MAP: {
                bb.put((byte) H_MAP);
                bb.putInt(0);

                int i = bb.position();

                Map m = value.asMap();
                List keys = stableKeys(m);
                toBytes(of(keys), lb, bb);
                List values = stableValues(m, keys);
                toBytes(of(values), lb, bb);

                int bytes = bb.position() - i;
                bb.putInt(i - 4, bytes);

                break;
            }
            case LIST: {
                List list = value.asList();
                int listh = listh(list);
                if (H_LIST == listh) {
                    bb.put((byte) H_LIST);
                    bb.putInt(list.size());
                    bb.putInt(0);

                    int i = bb.position();

                    for (WireValue v : value.asList()) {
                        toBytes(v, lb, bb);
                    }

                    int bytes = bb.position() - i;
                    bb.putInt(i - 4, bytes);
                } else {
                    // primitive homogeneous list
                    bb.put((byte) listh);
                    bb.putInt(list.size());
                    bb.putInt(0);

                    int i = bb.position();

                    switch (listh) {
                        case H_INT32_LIST:
                            for (WireValue v : value.asList()) {
                                bb.putInt(v.asInt());
                            }
                            break;
                        case H_INT64_LIST:
                            for (WireValue v : value.asList()) {
                                bb.putLong(v.asLong());
                            }
                            break;
                        case H_FLOAT32_LIST:
                            for (WireValue v : value.asList()) {
                                bb.putFloat(v.asFloat());
                            }
                            break;
                        case H_FLOAT64_LIST:
                            for (WireValue v : value.asList()) {
                                bb.putDouble(v.asDouble());
                            }
                            break;
                    }

                    int bytes = bb.position() - i;
                    bb.putInt(i - 4, bytes);

                }
                break;
            }
            case BLOB: {
                ByteBuffer b = value.asBlob();

                bb.put((byte) H_BLOB);
                bb.putInt(b.remaining());
                bb.put(b);

                break;
            }
            case UTF8: {
                byte[] b = value.asString().getBytes(Charsets.UTF_8);

                bb.put((byte) H_UTF8);
                bb.putInt(b.length);
                bb.put(b);

                break;
            }
            case INT32:
                bb.put((byte) H_INT32);
                bb.putInt(value.asInt());
                break;
            case INT64:
                bb.put((byte) H_INT64);
                bb.putLong(value.asLong());
                break;
            case FLOAT32:
                bb.put((byte) H_FLOAT32);
                bb.putFloat(value.asFloat());
                break;
            case FLOAT64:
                bb.put((byte) H_FLOAT64);
                bb.putDouble(value.asDouble());
                break;
            case BOOLEAN:
                bb.put((byte) (value.asBoolean() ? H_TRUE_BOOLEAN : H_FALSE_BOOLEAN));
                break;
            case MESSAGE:
                bb.put((byte) H_MESSAGE);
                MessageCodec.toBytes(value.asMessage(), lb, bb);
                break;
            case IMAGE:
                bb.put((byte) H_IMAGE);
                ImageCodec.toBytes(value.asImage(), lb, bb);
                break;
            case NULL:
                bb.put((byte) H_NULL);
                break;
            default:
                throw new IllegalArgumentException();
        }
    }


    // lut builder
    static class Lb {
        static class S {
            WireValue value;

            int count;

            // FIXME record this in expandOne/expand
            int maxd = -1;
            int maxi = -1;
//            boolean r = false;

            int luti = -1;
        }


        Map stats = new HashMap(4);
//        List rs = new ArrayList(4);

        List lut = new ArrayList(4);
        int lutNb = 0;


        int luti(WireValue value) {
            S s = stats.get(value);
            if (null != s) {
                return s.luti;
            } else {
                // not in the lut
                return -1;
            }
        }


        void init(WireValue value) {
            expand(value, 0, 0);
        }

        int expand(WireValue value, int d, int i) {
            switch (value.getType()) {
                case MAP:
                    expandOne(value, d, i);
                    i += 1;
                    // the rest
                    Map m = value.asMap();
                    List keys = stableKeys(m);
                    i = expand(of(keys), d + 1, i);
                    List values = stableValues(m, keys);
                    i = expand(of(values), d + 1, i);
                    break;
                case LIST:
                    expandOne(value, d, i);
                    i += 1;
                    // the rest
                    for (WireValue v : value.asList()) {
                        i = expand(v, d + 1, i);
                    }
                    break;
                default:
                    expandOne(value, d, i);
                    i += 1;
                    break;
            }
            return i;
        }
        void expandOne(WireValue value, int d, int i) {
            S s = stats.get(value);
            if (null == s) {
                s = new S();
                s.value = value;
                stats.put(value, s);
            }
            s.count += 1;
            s.maxd = Math.max(s.maxd, d);
            s.maxi = Math.max(s.maxi, i);
//            if (rmask && !s.r) {
//                s.r = true;
//                rs.add(s);
//            }
        }

        boolean opt() {

            int nb = estimateLutNb();
            if (nb <= 0) {
                return false;
            }

            // order r values by maxd
            // if include each, subtract down
            List rs = new ArrayList(stats.values());
            Collections.sort(rs, new Comparator() {
                @Override
                public int compare(S a, S b) {
                    if (a == b) {
                        return 0;
                    }

                    // maxd
                    if (a.maxd < b.maxd) {
                        return -1;
                    }
                    if (b.maxd < a.maxd) {
                        return 1;
                    }

                    // maxi
                    if (a.maxi < b.maxi) {
                        return -1;
                    }
                    if (b.maxi < a.maxi) {
                        return 1;
                    }

                    return 0;
                }
            });
            for (S s : rs) {
                if (include(s, nb)) {
                    s.luti = lut.size();
                    lut.add(s);
                    collapse(s.value);
                }
            }

            lutNb = nb(lut.size());

            // lut is ordered implicity by maxd

            return true;
        }

        void collapse(WireValue value) {
            switch (value.getType()) {
                case MAP:
                    collapseOne(value);
                    // the rest
                    Map m = value.asMap();
                    List keys = stableKeys(m);
                    collapse(of(keys));
                    List values = stableValues(m, keys);
                    collapse(of(values));
                    break;
                case LIST:
                    collapseOne(value);
                    // the rest
                    for (WireValue v : value.asList()) {
                        collapse(v);
                    }
                    break;
                default:
                    collapseOne(value);
                    break;
            }
        }
        void collapseOne(WireValue value) {
            S s = stats.get(value);
            assert null != s;
            s.count -= 1;
        }





        int[] sizes = new int[]{0, 0x10, 0x0100, 0x010000, 0x01000000};
        int nb(int c) {
            int nb = 0;
            while (sizes[nb] < c) {
                ++nb;
            }
            return nb;
        }






        int estimateLutNb() {
            int nb = nb(count(1));
            if (nb == sizes.length) {
                // too many unique values; punt
                return -1;
            }
            int pnb;
            do {
                pnb = nb;
                nb = nb(count(nb));
            } while (nb < pnb);

            return pnb;
        }
        int count(int nb) {
            int c = 0;
            for (S s : stats.values()) {
                if (include(s, nb)) {
                    c += 1;
                }
            }
            return c;
        }


        boolean include(S s, int nb) {
            int b = quickLowerSize(s.value.getType());
            return nb * s.count + b < s.count * b;
        }

        int quickLowerSize(Type type) {
            switch (type) {
                case MAP:
                    // TODO this is a lower estimate
                    // length + ...
                    return 12;
                case LIST:
                    // TODO this is a lower estimate
                    return 8;
                case BLOB:
                    // TODO this is a lower estimate
                    return 8;
                case UTF8:
                    // TODO this is a lower estimate
                    return 8;
                case INT32:
                    return 4;
                case INT64:
                    return 8;
                case FLOAT32:
                    return 4;
                case FLOAT64:
                    return 8;
                case BOOLEAN:
                    return 1;
                case MESSAGE:
                    // FIXME
                    return 1;
                case IMAGE:
                    // FIXME
                    return 1;
                case NULL:
                    return 1;
                default:
                    throw new IllegalArgumentException();
            }
        }
    }

    static List stableKeys(Map m) {
        List keys = new ArrayList(m.keySet());
        Collections.sort(keys, COMPARATOR_STABLE);
        return keys;
    }

    static List stableValues(Map m, List keys) {
        List values = new ArrayList(keys.size());
        for (WireValue key : keys) {
            values.add(m.get(key));
        }
        return values;
    }


    static final Comparator COMPARATOR_STABLE = new StableComparator();

    // stable ordering for all values
    static final class StableComparator implements Comparator {
        @Override
        public int compare(WireValue a, WireValue b) {
            Type ta = a.getType();
            Type tb = b.getType();

            int d = ta.ordinal() - tb.ordinal();
            if (0 != d) {
                return d;
            }
            assert ta.equals(tb);

            switch (ta) {
                case MAP: {
                    Map amap = a.asMap();
                    Map bmap = b.asMap();
                    d = amap.size() - bmap.size();
                    if (0 != d) {
                        return d;
                    }
                    List akeys = stableKeys(amap);
                    List bkeys = stableKeys(bmap);
                    d = compare(of(akeys), of(bkeys));
                    if (0 != d) {
                        return d;
                    }
                    return compare(of(stableValues(amap, akeys)), of(stableValues(bmap, bkeys)));
                }
                case LIST: {
                    List alist = a.asList();
                    List blist = b.asList();
                    int n = alist.size();
                    int m = blist.size();
                    d = n - m;
                    if (0 != d) {
                        return d;
                    }
                    for (int i = 0; i < n; ++i) {
                        d = compare(alist.get(i), blist.get(i));
                        if (0 != d) {
                            return d;
                        }
                    }
                    return 0;
                }
                case BLOB: {
                    ByteBuffer abytes = a.asBlob();
                    ByteBuffer bbytes = b.asBlob();
                    int n = abytes.remaining();
                    int m = bbytes.remaining();
                    d = n - m;
                    if (0 != d) {
                        return d;
                    }
                    for (int i = 0; i < n; ++i) {
                        d = (0xFF & abytes.get(i)) - (0xFF & bbytes.get(i));
                        if (0 != d) {
                            return d;
                        }
                    }
                    return 0;
                }
                case UTF8:
                    return a.asString().compareTo(b.asString());
                case INT32:
                    int ai = a.asInt();
                    int bi = b.asInt();
                    if (ai < bi) {
                        return -1;
                    }
                    if (bi < ai) {
                        return 1;
                    }
                    return 0;
                case INT64:
                    long an = a.asLong();
                    long bn = b.asLong();
                    if (an < bn) {
                        return -1;
                    }
                    if (bn < an) {
                        return 1;
                    }
                    return 0;
                case FLOAT32:
                    return Float.compare(a.asFloat(), b.asFloat());
                case FLOAT64:
                    return Double.compare(a.asDouble(), b.asDouble());
                case MESSAGE:
                    return a.asMessage().id.compareTo(b.asMessage().id);
                case IMAGE: {
                    // compare blobs
                    ByteBuffer abytes = a.asImage().toBuffer();
                    ByteBuffer bbytes = b.asImage().toBuffer();
                    int n = abytes.remaining();
                    int m = bbytes.remaining();
                    d = n - m;
                    if (0 != d) {
                        return d;
                    }
                    for (int i = 0; i < n; ++i) {
                        d = (0xFF & abytes.get(i)) - (0xFF & bbytes.get(i));
                        if (0 != d) {
                            return d;
                        }
                    }
                    return 0;
                }
                case NULL:
                    return 0;
                default:
                    throw new IllegalArgumentException();
            }
        }
    }


//    static final class SizeEstimate {
//        long bytes;
//        int verifiedNodes;
//        int unverifiedNodes;
//    }
//
//    public abstract SizeEstimate estimateSize();


    // logical view, regardless of wire format

    // FIXME move Id to first class type
    public Id asId() {
        return Id.valueOf(asString());
    }

    public abstract String asString();
    public abstract int asInt();
    public abstract long asLong();
    public abstract float asFloat();
    public abstract double asDouble();
    public abstract boolean asBoolean();
    public abstract List asList();
    public abstract Map asMap();
    public abstract ByteBuffer asBlob();
    public abstract Message asMessage();
    public abstract EncodedImage asImage();








    private static class BlobWireValue extends WireValue {
        final byte[] value;
        final int offset;
        final int length;

        BlobWireValue(byte[] value, int offset, int length) {
            super(Type.BLOB);
            this.value = value;
            this.offset = offset;
            this.length = length;
        }

        @Override
        public String asString() {
            return base64(ByteBuffer.wrap(value, offset, length));
        }
        @Override
        public int asInt() {
            throw new UnsupportedOperationException();
        }
        @Override
        public long asLong() {
            throw new UnsupportedOperationException();
        }
        @Override
        public float asFloat() {
            throw new UnsupportedOperationException();
        }
        @Override
        public double asDouble() {
            throw new UnsupportedOperationException();
        }
        @Override
        public boolean asBoolean() {
            throw new UnsupportedOperationException();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            return ByteBuffer.wrap(value, offset, length);
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }


    }

    private static class Utf8WireValue extends WireValue {
        final String value;

        Utf8WireValue(String value) {
            super(Type.UTF8);
            this.value = value;
        }

        @Override
        public String asString() {
            return value;
        }
        @Override
        public int asInt() {
            return Integer.parseInt(value);
        }
        @Override
        public long asLong() {
            return Long.parseLong(value);
        }
        @Override
        public float asFloat() {
            return Float.parseFloat(value);
        }
        @Override
        public double asDouble() {
            return Double.parseDouble(value);
        }
        @Override
        public boolean asBoolean() {
            return Boolean.parseBoolean(value);
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            return ByteBuffer.wrap(value.getBytes(Charsets.UTF_8));
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class NumberWireValue extends WireValue {
        static Type type(Number n) {
            if (n instanceof Integer) {
                return Type.INT32;
            }
            if (n instanceof Long) {
                return Type.INT64;
            }
            if (n instanceof Float) {
                return Type.FLOAT32;
            }
            if (n instanceof Double) {
                return Type.FLOAT64;
            }
            throw new IllegalArgumentException();
        }

        final Number value;

        NumberWireValue(Number value) {
            super(type(value));
            this.value = value;
        }

        @Override
        public String asString() {
            return value.toString();
        }
        @Override
        public int asInt() {
            return value.intValue();
        }
        @Override
        public long asLong() {
            return value.longValue();
        }
        @Override
        public float asFloat() {
            return value.floatValue();
        }
        @Override
        public double asDouble() {
            return value.doubleValue();
        }
        @Override
        public boolean asBoolean() {
            // c style
            return 0 != value.floatValue();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            // TODO
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class BooleanWireValue extends WireValue {

        final boolean value;

        BooleanWireValue(boolean value) {
            super(Type.BOOLEAN);
            this.value = value;
        }

        @Override
        public String asString() {
            return String.valueOf(value);
        }
        @Override
        public int asInt() {
            return value ? 1 : 0;
        }
        @Override
        public long asLong() {
            return value ? 1L : 0L;
        }
        @Override
        public float asFloat() {
            return value ? 1.f : 0.f;
        }
        @Override
        public double asDouble() {
            return value ? 1.0 : 0.0;
        }
        @Override
        public boolean asBoolean() {
            return value;
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            // TODO
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class ListWireValue extends WireValue {
        final List value;

        ListWireValue(List value) {
            super(Type.LIST);
            this.value = duckList(value);
        }

        @Override
        public String asString() {
            return value.toString();
        }
        @Override
        public int asInt() {
            throw new UnsupportedOperationException();
        }
        @Override
        public long asLong() {
            throw new UnsupportedOperationException();
        }
        @Override
        public float asFloat() {
            throw new UnsupportedOperationException();
        }
        @Override
        public double asDouble() {
            throw new UnsupportedOperationException();
        }
        @Override
        public boolean asBoolean() {
            throw new UnsupportedOperationException();
        }
        @Override
        public List asList() {
            return value;
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            // TODO
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class MapWireValue extends WireValue {
        final Map value;

        MapWireValue(Map value) {
            super(Type.MAP);
            this.value = duckMap(value);
        }

        @Override
        public String asString() {
            return value.toString();
        }
        @Override
        public int asInt() {
            throw new UnsupportedOperationException();
        }
        @Override
        public long asLong() {
            throw new UnsupportedOperationException();
        }
        @Override
        public float asFloat() {
            throw new UnsupportedOperationException();
        }
        @Override
        public double asDouble() {
            throw new UnsupportedOperationException();
        }
        @Override
        public boolean asBoolean() {
            throw new UnsupportedOperationException();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            return value;
        }
        @Override
        public ByteBuffer asBlob() {
            // TODO
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class MessageWireValue extends WireValue {
        final Message message;

        MessageWireValue(Message message) {
            super(Type.MESSAGE);
            this.message = message;
        }

        @Override
        public String asString() {
            throw new UnsupportedOperationException();
        }
        @Override
        public int asInt() {
            throw new UnsupportedOperationException();
        }
        @Override
        public long asLong() {
            throw new UnsupportedOperationException();
        }
        @Override
        public float asFloat() {
            throw new UnsupportedOperationException();
        }
        @Override
        public double asDouble() {
            throw new UnsupportedOperationException();
        }
        @Override
        public boolean asBoolean() {
            throw new UnsupportedOperationException();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            return message;
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }

    private static class ImageWireValue extends WireValue {
        final EncodedImage image;

        ImageWireValue(EncodedImage image) {
            super(Type.IMAGE);
            this.image = image;
        }


        @Override
        public String asString() {
            throw new UnsupportedOperationException();
        }
        @Override
        public int asInt() {
            throw new UnsupportedOperationException();
        }
        @Override
        public long asLong() {
            throw new UnsupportedOperationException();
        }
        @Override
        public float asFloat() {
            throw new UnsupportedOperationException();
        }
        @Override
        public double asDouble() {
            throw new UnsupportedOperationException();
        }
        @Override
        public boolean asBoolean() {
            throw new UnsupportedOperationException();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            return image;
        }
    }

    private static class NullWireValue extends WireValue {
        NullWireValue() {
            super(Type.NULL);
        }


        @Override
        public String asString() {
            throw new UnsupportedOperationException();
        }
        @Override
        public int asInt() {
            throw new UnsupportedOperationException();
        }
        @Override
        public long asLong() {
            throw new UnsupportedOperationException();
        }
        @Override
        public float asFloat() {
            throw new UnsupportedOperationException();
        }
        @Override
        public double asDouble() {
            throw new UnsupportedOperationException();
        }
        @Override
        public boolean asBoolean() {
            throw new UnsupportedOperationException();
        }
        @Override
        public List asList() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Map asMap() {
            throw new UnsupportedOperationException();
        }
        @Override
        public ByteBuffer asBlob() {
            throw new UnsupportedOperationException();
        }
        @Override
        public Message asMessage() {
            throw new UnsupportedOperationException();
        }
        @Override
        public EncodedImage asImage() {
            throw new UnsupportedOperationException();
        }
    }



    private static final class IdCodec {
        static final int LENGTH = Id.LENGTH;

        public static Id valueOf(byte[] bytes, int offset) {
            return new Id(bytes, offset);
        }

        public static void toBytes(Id id, ByteBuffer bb) {
            bb.put(id.bytes, id.offset, Id.LENGTH);
        }
    }


    private static final class ImageCodec {
        static final int H_F_WEBP = 1;
        static final int H_F_JPEG = 2;
        static final int H_F_PNG = 3;

        static final int H_O_REAR_FACING = 1;
        static final int H_O_FRONT_FACING = 2;


        public static EncodedImage valueOf(byte[] bytes, int offset) {
            EncodedImage.Format format;
            int c = offset;
            // skip bytes
            c += 4;
            switch (0xFF & bytes[c]) {
                case H_F_WEBP:
                    format = EncodedImage.Format.WEBP;
                    break;
                case H_F_JPEG:
                    format = EncodedImage.Format.JPEG;
                    break;
                case H_F_PNG:
                    format = EncodedImage.Format.PNG;
                    break;
                default:
                    throw new IllegalArgumentException();
            }
            c += 1;
            EncodedImage.Orientation orientation;
            switch (0xFF & bytes[c]) {
                case H_O_REAR_FACING:
                    orientation = EncodedImage.Orientation.REAR_FACING;
                    break;
                case H_O_FRONT_FACING:
                    orientation = EncodedImage.Orientation.FRONT_FACING;
                    break;
                default:
                    throw new IllegalArgumentException();
            }
            c += 1;
            int width = getint(bytes, c);
            c += 4;
            int height = getint(bytes, c);
            c += 4;
            int length = getint(bytes, c);
            c += 4;
            return new EncodedImage(format, orientation, width, height, bytes, c, length);
        }

        public static void toBytes(EncodedImage image, Lb lb, ByteBuffer bb) {
            bb.putInt(0);
            int i = bb.position();

            switch (image.format) {
                case WEBP:
                    bb.put((byte) H_F_WEBP);
                    break;
                case JPEG:
                    bb.put((byte) H_F_JPEG);
                    break;
                case PNG:
                    bb.put((byte) H_F_PNG);
                    break;
                default:
                    throw new IllegalArgumentException();
            }
            switch (image.orientation) {
                case REAR_FACING:
                    bb.put((byte) H_O_REAR_FACING);
                    break;
                case FRONT_FACING:
                    bb.put((byte) H_O_FRONT_FACING);
                    break;
                default:
                    throw new IllegalArgumentException();
            }
            bb.putInt(image.width);
            bb.putInt(image.height);
            bb.putInt(image.length);
            bb.put(image.bytes, image.offset, image.length);

            int bytes = bb.position() - i;
            bb.putInt(i - 4, bytes);
        }
    }

    private static final class MessageCodec {


        public static Message valueOf(byte[] bytes, int offset, CompressionState cs) {
            int c = offset;
            // skip bytes
            c += 4;

            Id id = IdCodec.valueOf(bytes, c);
            c += IdCodec.LENGTH;
            Id groupId = IdCodec.valueOf(bytes, c);
            c += IdCodec.LENGTH;
            int groupPriority = getint(bytes, c);
            c += 4;
            Route route = Route.valueOf(WireValue.valueOf(bytes, c, cs).asString());
            c += 5 + getint(bytes, c + 1);
            Map headers = WireValue.valueOf(bytes, c, cs).asMap();
            c += 5 + getint(bytes, c + 1);
            Map parameters = WireValue.valueOf(bytes, c, cs).asMap();
            return new Message(id, groupId, groupPriority, route, headers, parameters);
        }

        public static void toBytes(Message message, Lb lb, ByteBuffer bb) {
            bb.putInt(0);
            int i = bb.position();

            IdCodec.toBytes(message.id, bb);
            IdCodec.toBytes(message.groupId, bb);
            bb.putInt(message.groupPriority);
            // TODO (?) Route codec to avoid string conversion
            WireValue._toBytes(WireValue.of(message.route.toString()), lb, bb);
            WireValue._toBytes(WireValue.of(message.headers), lb, bb);
            WireValue._toBytes(WireValue.of(message.parameters), lb, bb);

            int bytes = bb.position() - i;
            bb.putInt(i - 4, bytes);
        }
    }


    public static boolean isWireValues(Collection values) {
        for (Object value : values) {
            if (!(value instanceof WireValue)) {
                return false;
            }
        }
        return true;
    }


    private static Map asWireValueMap(Map map) {
        if (isWireValues(map.keySet())) {
            // lazily transform the values
            return Maps.transformValues((Map) map, new Function() {
                @Override
                public WireValue apply(@Nullable Object input) {
                    return of(input);
                }
            });
        } else {
            Map t = new HashMap(map.size());
            for (Map.Entry e : map.entrySet()) {
                @Nullable Object j = t.put(of(e.getKey()), of(e.getValue()));
                if (null != j) {
                    throw new IllegalStateException("Map to WireValue is not a bijection.");
                }
            }
            return t;
        }
    }
    private static List asWireValueList(List list) {
        return Lists.transform(list, new Function() {
            @Override
            public WireValue apply(@Nullable Object input) {
                return of(input);
            }
        });
    }
    private static List asWireValueList(Collection collection) {
        List wireValueList = new ArrayList(collection.size());
        for (Object value : collection) {
            wireValueList.add(of(value));
        }
        return wireValueList;
    }
    private static Collection asWireValueCollection(Collection collection) {
        return Collections2.transform(collection, new Function() {
            @Override
            public WireValue apply(@Nullable Object input) {
                return of(input);
            }
        });
    }


    // FIXME config
    private static final boolean WV_MAP_STRICT_TYPES = false;

    private static WireValue duckCoerce(@Nullable Object arg) {
        WireValue value = WireValue.of(arg);
        if (value != arg && WV_MAP_STRICT_TYPES) {
            throw new IllegalArgumentException();
        }
        return value;
    }


    /** coerces arguments to {@link WireValue} where the {@link Map} interfaces
     * uses an object type (which won't trigger compile errors).
     * While not good practice to not use {@link WireValue} arguments,
     * the intention is clearly to use them and the program should work. */
    private static Map duckMap(final Map map) {
        return new ForwardingMap() {
            @Override
            protected Map delegate() {
                return map;
            }


            @Override
            public WireValue get(@Nullable Object key) {
                return super.get(duckCoerce(key));
            }

            @Override
            public WireValue remove(Object object) {
                return super.remove(duckCoerce(object));
            }

            @Override
            public boolean containsKey(@Nullable Object key) {
                return super.containsKey(duckCoerce(key));
            }

            @Override
            public boolean containsValue(@Nullable Object value) {
                return super.containsValue(duckCoerce(value));
            }
        };
    }

    private static List duckList(final List list) {
        return new ForwardingList() {
            @Override
            protected List delegate() {
                return list;
            }

            @Override
            public boolean remove(Object object) {
                return super.remove(duckCoerce(object));
            }

            @Override
            public boolean removeAll(Collection collection) {
                return super.removeAll(asWireValueCollection(collection));
            }

            @Override
            public boolean contains(Object object) {
                return super.contains(duckCoerce(object));
            }

            @Override
            public boolean containsAll(Collection collection) {
                return super.containsAll(asWireValueCollection(collection));
            }

            @Override
            public int indexOf(Object element) {
                return super.indexOf(duckCoerce(element));
            }

            @Override
            public int lastIndexOf(Object element) {
                return super.lastIndexOf(duckCoerce(element));
            }
        };
    }



    // wire utils

    public static int getint(byte[] bytes, int offset) {
        return ((0xFF & bytes[offset]) << 24)
                | ((0xFF & bytes[offset + 1]) << 16)
                | ((0xFF & bytes[offset + 2]) << 8)
                | (0xFF & bytes[offset + 3]);
    }
    public static long getlong(byte[] bytes, int offset) {
        return ((0xFFL & bytes[offset]) << 56)
                | ((0xFFL & bytes[offset + 1]) << 48)
                | ((0xFFL & bytes[offset + 2]) << 40)
                | ((0xFFL & bytes[offset + 3]) << 32)
                | ((0xFFL & bytes[offset + 4]) << 24)
                | ((0xFFL & bytes[offset + 5]) << 16)
                | ((0xFFL & bytes[offset + 6]) << 8)
                | (0xFFL & bytes[offset + 7]);
    }

    public static void putint(byte[] bytes, int offset, int value) {
        bytes[offset] = (byte) (value >>> 24);
        bytes[offset + 1] = (byte) (value >>> 16);
        bytes[offset + 2] = (byte) (value >>> 8);
        bytes[offset + 3] = (byte) value;
    }
    public static void putlong(byte[] bytes, int offset, long value) {
        bytes[offset] = (byte) (value >>> 56);
        bytes[offset + 1] = (byte) (value >>> 48);
        bytes[offset + 2] = (byte) (value >>> 40);
        bytes[offset + 3] = (byte) (value >>> 32);
        bytes[offset + 4] = (byte) (value >>> 24);
        bytes[offset + 5] = (byte) (value >>> 16);
        bytes[offset + 6] = (byte) (value >>> 8);
        bytes[offset + 7] = (byte) value;
    }




}