/*
 * 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.ignite.codegen;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import org.apache.ignite.internal.GridCodegenConverter;
import org.apache.ignite.internal.GridDirectCollection;
import org.apache.ignite.internal.GridDirectMap;
import org.apache.ignite.internal.GridDirectTransient;
import org.apache.ignite.internal.IgniteCodeGeneratingFail;
import org.apache.ignite.internal.processors.affinity.AffinityTopologyVersion;
import org.apache.ignite.internal.processors.cache.mvcc.DeadlockProbe;
import org.apache.ignite.internal.processors.cache.mvcc.ProbedTx;
import org.apache.ignite.internal.util.IgniteUtils;
import org.apache.ignite.internal.util.typedef.internal.SB;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.lang.IgniteUuid;
import org.apache.ignite.plugin.extensions.communication.Message;
import org.apache.ignite.plugin.extensions.communication.MessageCollectionItemType;
import org.jetbrains.annotations.Nullable;

import static java.lang.reflect.Modifier.isStatic;
import static java.lang.reflect.Modifier.isTransient;

/**
* Direct marshallable code generator.
*/
public class MessageCodeGenerator {
    /** */
    private static final Comparator<Field> FIELD_CMP = new Comparator<Field>() {
        @Override public int compare(Field f1, Field f2) {
            return f1.getName().compareTo(f2.getName());
        }
    };

    /** */
    public static final String DFLT_SRC_DIR = U.getIgniteHome() + "/modules/core/src/main/java";

    /** */
    public static final String INDEXING_SRC_DIR = U.getIgniteHome() + "/modules/indexing/src/main/java";

    /** */
    public static final String CALCITE_SRC_DIR = U.getIgniteHome() + "/modules/calcite/src/main/java";

    /** */
    private static final Class<?> BASE_CLS = Message.class;

    /** */
    private static final String EMPTY = "";

    /** */
    public static final String TAB = "    ";

    /** */
    private static final String BUF_VAR = "buf";

    /** */
    private static final Map<Class<?>, MessageCollectionItemType> TYPES = U.newHashMap(30);

    static {
        TYPES.put(byte.class, MessageCollectionItemType.BYTE);
        TYPES.put(Byte.class, MessageCollectionItemType.BYTE);
        TYPES.put(short.class, MessageCollectionItemType.SHORT);
        TYPES.put(Short.class, MessageCollectionItemType.SHORT);
        TYPES.put(int.class, MessageCollectionItemType.INT);
        TYPES.put(Integer.class, MessageCollectionItemType.INT);
        TYPES.put(long.class, MessageCollectionItemType.LONG);
        TYPES.put(Long.class, MessageCollectionItemType.LONG);
        TYPES.put(float.class, MessageCollectionItemType.FLOAT);
        TYPES.put(Float.class, MessageCollectionItemType.FLOAT);
        TYPES.put(double.class, MessageCollectionItemType.DOUBLE);
        TYPES.put(Double.class, MessageCollectionItemType.DOUBLE);
        TYPES.put(char.class, MessageCollectionItemType.CHAR);
        TYPES.put(Character.class, MessageCollectionItemType.CHAR);
        TYPES.put(boolean.class, MessageCollectionItemType.BOOLEAN);
        TYPES.put(Boolean.class, MessageCollectionItemType.BOOLEAN);
        TYPES.put(byte[].class, MessageCollectionItemType.BYTE_ARR);
        TYPES.put(short[].class, MessageCollectionItemType.SHORT_ARR);
        TYPES.put(int[].class, MessageCollectionItemType.INT_ARR);
        TYPES.put(long[].class, MessageCollectionItemType.LONG_ARR);
        TYPES.put(float[].class, MessageCollectionItemType.FLOAT_ARR);
        TYPES.put(double[].class, MessageCollectionItemType.DOUBLE_ARR);
        TYPES.put(char[].class, MessageCollectionItemType.CHAR_ARR);
        TYPES.put(boolean[].class, MessageCollectionItemType.BOOLEAN_ARR);
        TYPES.put(String.class, MessageCollectionItemType.STRING);
        TYPES.put(BitSet.class, MessageCollectionItemType.BIT_SET);
        TYPES.put(UUID.class, MessageCollectionItemType.UUID);
        TYPES.put(IgniteUuid.class, MessageCollectionItemType.IGNITE_UUID);
        TYPES.put(AffinityTopologyVersion.class, MessageCollectionItemType.AFFINITY_TOPOLOGY_VERSION);
    }

    /**
     * @param cls Class.
     * @return Type enum value.
     */
    private static MessageCollectionItemType typeEnum(Class<?> cls) {
        MessageCollectionItemType type = TYPES.get(cls);

        if (type == null) {
            assert Message.class.isAssignableFrom(cls) : cls;

            type = MessageCollectionItemType.MSG;
        }

        return type;
    }

    /** */
    private final Collection<String> write = new ArrayList<>();

    /** */
    private final Collection<String> read = new ArrayList<>();

    /** */
    private final Map<Class<?>, Integer> fieldCnt = new HashMap<>();

    /** */
    private final String srcDir;

    /** */
    private int totalFieldCnt;

    /** */
    private List<Field> fields;

    /** */
    private int indent;

    /**
     * @param args Arguments.
     * @throws Exception In case of error.
     */
    public static void main(String[] args) throws Exception {
        String srcDir = DFLT_SRC_DIR;

        if (args != null && args.length > 0)
            srcDir = args[0];

        MessageCodeGenerator gen = new MessageCodeGenerator(srcDir);

        gen.generateAndWrite(ProbedTx.class);
        gen.generateAndWrite(DeadlockProbe.class);

//        gen.generateAll(true);

//        gen.generateAndWrite(GridCacheMessage.class);

//        gen.generateAndWrite(GridMessageCollection.class);
//        gen.generateAndWrite(DataStreamerEntry.class);

//        gen.generateAndWrite(GridDistributedLockRequest.class);
//        gen.generateAndWrite(GridDistributedLockResponse.class);
//        gen.generateAndWrite(GridNearLockRequest.class);
//        gen.generateAndWrite(GridNearLockResponse.class);
//        gen.generateAndWrite(GridDhtLockRequest.class);
//        gen.generateAndWrite(GridDhtLockResponse.class);
//
//        gen.generateAndWrite(GridDistributedTxPrepareRequest.class);
//        gen.generateAndWrite(GridDistributedTxPrepareResponse.class);
//        gen.generateAndWrite(GridNearTxPrepareRequest.class);
//        gen.generateAndWrite(GridNearTxPrepareResponse.class);
//        gen.generateAndWrite(GridDhtTxPrepareRequest.class);
//        gen.generateAndWrite(GridDhtTxPrepareResponse.class);
//
//        gen.generateAndWrite(GridDistributedTxFinishRequest.class);
//        gen.generateAndWrite(GridDistributedTxFinishResponse.class);
//        gen.generateAndWrite(GridNearTxFinishRequest.class);
//        gen.generateAndWrite(GridNearTxFinishResponse.class);
//        gen.generateAndWrite(GridDhtTxFinishRequest.class);
//        gen.generateAndWrite(GridDhtTxFinishResponse.class);
//
//        gen.generateAndWrite(IncrementalSnapshotAwareMessage.class);

//        gen.generateAndWrite(GridCacheTxRecoveryRequest.class);
//        gen.generateAndWrite(GridCacheTxRecoveryResponse.class);

//        gen.generateAndWrite(GridQueryCancelRequest.class);
//        gen.generateAndWrite(GridQueryFailResponse.class);
//        gen.generateAndWrite(GridQueryNextPageRequest.class);
//        gen.generateAndWrite(GridQueryNextPageResponse.class);
//        gen.generateAndWrite(GridQueryRequest.class);
//        gen.generateAndWrite(GridCacheSqlQuery.class);

//        gen.generateAndWrite(GridH2Null.class);
//        gen.generateAndWrite(GridH2Boolean.class);
//        gen.generateAndWrite(GridH2Byte.class);
//        gen.generateAndWrite(GridH2Short.class);
//        gen.generateAndWrite(GridH2Integer.class);
//        gen.generateAndWrite(GridH2Long.class);
//        gen.generateAndWrite(GridH2Decimal.class);
//        gen.generateAndWrite(GridH2Double.class);
//        gen.generateAndWrite(GridH2Float.class);
//        gen.generateAndWrite(GridH2Time.class);
//        gen.generateAndWrite(GridH2Date.class);
//        gen.generateAndWrite(GridH2Timestamp.class);
//        gen.generateAndWrite(GridH2Bytes.class);
//        gen.generateAndWrite(GridH2String.class);
//        gen.generateAndWrite(GridH2Array.class);
//        gen.generateAndWrite(GridH2JavaObject.class);
//        gen.generateAndWrite(GridH2Uuid.class);
//        gen.generateAndWrite(GridH2Geometry.class);
//        gen.generateAndWrite(GridH2CacheObject.class);
//        gen.generateAndWrite(GridH2IndexRangeRequest.class);
//        gen.generateAndWrite(GridH2IndexRangeResponse.class);
//        gen.generateAndWrite(GridH2RowRange.class);
//        gen.generateAndWrite(GridH2RowRangeBounds.class);
//        gen.generateAndWrite(GridH2QueryRequest.class);
//        gen.generateAndWrite(GridH2RowMessage.class);
//        gen.generateAndWrite(GridCacheVersion.class);
//        gen.generateAndWrite(GridCacheVersionEx.class);
//        gen.generateAndWrite(GridH2DmlRequest.class);
//        gen.generateAndWrite(GridH2DmlResponse.class);
//        gen.generateAndWrite(GridNearTxEnlistRequest.class);
//        gen.generateAndWrite(GridNearTxEnlistResponse.class);
//        gen.generateAndWrite(GenerateEncryptionKeyRequest.class);
//        gen.generateAndWrite(GenerateEncryptionKeyResponse.class);
    }

    /**
     * @param srcDir Source directory.
     */
    public MessageCodeGenerator(String srcDir) {
        this.srcDir = srcDir;
    }

    /**
     * Generates code for all classes.
     *
     * @param write Whether to write to file.
     * @throws Exception In case of error.
     */
    public void generateAll(boolean write) throws Exception {
        Collection<Class<? extends Message>> classes = classes();

        for (Class<? extends Message> cls : classes) {
            try {
                boolean isAbstract = Modifier.isAbstract(cls.getModifiers());

                System.out.println("Processing class: " + cls.getName() + (isAbstract ? " (abstract)" : ""));

                if (write)
                    generateAndWrite(cls);
                else
                    generate(cls);
            }
            catch (IllegalStateException e) {
                System.out.println("Will skip class generation [cls=" + cls + ", err=" + e.getMessage() + ']');
            }
        }
    }

    /**
     * Generates code for provided class and writes it to source file.
     * Note: this method must be called only from {@code generateAll(boolean)}
     * and only with updating {@code CLASSES_ORDER_FILE} and other auto generated files.
     *
     * @param cls Class.
     * @throws Exception In case of error.
     */
    @SuppressWarnings("ConstantConditions")
    public void generateAndWrite(Class<? extends Message> cls) throws Exception {
        assert cls != null;

        generate(cls);

        File file = new File(srcDir, cls.getName().replace('.', File.separatorChar) + ".java");

        if (!file.exists() || !file.isFile()) {
            System.out.println("Source file not found: " + file.getPath());

            return;
        }

        Collection<String> src = new ArrayList<>();

        BufferedReader rdr = null;

        try {
            rdr = new BufferedReader(new FileReader(file));

            String line;
            boolean skip = false;

            boolean writeFound = false;
            boolean readFound = false;
            boolean fieldCntFound = false;

            while ((line = rdr.readLine()) != null) {
                if (!skip) {
                    src.add(line);

                    if (line.contains("public boolean writeTo(ByteBuffer buf, MessageWriter writer)")) {
                        src.addAll(write);

                        skip = true;

                        writeFound = true;
                    }
                    else if (line.contains("public boolean readFrom(ByteBuffer buf, MessageReader reader)")) {
                        src.addAll(read);

                        skip = true;

                        readFound = true;
                    }
                    else if (line.contains("public byte fieldsCount()")) {
                        src.add(TAB + TAB + "return " + totalFieldCnt + ";");

                        skip = true;

                        fieldCntFound = true;
                    }
                }
                else if (line.startsWith(TAB + "}")) {
                    src.add(line);

                    skip = false;
                }
            }

            if (!writeFound)
                System.out.println("    writeTo method doesn't exist.");

            if (!readFound)
                System.out.println("    readFrom method doesn't exist.");

            if (!fieldCntFound)
                System.out.println("    fieldCount method doesn't exist.");
        }
        finally {
            if (rdr != null)
                rdr.close();
        }

        BufferedWriter wr = null;

        try {
            wr = new BufferedWriter(new FileWriter(file));

            for (String line : src)
                wr.write(line + '\n');
        }
        finally {
            if (wr != null)
                wr.close();
        }
    }

    /**
     * Generates code for provided class.
     *
     * @param cls Class.
     * @throws Exception In case of error.
     */
    private void generate(Class<? extends Message> cls) throws Exception {
        assert cls != null;

        if (cls.isInterface())
            return;

        if (cls.isAnnotationPresent(IgniteCodeGeneratingFail.class))
            throw new IllegalStateException("@IgniteCodeGeneratingFail is provided for class: " + cls.getName());

        write.clear();
        read.clear();

        fields = new ArrayList<>();

        Field[] declaredFields = cls.getDeclaredFields();

        for (Field field : declaredFields) {
            int mod = field.getModifiers();

            if (!isStatic(mod) && !isTransient(mod) && !field.isAnnotationPresent(GridDirectTransient.class))
                fields.add(field);
        }

        Collections.sort(fields, FIELD_CMP);

        int state = startState(cls);

        totalFieldCnt = state + fields.size();

        indent = 2;

        boolean hasSuper = cls.getSuperclass() != Object.class;

        start(write, hasSuper ? "writeTo" : null, true);
        start(read, hasSuper ? "readFrom" : null, false);

        indent++;

        for (Field field : fields)
            processField(field, state++);

        indent--;

        finish(write, null);
        finish(read, cls.getSimpleName());
    }

    /**
     * @param cls Message class.
     * @return Start state.
     */
    private int startState(Class<?> cls) {
        assert cls != null;

        Class<?> superCls = cls.getSuperclass();

        Integer state = fieldCnt.get(superCls);

        if (state != null)
            return state;

        state = 0;

        while (cls.getSuperclass() != Object.class) {
            cls = cls.getSuperclass();

            for (Field field : cls.getDeclaredFields()) {
                int mod = field.getModifiers();

                if (!isStatic(mod) && !isTransient(mod) && !field.isAnnotationPresent(GridDirectTransient.class))
                    state++;
            }
        }

        fieldCnt.put(superCls, state);

        return state;
    }

    /**
     * @param code Code lines.
     * @param superMtd Super class method name.
     * @param write Whether write code is generated.
     */
    private void start(Collection<String> code, @Nullable String superMtd, boolean write) {
        assert code != null;

        code.add(builder().a(write ? "writer" : "reader").a(".setBuffer(").a(BUF_VAR).a(");").toString());
        code.add(EMPTY);

        if (!write) {
            code.add(builder().a("if (!reader.beforeMessageRead())").toString());

            indent++;

            code.add(builder().a("return false;").toString());
            code.add(EMPTY);

            indent--;
        }

        if (superMtd != null) {
            if (write)
                returnFalseIfFailed(code, "super." + superMtd, BUF_VAR, "writer");
            else
                returnFalseIfFailed(code, "super." + superMtd, BUF_VAR, "reader");

            code.add(EMPTY);
        }

        if (write) {
            code.add(builder().a("if (!writer.isHeaderWritten()) {").toString());

            indent++;

            returnFalseIfFailed(code, "writer.writeHeader", "directType()", "fieldsCount()");

            code.add(EMPTY);
            code.add(builder().a("writer.onHeaderWritten();").toString());

            indent--;

            code.add(builder().a("}").toString());
            code.add(EMPTY);
        }

        if (!fields.isEmpty())
            code.add(builder().a("switch (").a(write ? "writer.state()" : "reader.state()").a(") {").toString());
    }

    /**
     * @param code Code lines.
     */
    private void finish(Collection<String> code, String readClsName) {
        assert code != null;

        if (!fields.isEmpty()) {
            code.add(builder().a("}").toString());
            code.add(EMPTY);
        }

        if (readClsName == null)
            code.add(builder().a("return true;").toString());
        else
            code.add(builder().a("return reader.afterMessageRead(").a(readClsName).a(".class);").toString());
    }

    /**
     * @param field Field.
     * @param opt Case option.
     */
    private void processField(Field field, int opt) {
        assert field != null;
        assert opt >= 0;

        GridDirectCollection colAnn = field.getAnnotation(GridDirectCollection.class);
        GridDirectMap mapAnn = field.getAnnotation(GridDirectMap.class);

        if (colAnn == null && Collection.class.isAssignableFrom(field.getType()))
            throw new IllegalStateException("@GridDirectCollection annotation is not provided for field: " +
                field.getName());

        if (mapAnn == null && Map.class.isAssignableFrom(field.getType()))
            throw new IllegalStateException("@GridDirectMap annotation is not provided for field: " + field.getName());

        writeField(field, opt, colAnn, mapAnn);
        readField(field, opt, colAnn, mapAnn);
    }

    /**
     * @param field Field.
     * @param opt Case option.
     * @param colAnn Collection annotation.
     * @param mapAnn Map annotation.
     */
    private void writeField(Field field, int opt, @Nullable GridDirectCollection colAnn,
        @Nullable GridDirectMap mapAnn) {
        assert field != null;
        assert opt >= 0;

        write.add(builder().a("case ").a(opt).a(":").toString());

        indent++;

        GridCodegenConverter fldPreproc = field.getAnnotation(GridCodegenConverter.class);

        String getExp = (fldPreproc != null && !fldPreproc.get().isEmpty()) ? fldPreproc.get() : field.getName();
        Class<?> writeType = (fldPreproc != null && !fldPreproc.type().equals(GridCodegenConverter.Default.class)) ?
            fldPreproc.type() : field.getType();

        returnFalseIfWriteFailed(writeType, field.getName(), colAnn != null ? colAnn.value() : null,
            mapAnn != null ? mapAnn.keyType() : null, mapAnn != null ? mapAnn.valueType() : null, false, getExp);

        write.add(EMPTY);
        write.add(builder().a("writer.incrementState();").toString());
        write.add(EMPTY);

        indent--;
    }

    /**
     * @param field Field.
     * @param opt Case option.
     * @param colAnn Collection annotation.
     * @param mapAnn Map annotation.
     */
    private void readField(Field field, int opt, @Nullable GridDirectCollection colAnn,
        @Nullable GridDirectMap mapAnn) {
        assert field != null;
        assert opt >= 0;

        read.add(builder().a("case ").a(opt).a(":").toString());

        indent++;

        GridCodegenConverter fldPreproc = field.getAnnotation(GridCodegenConverter.class);
        String setExp = (fldPreproc != null && !fldPreproc.get().isEmpty()) ? fldPreproc.set() : "";
        Class<?> writeType = (fldPreproc != null && !fldPreproc.type().equals(GridCodegenConverter.Default.class)) ?
            fldPreproc.type() : field.getType();

        returnFalseIfReadFailed(writeType, field.getName(), colAnn != null ? colAnn.value() : null,
            mapAnn != null ? mapAnn.keyType() : null, mapAnn != null ? mapAnn.valueType() : null, setExp);

        read.add(EMPTY);
        read.add(builder().a("reader.incrementState();").toString());
        read.add(EMPTY);

        indent--;
    }

    /**
     * @param type Field type.
     * @param name Field name.
     * @param colItemType Collection item type.
     * @param mapKeyType Map key type.
     * @param mapValType Map key value.
     * @param raw Raw write flag.
     */
    private void returnFalseIfWriteFailed(Class<?> type, String name, @Nullable Class<?> colItemType,
        @Nullable Class<?> mapKeyType, @Nullable Class<?> mapValType, boolean raw, String getExpr) {
        assert type != null;
        assert name != null;

        String field = raw ? "null" : '"' + name + '"';

        if (type == byte.class)
            returnFalseIfFailed(write, "writer.writeByte", field, getExpr);
        else if (type == short.class)
            returnFalseIfFailed(write, "writer.writeShort", field, getExpr);
        else if (type == int.class)
            returnFalseIfFailed(write, "writer.writeInt", field, getExpr);
        else if (type == long.class)
            returnFalseIfFailed(write, "writer.writeLong", field, getExpr);
        else if (type == float.class)
            returnFalseIfFailed(write, "writer.writeFloat", field, getExpr);
        else if (type == double.class)
            returnFalseIfFailed(write, "writer.writeDouble", field, getExpr);
        else if (type == char.class)
            returnFalseIfFailed(write, "writer.writeChar", field, getExpr);
        else if (type == boolean.class)
            returnFalseIfFailed(write, "writer.writeBoolean", field, getExpr);
        else if (type == byte[].class)
            returnFalseIfFailed(write, "writer.writeByteArray", field, getExpr);
        else if (type == short[].class)
            returnFalseIfFailed(write, "writer.writeShortArray", field, getExpr);
        else if (type == int[].class)
            returnFalseIfFailed(write, "writer.writeIntArray", field, getExpr);
        else if (type == long[].class)
            returnFalseIfFailed(write, "writer.writeLongArray", field, getExpr);
        else if (type == float[].class)
            returnFalseIfFailed(write, "writer.writeFloatArray", field, getExpr);
        else if (type == double[].class)
            returnFalseIfFailed(write, "writer.writeDoubleArray", field, getExpr);
        else if (type == char[].class)
            returnFalseIfFailed(write, "writer.writeCharArray", field, getExpr);
        else if (type == boolean[].class)
            returnFalseIfFailed(write, "writer.writeBooleanArray", field, getExpr);
        else if (type == String.class)
            returnFalseIfFailed(write, "writer.writeString", field, getExpr);
        else if (type == BitSet.class)
            returnFalseIfFailed(write, "writer.writeBitSet", field, getExpr);
        else if (type == UUID.class)
            returnFalseIfFailed(write, "writer.writeUuid", field, getExpr);
        else if (type == IgniteUuid.class)
            returnFalseIfFailed(write, "writer.writeIgniteUuid", field, getExpr);
        else if (type == AffinityTopologyVersion.class)
            returnFalseIfFailed(write, "writer.writeAffinityTopologyVersion", field, getExpr);
        else if (type.isEnum()) {
            String arg = getExpr + " != null ? (byte)" + getExpr + ".ordinal() : -1";

            returnFalseIfFailed(write, "writer.writeByte", field, arg);
        }
        else if (BASE_CLS.isAssignableFrom(type))
            returnFalseIfFailed(write, "writer.writeMessage", field, getExpr);
        else if (type.isArray()) {
            returnFalseIfFailed(write, "writer.writeObjectArray", field, getExpr,
                "MessageCollectionItemType." + typeEnum(type.getComponentType()));
        }
        else if (Collection.class.isAssignableFrom(type) && !Set.class.isAssignableFrom(type)) {
            assert colItemType != null;

            returnFalseIfFailed(write, "writer.writeCollection", field, getExpr,
                "MessageCollectionItemType." + typeEnum(colItemType));
        }
        else if (Map.class.isAssignableFrom(type)) {
            assert mapKeyType != null;
            assert mapValType != null;

            returnFalseIfFailed(write, "writer.writeMap", field, getExpr,
                "MessageCollectionItemType." + typeEnum(mapKeyType),
                "MessageCollectionItemType." + typeEnum(mapValType));
        }
        else
            throw new IllegalStateException("Unsupported type: " + type);
    }

    /**
     * @param type Field type.
     * @param name Field name.
     * @param colItemType Collection item type.
     * @param mapKeyType Map key type.
     * @param mapValType Map value type.
     */
    private void returnFalseIfReadFailed(Class<?> type, @Nullable String name, @Nullable Class<?> colItemType,
        @Nullable Class<?> mapKeyType, @Nullable Class<?> mapValType, String setExpr) {
        assert type != null;

        String field = '"' + name + '"';

        if (type == byte.class)
            returnFalseIfReadFailed(name, "reader.readByte", setExpr, field);
        else if (type == short.class)
            returnFalseIfReadFailed(name, "reader.readShort", setExpr, field);
        else if (type == int.class)
            returnFalseIfReadFailed(name, "reader.readInt", setExpr, field);
        else if (type == long.class)
            returnFalseIfReadFailed(name, "reader.readLong", setExpr, field);
        else if (type == float.class)
            returnFalseIfReadFailed(name, "reader.readFloat", setExpr, field);
        else if (type == double.class)
            returnFalseIfReadFailed(name, "reader.readDouble", setExpr, field);
        else if (type == char.class)
            returnFalseIfReadFailed(name, "reader.readChar", setExpr, field);
        else if (type == boolean.class)
            returnFalseIfReadFailed(name, "reader.readBoolean", setExpr, field);
        else if (type == byte[].class)
            returnFalseIfReadFailed(name, "reader.readByteArray", setExpr, field);
        else if (type == short[].class)
            returnFalseIfReadFailed(name, "reader.readShortArray", setExpr, field);
        else if (type == int[].class)
            returnFalseIfReadFailed(name, "reader.readIntArray", setExpr, field);
        else if (type == long[].class)
            returnFalseIfReadFailed(name, "reader.readLongArray", setExpr, field);
        else if (type == float[].class)
            returnFalseIfReadFailed(name, "reader.readFloatArray", setExpr, field);
        else if (type == double[].class)
            returnFalseIfReadFailed(name, "reader.readDoubleArray", setExpr, field);
        else if (type == char[].class)
            returnFalseIfReadFailed(name, "reader.readCharArray", setExpr, field);
        else if (type == boolean[].class)
            returnFalseIfReadFailed(name, "reader.readBooleanArray", setExpr, field);
        else if (type == String.class)
            returnFalseIfReadFailed(name, "reader.readString", setExpr, field);
        else if (type == BitSet.class)
            returnFalseIfReadFailed(name, "reader.readBitSet", setExpr, field);
        else if (type == UUID.class)
            returnFalseIfReadFailed(name, "reader.readUuid", setExpr, field);
        else if (type == IgniteUuid.class)
            returnFalseIfReadFailed(name, "reader.readIgniteUuid", setExpr, field);
        else if (type == AffinityTopologyVersion.class)
            returnFalseIfReadFailed(name, "reader.readAffinityTopologyVersion", setExpr, field);
        else if (type.isEnum()) {
            String loc = name + "Ord";

            read.add(builder().a("byte ").a(loc).a(";").toString());
            read.add(EMPTY);

            returnFalseIfReadFailed(loc, "reader.readByte", setExpr, field);

            read.add(EMPTY);
            read.add(builder().a(name).a(" = ").a(type.getSimpleName()).a(".fromOrdinal(").a(loc).a(");").toString());
        }
        else if (BASE_CLS.isAssignableFrom(type))
            returnFalseIfReadFailed(name, "reader.readMessage", setExpr, field);
        else if (type.isArray()) {
            Class<?> compType = type.getComponentType();

            returnFalseIfReadFailed(name, "reader.readObjectArray", setExpr, field,
                "MessageCollectionItemType." + typeEnum(compType),
                compType.getSimpleName() + ".class");
        }
        else if (Collection.class.isAssignableFrom(type) && !Set.class.isAssignableFrom(type)) {
            assert colItemType != null;

            returnFalseIfReadFailed(name, "reader.readCollection", setExpr, field,
                "MessageCollectionItemType." + typeEnum(colItemType));
        }
        else if (Map.class.isAssignableFrom(type)) {
            assert mapKeyType != null;
            assert mapValType != null;

            boolean linked = type.equals(LinkedHashMap.class);

            returnFalseIfReadFailed(name, "reader.readMap", setExpr, field,
                "MessageCollectionItemType." + typeEnum(mapKeyType),
                "MessageCollectionItemType." + typeEnum(mapValType),
                linked ? "true" : "false");
        }
        else
            throw new IllegalStateException("Unsupported type: " + type);
    }

    /**
     * @param var Variable name.
     * @param mtd Method name.
     * @param args Method arguments.
     */
    private void returnFalseIfReadFailed(String var, String mtd, String setConverter, @Nullable String... args) {
        assert mtd != null;

        String argsStr = "";

        if (args != null && args.length > 0) {
            for (String arg : args)
                argsStr += arg + ", ";

            argsStr = argsStr.substring(0, argsStr.length() - 2);
        }

        if (setConverter.isEmpty())
            read.add(builder().a(var).a(" = ").a(mtd).a("(").a(argsStr).a(");").toString());
        else {
            read.add(builder().a(var).a(" = ").a(setConverter
                .replace("$val$", new SB().a(mtd).a("(").a(argsStr).a(")").toString())).a(";").toString());
        }
        read.add(EMPTY);

        read.add(builder().a("if (!reader.isLastRead())").toString());

        indent++;

        read.add(builder().a("return false;").toString());

        indent--;
    }

    /**
     * @param code Code lines.
     * @param accessor Field or method name.
     * @param args Method arguments.
     */
    private void returnFalseIfFailed(Collection<String> code, String accessor, @Nullable String... args) {
        assert code != null;
        assert accessor != null;

        String argsStr = "";

        if (args != null && args.length > 0) {
            for (String arg : args)
                argsStr += arg + ", ";

            argsStr = argsStr.substring(0, argsStr.length() - 2);
        }

        code.add(builder().a("if (!").a(accessor).a("(").a(argsStr).a("))").toString());

        indent++;

        code.add(builder().a("return false;").toString());

        indent--;
    }

    /**
     * Creates new builder with correct indent.
     *
     * @return Builder.
     */
    private SB builder() {
        assert indent > 0;

        SB sb = new SB();

        for (int i = 0; i < indent; i++)
            sb.a(TAB);

        return sb;
    }

    /**
     * Gets all direct marshallable classes.
     * First classes will be classes from {@code classesOrder} with same order
     * as ordered values. Other classes will be at the end and ordered by name
     * (with package prefix).
     * That orders need for saving {@code directType} value.
     *
     * @return Classes.
     * @throws Exception In case of error.
     */
    private Collection<Class<? extends Message>> classes() throws Exception {
        Collection<Class<? extends Message>> col = new TreeSet<>(
            new Comparator<Class<? extends Message>>() {
                @Override public int compare(Class<? extends Message> c1,
                    Class<? extends Message> c2) {
                    return c1.getName().compareTo(c2.getName());
                }
            });

        ClassLoader ldr = getClass().getClassLoader();

        for (URL url : IgniteUtils.classLoaderUrls(ldr)) {
            File file = new File(url.toURI());

            int prefixLen = file.getPath().length() + 1;

            processFile(file, ldr, prefixLen, col);
        }

        return col;
    }

    /**
     * Recursively process provided file or directory.
     *
     * @param file File.
     * @param ldr Class loader.
     * @param prefixLen Path prefix length.
     * @param col Classes.
     * @throws Exception In case of error.
     */
    @SuppressWarnings("unchecked")
    private void processFile(File file, ClassLoader ldr, int prefixLen,
        Collection<Class<? extends Message>> col) throws Exception {
        assert file != null;
        assert ldr != null;
        assert prefixLen > 0;
        assert col != null;

        if (!file.exists())
            throw new FileNotFoundException("File doesn't exist: " + file);

        if (file.isDirectory()) {
            for (File f : file.listFiles())
                processFile(f, ldr, prefixLen, col);
        }
        else {
            assert file.isFile();

            String path = file.getPath();

            if (path.endsWith(".class")) {
                String clsName = path.substring(prefixLen, path.length() - 6).replace(File.separatorChar, '.');

                Class<?> cls = Class.forName(clsName, false, ldr);

                if (cls.getDeclaringClass() == null && cls.getEnclosingClass() == null &&
                    !BASE_CLS.equals(cls) && BASE_CLS.isAssignableFrom(cls))
                    col.add((Class<? extends Message>)cls);
            }
        }
    }
}
