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

package org.apache.isis.core.commons.encoding;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Typesafe writing and reading of fields, providing some level of integrity
 * checking of encoded messages.
 * 
 * <p>
 * The {@link #write(DataOutputExtended, Object)} writes out field type and then
 * the data for that field type. The field type is represented by this
 * enumberation, with the {@link FieldType#getIdx() index} being what is written
 * to the stream (hence of type <tt>byte</tt> to keep small).
 * 
 * <p>
 * Conversely, the {@link #read(DataInputExtended)} reads the field type and
 * then the data for that field type.
 */
public abstract class FieldType<T> {

    private static Logger LOG = LoggerFactory.getLogger(FieldType.class);

    private static String LOG_INDENT = ". . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . ";
    private static final int NULL_BIT = 64; // 2 to the 6

    private static Map<Byte, FieldType<?>> cache = new HashMap<Byte, FieldType<?>>();
    private static int next = 0;

    static enum Indenting {
        INDENT_ONLY, INDENT_AND_OUTDENT;
    }

    public static FieldType<Boolean> BOOLEAN = new FieldType<Boolean>((byte) next++, Boolean.class, Indenting.INDENT_ONLY) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Boolean value) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeBoolean(value);
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected Boolean doRead(final DataInputExtended input) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final boolean value = inputStream.readBoolean();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                return value;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<boolean[]> BOOLEAN_ARRAY = new FieldType<boolean[]>((byte) next++, boolean[].class, Indenting.INDENT_AND_OUTDENT) {
        @Override
        protected void doWrite(final DataOutputExtended output, final boolean[] values) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeInt(values.length);
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(values.length);
                }
                for (int i = 0; i < values.length; i++) {
                    outputStream.writeBoolean(values[i]);
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected boolean[] doRead(final DataInputExtended input) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataInputStream inputStream = input.getDataInputStream();
                final int length = inputStream.readInt();
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(length);
                }
                final boolean[] values = new boolean[length];
                for (int i = 0; i < values.length; i++) {
                    values[i] = inputStream.readBoolean();
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
                return values;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<Byte> BYTE = new FieldType<Byte>((byte) next++, Byte.class, Indenting.INDENT_ONLY) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Byte value) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeByte(value.byteValue());
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected Byte doRead(final DataInputExtended input) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final byte value = inputStream.readByte();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                return value;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<byte[]> BYTE_ARRAY = new FieldType<byte[]>((byte) next++, byte[].class, Indenting.INDENT_AND_OUTDENT) {
        @Override
        protected void doWrite(final DataOutputExtended output, final byte[] values) throws IOException {
            try {
                final DataOutputStream outputStream = output.getDataOutputStream();
                final int length = values.length;
                outputStream.writeInt(length);
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append("length:").append(length).append(" [BYTE ARRAY]"));
                }

                // rather than looping through the array,
                // we take advantage of optimization built into DataOutputStream
                outputStream.write(values);
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected byte[] doRead(final DataInputExtended input) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final int length = inputStream.readInt();
                if (LOG.isDebugEnabled()) {
                    final StringBuilder msg = new StringBuilder().append("length:").append(length).append(" [BYTE ARRAY]");
                    log(this, msg);
                }

                final byte[] bytes = new byte[length];
                readBytes(inputStream, bytes);
                return bytes;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        // rather than looping through the array,
        // we take advantage of optimization built into DataInputStream
        private void readBytes(final DataInputStream inputStream, final byte[] bytes) throws IOException {
            inputStream.read(bytes);
        }
    };

    public static FieldType<Short> SHORT = new FieldType<Short>((byte) next++, Short.class, Indenting.INDENT_ONLY) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Short value) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeShort(value.shortValue());
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected Short doRead(final DataInputExtended input) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final short value = inputStream.readShort();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                return value;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<short[]> SHORT_ARRAY = new FieldType<short[]>((byte) next++, short[].class, Indenting.INDENT_AND_OUTDENT) {
        @Override
        protected void doWrite(final DataOutputExtended output, final short[] values) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeInt(values.length);
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(values.length);
                }

                for (int i = 0; i < values.length; i++) {
                    outputStream.writeShort(values[i]);
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected short[] doRead(final DataInputExtended input) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataInputStream inputStream = input.getDataInputStream();
                final int length = inputStream.readInt();
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(length);
                }

                final short[] values = new short[length];
                for (int i = 0; i < values.length; i++) {
                    values[i] = inputStream.readShort();
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
                return values;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<Integer> INTEGER = new FieldType<Integer>((byte) next++, Integer.class, Indenting.INDENT_ONLY) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Integer value) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeInt(value.intValue());
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected Integer doRead(final DataInputExtended input) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final int value = inputStream.readInt();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                return value;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<Integer> UNSIGNED_BYTE = new FieldType<Integer>((byte) next++, Integer.class, Indenting.INDENT_ONLY) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Integer value) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeByte(value);
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected Integer doRead(final DataInputExtended input) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final int value = inputStream.readUnsignedByte();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                return value;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<Integer> UNSIGNED_SHORT = new FieldType<Integer>((byte) next++, Integer.class, Indenting.INDENT_ONLY) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Integer value) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeShort(value);
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected Integer doRead(final DataInputExtended input) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final int value = inputStream.readUnsignedShort();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                return value;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<int[]> INTEGER_ARRAY = new FieldType<int[]>((byte) next++, int[].class, Indenting.INDENT_AND_OUTDENT) {
        @Override
        protected void doWrite(final DataOutputExtended output, final int[] values) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeInt(values.length);
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(values.length);
                }

                for (int i = 0; i < values.length; i++) {
                    outputStream.writeInt(values[i]);
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected int[] doRead(final DataInputExtended input) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataInputStream inputStream = input.getDataInputStream();
                final int length = inputStream.readInt();
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(length);
                }

                final int[] values = new int[length];
                for (int i = 0; i < values.length; i++) {
                    values[i] = inputStream.readInt();
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
                return values;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<Long> LONG = new FieldType<Long>((byte) next++, Long.class, Indenting.INDENT_ONLY) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Long value) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeLong(value.intValue());
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected Long doRead(final DataInputExtended input) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final long value = inputStream.readLong();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                return value;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };
    public static FieldType<long[]> LONG_ARRAY = new FieldType<long[]>((byte) next++, long[].class, Indenting.INDENT_AND_OUTDENT) {
        @Override
        protected void doWrite(final DataOutputExtended output, final long[] values) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeInt(values.length);
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(values.length);
                }

                for (int i = 0; i < values.length; i++) {
                    outputStream.writeLong(values[i]);
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected long[] doRead(final DataInputExtended input) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();

                final DataInputStream inputStream = input.getDataInputStream();
                final int length = inputStream.readInt();
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(length);
                }

                final long[] values = new long[length];
                for (int i = 0; i < values.length; i++) {
                    values[i] = inputStream.readLong();
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
                return values;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<Character> CHAR = new FieldType<Character>((byte) next++, Character.class, Indenting.INDENT_ONLY) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Character value) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeLong(value.charValue());
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected Character doRead(final DataInputExtended input) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final char value = inputStream.readChar();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                return value;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<char[]> CHAR_ARRAY = new FieldType<char[]>((byte) next++, char[].class, Indenting.INDENT_AND_OUTDENT) {
        // TODO: could perhaps optimize by writing out as a string
        @Override
        protected void doWrite(final DataOutputExtended output, final char[] values) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeInt(values.length);
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(values.length);
                }

                for (int i = 0; i < values.length; i++) {
                    outputStream.writeChar(values[i]);
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected char[] doRead(final DataInputExtended input) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataInputStream inputStream = input.getDataInputStream();
                final int length = inputStream.readInt();
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(length);
                }

                final char[] values = new char[length];
                for (int i = 0; i < values.length; i++) {
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                    values[i] = inputStream.readChar();
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
                return values;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<Float> FLOAT = new FieldType<Float>((byte) next++, Float.class, Indenting.INDENT_ONLY) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Float value) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeFloat(value);
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected Float doRead(final DataInputExtended input) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final float value = inputStream.readFloat();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                return value;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<float[]> FLOAT_ARRAY = new FieldType<float[]>((byte) next++, float[].class, Indenting.INDENT_AND_OUTDENT) {
        @Override
        protected void doWrite(final DataOutputExtended output, final float[] values) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeInt(values.length);
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(values.length);
                }

                for (int i = 0; i < values.length; i++) {
                    outputStream.writeFloat(values[i]);
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected float[] doRead(final DataInputExtended input) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataInputStream inputStream = input.getDataInputStream();
                final int length = inputStream.readInt();
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(length);
                }

                final float[] values = new float[length];
                for (int i = 0; i < values.length; i++) {
                    values[i] = inputStream.readFloat();
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
                return values;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<Double> DOUBLE = new FieldType<Double>((byte) next++, Double.class, Indenting.INDENT_ONLY) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Double value) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeDouble(value);
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected Double doRead(final DataInputExtended input) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final double value = inputStream.readDouble();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                return value;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<double[]> DOUBLE_ARRAY = new FieldType<double[]>((byte) next++, double[].class, Indenting.INDENT_AND_OUTDENT) {
        @Override
        protected void doWrite(final DataOutputExtended output, final double[] values) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeInt(values.length);
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(values.length);
                }

                for (int i = 0; i < values.length; i++) {
                    outputStream.writeDouble(values[i]);
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected double[] doRead(final DataInputExtended input) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataInputStream inputStream = input.getDataInputStream();
                final int length = inputStream.readInt();
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(length);
                }

                final double[] values = new double[length];
                for (int i = 0; i < values.length; i++) {
                    values[i] = inputStream.readDouble();
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
                return values;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<String> STRING = new FieldType<String>((byte) next++, String.class, Indenting.INDENT_ONLY) {
        @Override
        protected void doWrite(final DataOutputExtended output, final String value) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeUTF(value);
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected String doRead(final DataInputExtended input) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final String value = inputStream.readUTF();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(value));
                }
                return value;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };
    public static FieldType<String[]> STRING_ARRAY = new FieldType<String[]>((byte) next++, String[].class, Indenting.INDENT_AND_OUTDENT) {
        @Override
        protected void doWrite(final DataOutputExtended output, final String[] values) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeInt(values.length);
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(values.length);
                }

                for (int i = 0; i < values.length; i++) {
                    // using FieldType to write out takes care of null handling
                    FieldType.STRING.write(output, values[i]);
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected String[] doRead(final DataInputExtended input) throws IOException {
            try {
                final StringBuilder buf = new StringBuilder();
                final DataInputStream inputStream = input.getDataInputStream();
                final int length = inputStream.readInt();
                if (LOG.isDebugEnabled()) {
                    buf.append("length: ").append(length);
                }

                final String[] values = new String[length];
                for (int i = 0; i < values.length; i++) {
                    // using FieldType to read in takes care of null handling
                    values[i] = FieldType.STRING.read(input);
                    if (LOG.isDebugEnabled()) {
                        buf.append(i == 0 ? ": " : ", ");
                        buf.append(values[i]);
                    }
                }
                if (LOG.isDebugEnabled()) {
                    log(this, buf);
                }
                return values;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }
    };

    public static FieldType<Encodable> ENCODABLE = new FieldType<Encodable>((byte) next++, Encodable.class, Indenting.INDENT_AND_OUTDENT) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Encodable encodable) throws IOException {
            try {
                // write out class
                final String className = encodable.getClass().getName();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(className));
                }
                output.writeUTF(className);

                // recursively encode
                encodable.encode(output);
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected Encodable doRead(final DataInputExtended input) throws IOException {
            try {
                // read in class name ...
                final String className = input.readUTF();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append(className));
                }

                Class<?> cls;
                try {
                    // ...obtain constructor
                    cls = Thread.currentThread().getContextClassLoader().loadClass(className);

                    final Constructor<?> constructor = cls.getConstructor(new Class[] { DataInputExtended.class });

                    // recursively decode
                    return (Encodable) constructor.newInstance(new Object[] { input });
                } catch (final ClassNotFoundException ex) {
                    throw new FailedToDecodeException(ex);
                } catch (final IllegalArgumentException ex) {
                    throw new FailedToDecodeException(ex);
                } catch (final InstantiationException ex) {
                    throw new FailedToDecodeException(ex);
                } catch (final IllegalAccessException ex) {
                    throw new FailedToDecodeException(ex);
                } catch (final InvocationTargetException ex) {
                    throw new FailedToDecodeException(ex);
                } catch (final SecurityException ex) {
                    throw new FailedToDecodeException(ex);
                } catch (final NoSuchMethodException ex) {
                    throw new FailedToDecodeException(ex);
                }

            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected boolean checksStream() {
            return false;
        }
    };

    public static FieldType<Encodable[]> ENCODABLE_ARRAY = new FieldType<Encodable[]>((byte) next++, Encodable[].class, Indenting.INDENT_AND_OUTDENT) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Encodable[] values) throws IOException {
            try {
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeInt(values.length);
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append("length: ").append(values.length));
                }
                for (final Encodable encodable : values) {
                    // using FieldType to write out takes care of null handling
                    FieldType.ENCODABLE.write(output, encodable);
                }
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @SuppressWarnings("unchecked")
        @Override
        protected <Q> Q[] doReadArray(final DataInputExtended input, final Class<Q> elementType) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final int length = inputStream.readInt();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append("length: ").append(length));
                }

                final Q[] values = (Q[]) Array.newInstance(elementType, length);
                for (int i = 0; i < values.length; i++) {
                    // using FieldType to read in takes care of null handling
                    values[i] = (Q) FieldType.ENCODABLE.read(input);
                }
                return values;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected boolean checksStream() {
            return false;
        }
    };

    public static FieldType<Serializable> SERIALIZABLE = new FieldType<Serializable>((byte) next++, Serializable.class, Indenting.INDENT_ONLY) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Serializable value) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append("[SERIALIZABLE]"));
                }

                // write out as blob of bytes
                final ObjectOutputStream oos = new ObjectOutputStream(output.getDataOutputStream());
                oos.writeObject(value);
                oos.flush();
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected Serializable doRead(final DataInputExtended input) throws IOException {
            try {
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append("[SERIALIZABLE]"));
                }

                // read in a blob of bytes
                final ObjectInputStream ois = new ObjectInputStream(input.getDataInputStream());
                try {
                    return (Serializable) ois.readObject();
                } catch (final ClassNotFoundException ex) {
                    throw new FailedToDeserializeException(ex);
                }
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected boolean checksStream() {
            return false;
        }
    };

    public static FieldType<Serializable[]> SERIALIZABLE_ARRAY = new FieldType<Serializable[]>((byte) next++, Serializable[].class, Indenting.INDENT_AND_OUTDENT) {
        @Override
        protected void doWrite(final DataOutputExtended output, final Serializable[] values) throws IOException {
            try {
                final DataOutputStream outputStream = output.getDataOutputStream();
                outputStream.writeInt(values.length);
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append("length: ").append(values.length));
                }

                for (final Serializable value : values) {
                    // using FieldType to write out takes care of null handling
                    FieldType.SERIALIZABLE.write(output, value);
                }
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        @SuppressWarnings("unchecked")
        protected <Q> Q[] doReadArray(final DataInputExtended input, final Class<Q> elementType) throws IOException {
            try {
                final DataInputStream inputStream = input.getDataInputStream();
                final int length = inputStream.readInt();
                if (LOG.isDebugEnabled()) {
                    log(this, new StringBuilder().append("length: ").append(length));
                }

                final Q[] values = (Q[]) Array.newInstance(elementType, length);
                for (int i = 0; i < values.length; i++) {
                    // using FieldType to read in takes care of null handling
                    values[i] = (Q) FieldType.SERIALIZABLE.read(input);
                }
                return values;
            } finally {
                if (LOG.isDebugEnabled()) {
                    unlog(this);
                }
            }
        }

        @Override
        protected boolean checksStream() {
            return false;
        }
    };

    public static FieldType<?> get(final byte idx) {
        return cache.get(idx);
    }

    private final byte idx;
    private final Class<T> cls;
    private final Indenting indenting;

    private FieldType(final byte idx, final Class<T> cls, final Indenting indenting) {
        this.idx = idx;
        this.cls = cls;
        this.indenting = indenting;
        cache.put(idx, this);
    }

    public byte getIdx() {
        return idx;
    }

    public Class<T> getCls() {
        return cls;
    }

    /**
     * Whether this implementation checks ordering in the stream.
     * 
     * <p>
     * Broadly, the type safe ones do, the {@link Encodable} and
     * {@link Serializable} ones do not.
     */
    protected boolean checksStream() {
        return true;
    }

    public final T read(final DataInputExtended input) throws IOException {
        final DataInputStream inputStream = input.getDataInputStream();
        final byte fieldTypeIdxAndNullability = inputStream.readByte();

        final boolean isNull = fieldTypeIdxAndNullability >= NULL_BIT;
        final byte fieldTypeIdx = (byte) (fieldTypeIdxAndNullability - (isNull ? NULL_BIT : 0));
        try {
            final FieldType<?> fieldType = FieldType.get(fieldTypeIdx);
            if (fieldType == null || (fieldType.checksStream() && fieldType != this)) {
                throw new IllegalStateException("Mismatch in stream: expected " + this + " but got " + fieldType + " (" + fieldTypeIdx + ")");
            }

            if (isNull && LOG.isDebugEnabled()) {
                // only log if reading a null; otherwise actual value read
                // logged later
                log(this, new StringBuilder().append("(null)"));
            }

            if (isNull) {
                return null;
            } else {
                return doRead(input);
            }
        } finally {
            if (isNull && LOG.isDebugEnabled()) {
                // only unlog if reading a null
                unlog(this);
            }
        }
    }

    public final <Q> Q[] readArray(final DataInputExtended input, final Class<Q> elementType) throws IOException {
        final DataInputStream inputStream = input.getDataInputStream();
        final byte fieldTypeIdxAndNullability = inputStream.readByte();

        final boolean isNull = fieldTypeIdxAndNullability >= NULL_BIT;
        final byte fieldTypeIdx = (byte) (fieldTypeIdxAndNullability - (isNull ? NULL_BIT : 0));
        try {
            final FieldType<?> fieldType = FieldType.get(fieldTypeIdx);
            if (fieldType.checksStream() && fieldType != this) {
                throw new IllegalStateException("Mismatch in stream: expected " + this + " but got " + fieldType);
            }

            if (isNull && LOG.isDebugEnabled()) {
                // only log if reading a null; otherwise actual value read
                // logged later
                log(this, new StringBuilder().append("(null)"));
            }

            if (isNull) {
                return null;
            } else {
                return doReadArray(input, elementType);
            }

        } finally {
            if (isNull && LOG.isDebugEnabled()) {
                // only unlog if reading a null
                unlog(this);
            }
        }

    }

    public final void write(final DataOutputExtended output, final T value) throws IOException {
        byte fieldTypeIdxAndNullability = getIdx();
        final boolean isNull = value == null;
        if (isNull) {
            // set high order bit
            fieldTypeIdxAndNullability += NULL_BIT;
        }
        try {

            final DataOutputStream outputStream = output.getDataOutputStream();

            outputStream.write(fieldTypeIdxAndNullability);
            if (isNull && LOG.isDebugEnabled()) {
                // only log if writing a null; otherwise actual value logged
                // later
                log(this, new StringBuilder().append("(null)"));
            }

            if (!isNull) {
                doWrite(output, value);
            }
        } finally {
            if (isNull && LOG.isDebugEnabled()) {
                // only unlog if writing a null
                unlog(this);
            }
        }
    }

    protected T doRead(final DataInputExtended input) throws IOException {
        throw new UnsupportedOperationException("not supported for this field type");
    }

    protected <Q> Q[] doReadArray(final DataInputExtended input, final Class<Q> elementType) throws IOException {
        throw new UnsupportedOperationException("not supported for this field type");
    }

    protected abstract void doWrite(DataOutputExtended output, T value) throws IOException;

    private boolean isIndentingAndOutdenting() {
        return indenting == Indenting.INDENT_AND_OUTDENT;
    }

    // ///////////////////////////////////////////////////////
    // debugging
    // ///////////////////////////////////////////////////////

    private static ThreadLocal<int[]> debugIndent = new ThreadLocal<int[]>();

    private static void log(final FieldType<?> fieldType, final StringBuilder buf) {
        buf.insert(0, ": ");
        buf.insert(0, fieldType);
        if (fieldType.isIndentingAndOutdenting()) {
            buf.insert(0, "> ");
        }
        buf.insert(0, spaces(currentDebugLevel()));
        incrementDebugLevel();
        LOG.debug(buf.toString());
    }

    private static void unlog(final FieldType<?> fieldType) {
        unlog(fieldType, new StringBuilder());
    }

    private static void unlog(final FieldType<?> fieldType, final StringBuilder buf) {
        if (fieldType.isIndentingAndOutdenting()) {
            buf.insert(0, "< ");
        }
        decrementDebugLevel();
        if (fieldType.isIndentingAndOutdenting()) {
            buf.insert(0, spaces(currentDebugLevel()));
            LOG.debug(buf.toString());
        }
    }

    private static String spaces(final int num) {
        return LOG_INDENT.substring(0, num);
    }

    private static int currentDebugLevel() {
        return debugIndent()[0];
    }

    private static void incrementDebugLevel() {
        final int[] indentLevel = debugIndent();
        indentLevel[0] += 2;
    }

    private static void decrementDebugLevel() {
        final int[] indentLevel = debugIndent();
        indentLevel[0] -= 2;
    }

    private static int[] debugIndent() {
        int[] indentLevel = debugIndent.get();
        if (indentLevel == null) {
            indentLevel = new int[1];
            debugIndent.set(indentLevel);
        }
        return indentLevel;
    }

    // ///////////////////////////////////////////////////////
    // toString
    // ///////////////////////////////////////////////////////

    @Override
    public String toString() {
        return getCls().getSimpleName();
    }

}
