public abstract class AdapterGenerator {

    /**
     * Gets the JSON name for the element the name passed to
     * {@link SerializedName} will be used. If the element is
     * not annotated with {@link SerializedName}, the variable
     * name is used.
     *
     * @param element the element to get the name for.
     * @return a non null string to use as the JSON key.
     */
    @NotNull
    static String getJsonName(@NotNull Element element) {

        String name = element.getAnnotation(SerializedName.class) != null
                ? element.getAnnotation(SerializedName.class).value()
                : null;

        if (name == null || name.isEmpty()) {
            name = element.getSimpleName().toString();
        }
        return name;
    }

    /**
     * Returns the alternate names for the {@link Element}.
     *
     * @param element the element to check for alternate names.
     * @return an array of alternate names, or null if there are none.
     */
    @Nullable
    static String[] getAlternateJsonNames(@NotNull Element element) {
        return element.getAnnotation(SerializedName.class) != null
                ? element.getAnnotation(SerializedName.class).alternate()
                : null;
    }

    /**
     * Creates a {@link TypeSpec} for the the type adapter.
     *
     * @param stagGenerator the generator for the Stag class.
     * @return A non null {@link TypeSpec} describing the adapter class.
     */
    @NotNull
    public abstract TypeSpec createTypeAdapterSpec(@NotNull StagGenerator stagGenerator);

    /**
     * Creates a TypeToken field in the generated adapter factory
     *
     * @param typeMirror Type of class for which TypeToken has to be generated
     * @return {@link FieldSpec}
     */
    @NotNull
    static FieldSpec createTypeTokenSpec(@NotNull TypeMirror typeMirror) {
        ParameterizedTypeName typeTokenType = ParameterizedTypeName.get(ClassName.get(TypeToken.class), TypeVariableName.get(typeMirror));
        FieldSpec.Builder typeTokenBuilder = FieldSpec.builder(typeTokenType, "TYPE_TOKEN", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL);
        typeTokenBuilder.initializer("TypeToken.get(" + typeMirror.toString() + ".class)");
        return typeTokenBuilder.build();
    }
} public class EnumTypeAdapterGenerator extends AdapterGenerator {

    @NotNull
    private final ClassInfo mInfo;

    @NotNull
    private final TypeElement mElement;

    public EnumTypeAdapterGenerator(@NotNull ClassInfo info, @NotNull TypeElement element) {
        mInfo = info;
        mElement = element;
    }

    @NotNull
    private static MethodSpec getWriteMethodSpec(@NotNull TypeName typeName) {
        return MethodSpec.methodBuilder("write")
                .addParameter(JsonWriter.class, "writer")
                .addParameter(typeName, "object")
                .returns(void.class)
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addException(IOException.class)
                .addStatement("writer.value(object == null ? null : CONSTANT_TO_NAME.get(object))")
                .build();
    }

    @NotNull
    private static MethodSpec getReadMethodSpec(@NotNull TypeName typeName) {
        return MethodSpec.methodBuilder("read")
                .addParameter(JsonReader.class, "reader")
                .returns(typeName)
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addException(IOException.class)
                .beginControlFlow("if (reader.peek() == com.google.gson.stream.JsonToken.NULL)")
                .addStatement("reader.nextNull()")
                .addStatement("return null")
                .endControlFlow()
                .addStatement("return NAME_TO_CONSTANT.get(reader.nextString())")
                .build();
    }

    /**
     * Generates the TypeSpec for the TypeAdapter
     * that this enum generates.
     *
     * @return a valid TypeSpec that can be written
     * to a file or added to another class.
     */
    @Override
    @NotNull
    public TypeSpec createTypeAdapterSpec(@NotNull StagGenerator stagGenerator) {
        TypeMirror typeMirror = mInfo.getType();
        TypeName typeVariableName = TypeVariableName.get(typeMirror);

        MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(Gson.class, "gson");

        String className = FileGenUtils.unescapeEscapedString(mInfo.getTypeAdapterClassName());
        TypeSpec.Builder adapterBuilder = TypeSpec.classBuilder(className)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .superclass(ParameterizedTypeName.get(ClassName.get(TypeAdapter.class), typeVariableName));


        Map<String, Element> nameToConstant = new LinkedHashMap<>();
        Map<Element, String> constantToName = new LinkedHashMap<>();

        for (Element enclosingElement : mElement.getEnclosedElements()) {
            if (enclosingElement.getKind() == ElementKind.ENUM_CONSTANT) {
                String name = getJsonName(enclosingElement);
                nameToConstant.put(name, enclosingElement);
                constantToName.put(enclosingElement, name);

                String[] alternateJsonNames = getAlternateJsonNames(enclosingElement);
                if (alternateJsonNames != null && alternateJsonNames.length > 0) {
                    for (String alternate : alternateJsonNames) {
                        nameToConstant.put(alternate, enclosingElement);
                    }
                }
            }
        }


        MethodSpec writeMethod = getWriteMethodSpec(typeVariableName);
        MethodSpec readMethod = getReadMethodSpec(typeVariableName);

        adapterBuilder.addField(createTypeTokenSpec(typeMirror));

        TypeName typeName =
                ParameterizedTypeName.get(ClassName.get(HashMap.class), TypeVariableName.get(String.class),
                                          TypeVariableName.get(typeMirror));
        adapterBuilder.addField(typeName, "NAME_TO_CONSTANT", Modifier.PRIVATE, Modifier.STATIC,
                                Modifier.FINAL);

        typeName = ParameterizedTypeName.get(ClassName.get(HashMap.class), TypeVariableName.get(typeMirror),
                                             TypeVariableName.get(String.class));
        adapterBuilder.addField(typeName, "CONSTANT_TO_NAME", Modifier.PRIVATE, Modifier.STATIC,
                                Modifier.FINAL);

        CodeBlock.Builder staticBlockBuilder = CodeBlock.builder();
        staticBlockBuilder.addStatement("NAME_TO_CONSTANT = new HashMap<>(" + nameToConstant.size() + ")");
        for (Map.Entry<String, Element> entry : nameToConstant.entrySet()) {
            staticBlockBuilder.addStatement(
                    "NAME_TO_CONSTANT.put(\"" + entry.getKey() + "\", " + typeVariableName + "." +
                    entry.getValue().getSimpleName().toString() + ")");
        }

        staticBlockBuilder.add("\n");
        staticBlockBuilder.addStatement("CONSTANT_TO_NAME = new HashMap<>(" + constantToName.size() + ")");
        for (Map.Entry<Element, String> entry : constantToName.entrySet()) {
            staticBlockBuilder.addStatement("CONSTANT_TO_NAME.put(" + typeVariableName + "." +
                                            entry.getKey().getSimpleName().toString() + ", \"" +
                                            entry.getValue() + "\")");
        }

        adapterBuilder.addStaticBlock(staticBlockBuilder.build());

        adapterBuilder.addMethod(constructorBuilder.build());
        adapterBuilder.addMethod(writeMethod);
        adapterBuilder.addMethod(readMethod);

        return adapterBuilder.build();
    }
} public class StagFactoryGenerator {

    @NotNull public static final String NAME = "StagFactory";
    @NotNull private final List<ClassInfo> mClassInfoList;
    @NotNull private final String mFileName;

    public StagFactoryGenerator(@NotNull List<ClassInfo> classInfoList, @NotNull String fileName) {
        mClassInfoList = new ArrayList<>(classInfoList);
        mFileName = fileName;
    }

    @NotNull
    public TypeSpec getTypeAdapterFactorySpec() {
        TypeSpec.Builder adapterBuilder = TypeSpec.classBuilder(mFileName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addSuperinterface(TypeAdapterFactory.class)
                .addMethod(getCreateMethodSpec());

        return adapterBuilder.build();
    }

    @NotNull
    private MethodSpec getCreateMethodSpec() {
        TypeVariableName genericType = TypeVariableName.get("T");
        AnnotationSpec suppressedWarnings = AnnotationSpec.builder(SuppressWarnings.class)
                .addMember("value", "\"unchecked\"")
                .addMember("value", "\"rawtypes\"")
                .build();
        MethodSpec.Builder builder = MethodSpec.methodBuilder("create")
                .addTypeVariable(genericType)
                .addParameter(Gson.class, "gson")
                .addParameter(ParameterizedTypeName.get(ClassName.get(TypeToken.class), genericType), "type")
                .returns(ParameterizedTypeName.get(ClassName.get(TypeAdapter.class), genericType))
                .addAnnotation(suppressedWarnings)
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addCode("Class<? super T> clazz = type.getRawType();\n");

        for (ClassInfo classInfo : mClassInfoList) {
            builder.beginControlFlow("if (clazz == " + classInfo.getClassAndPackage() + ".class)");
            List<? extends TypeMirror> typeArguments = classInfo.getTypeArguments();
            if (typeArguments == null || typeArguments.isEmpty()) {
                builder.addStatement("return (TypeAdapter<T>) new " + classInfo.getTypeAdapterQualifiedClassName() + "(gson)");
            } else {
                builder.addStatement("java.lang.reflect.Type parameters = type.getType()");
                builder.beginControlFlow("if (parameters instanceof java.lang.reflect.ParameterizedType)");
                builder.addStatement(
                        "java.lang.reflect.ParameterizedType parameterizedType = (java.lang.reflect.ParameterizedType) parameters");
                builder.addStatement(
                        "java.lang.reflect.Type[] parametersType = parameterizedType.getActualTypeArguments()");
                String statement = "return (TypeAdapter<T>) new " + classInfo.getTypeAdapterQualifiedClassName() + "(gson";
                for (int idx = 0; idx < typeArguments.size(); idx++) {
                    statement += ", parametersType[" + idx + "]";
                }
                statement += ")";
                builder.addStatement(statement);
                builder.endControlFlow();
                builder.beginControlFlow("else");
                builder.addStatement("TypeToken objectToken = TypeToken.get(Object.class)");
                statement = "return (TypeAdapter<T>) new " + classInfo.getTypeAdapterQualifiedClassName() + "(gson";
                for (int idx = 0; idx < typeArguments.size(); idx++) {
                    statement += ", objectToken.getType()";
                }
                statement += ")";
                builder.addStatement(statement);
                builder.endControlFlow();
            }
            builder.endControlFlow();
        }

        builder.addCode("return null;\n");

        return builder.build();
    }
} public class StagGenerator {

    @NotNull
    private static final String CLASS_STAG = "Stag";
    @NotNull
    private static final String CLASS_TYPE_ADAPTER_FACTORY = "Factory";

    @NotNull
    private final Map<String, ClassInfo> mKnownClasses;

    public StagGenerator(@NotNull Set<TypeMirror> knownTypes) {
        mKnownClasses = new LinkedHashMap<>(knownTypes.size());

        for (TypeMirror knownType : knownTypes) {
            if (!TypeUtils.isAbstract(knownType)) {
                ClassInfo classInfo = new ClassInfo(knownType);
                mKnownClasses.put(knownType.toString(), classInfo);
            }
        }
    }

    @Nullable
    ClassInfo getKnownClass(@NotNull TypeMirror typeMirror) {
        return mKnownClasses.get(typeMirror.toString());
    }

    /**
     * Generates the public API in the form of the {@code Stag.Factory} type adapter factory
     * for the annotated classes. Creates the spec for the class.
     *
     * @param generatedStagFactoryWrappers List of Sub Factories that have been created
     * @return A non null TypeSpec for the factory class.
     */
    @NotNull
    public static TypeSpec createStagSpec(List<SubFactoriesInfo> generatedStagFactoryWrappers) {
        TypeSpec.Builder stagBuilder =
                TypeSpec.classBuilder(CLASS_STAG).addModifiers(Modifier.PUBLIC, Modifier.FINAL);
        stagBuilder.addType(getAdapterFactorySpec(generatedStagFactoryWrappers));

        return stagBuilder.build();
    }

    @NotNull
    private static TypeSpec getAdapterFactorySpec(@NotNull List<SubFactoriesInfo> generatedStagFactoryWrappers) {
        TypeVariableName genericTypeName = TypeVariableName.get("T");

        TypeSpec.Builder adapterFactoryBuilder = TypeSpec.classBuilder(CLASS_TYPE_ADAPTER_FACTORY)
                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                .addSuperinterface(TypeAdapterFactory.class);

        ParameterizedTypeName hashMapOfStringToInteger = ParameterizedTypeName.get(HashMap.class, String.class, Integer.class);
        FieldSpec.Builder packageToIndexMapField = FieldSpec.builder(hashMapOfStringToInteger,
                                                                     "packageToIndexMap", Modifier.FINAL, Modifier.PRIVATE).initializer("new " + hashMapOfStringToInteger.toString() + "( " + generatedStagFactoryWrappers.size() + ")");
        adapterFactoryBuilder.addField(packageToIndexMapField.build());

        TypeVariableName typeAdapterFactoryArray = TypeVariableName.get("TypeAdapterFactory[]");
        FieldSpec.Builder typeAdapterFactoryArrayField = FieldSpec.builder(typeAdapterFactoryArray,
                                                                           "typeAdapterFactoryArray", Modifier.FINAL, Modifier.PRIVATE).initializer("new TypeAdapterFactory[" + generatedStagFactoryWrappers.size() + "]");
        adapterFactoryBuilder.addField(typeAdapterFactoryArrayField.build());

        MethodSpec.Builder getPackageNameMethodBuilder = MethodSpec.methodBuilder("getPackageName")
                .addTypeVariable(genericTypeName)
                .returns(String.class)
                .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
                .addParameter(ParameterizedTypeName.get(ClassName.get(Class.class), genericTypeName), "clazz")
                .addStatement("String name = clazz.getName()")
                .addStatement("int last = name.lastIndexOf('.')")
                .addStatement("return last == -1 ? null : name.substring(0, last)");
        adapterFactoryBuilder.addMethod(getPackageNameMethodBuilder.build());

        MethodSpec.Builder createTypeAdapterFactoryMethodBuilder = MethodSpec.methodBuilder("createTypeAdapterFactory")
                .returns(TypeAdapterFactory.class)
                .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
                .addParameter(int.class, "index")
                .addStatement("TypeAdapterFactory result = null");

        SwitchCodeBlockBuilder factorySwitchBuilder = new SwitchCodeBlockBuilder()
                .beginSwitch("switch(index)");

        for (int index = 0; index < generatedStagFactoryWrappers.size(); index++) {
            factorySwitchBuilder
                    .beginCase("case $L", index)
                    .addStatement("result = new $L()", generatedStagFactoryWrappers.get(index).classAndPackageName)
                    .endCase();
        }

        factorySwitchBuilder.endSwitch();

        createTypeAdapterFactoryMethodBuilder
                .addCode(factorySwitchBuilder.build())
                .addStatement("return result");
        adapterFactoryBuilder.addMethod(createTypeAdapterFactoryMethodBuilder.build());

        MethodSpec.Builder getTypeAdapterFactoryMethodBuilder = MethodSpec.methodBuilder("getTypeAdapterFactory")
                .returns(TypeAdapterFactory.class)
                .addModifiers(Modifier.PRIVATE)
                .addParameter(int.class, "index")
                .addStatement("TypeAdapterFactory typeAdapterFactory = typeAdapterFactoryArray[index]")
                .beginControlFlow("if (typeAdapterFactory == null)")
                .addStatement("typeAdapterFactory = createTypeAdapterFactory(index)")
                .addStatement("typeAdapterFactoryArray[index] = typeAdapterFactory")
                .endControlFlow()
                .addStatement("return typeAdapterFactory");
        adapterFactoryBuilder.addMethod(getTypeAdapterFactoryMethodBuilder.build());

        MethodSpec.Builder getTypeAdapterMethodBuilder = MethodSpec.methodBuilder("getTypeAdapterFactory")
                .returns(ClassName.get(TypeAdapterFactory.class))
                .addModifiers(Modifier.PRIVATE)
                .addParameter(ParameterizedTypeName.get(ClassName.get(Class.class), TypeVariableName.get("?")), "clazz")
                .addParameter(String.class, "currentPackageName")
                .addParameter(int.class, "index")
                .addStatement("String packageName = getPackageName(clazz)")
                .addStatement("packageToIndexMap.put(packageName, index)")
                .beginControlFlow("if (currentPackageName.equals(packageName))")
                .addStatement("return getTypeAdapterFactory(index)")
                .endControlFlow()
                .addStatement("return null");
        adapterFactoryBuilder.addMethod(getTypeAdapterMethodBuilder.build());

        MethodSpec.Builder getSubTypeAdapterMethodBuilder = MethodSpec.methodBuilder("getSubFactory")
                .returns(ClassName.get(TypeAdapterFactory.class))
                .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
                                       .addMember("value", "\"fallthrough\"")
                                       .build())
                .addModifiers(Modifier.PRIVATE, Modifier.SYNCHRONIZED)
                .addParameter(String.class, "currentPackageName")
                .addStatement("Integer index = packageToIndexMap.get(currentPackageName);")
                .beginControlFlow("if (index != null)")
                .addStatement("TypeAdapterFactory typeAdapterFactory = getTypeAdapterFactory(index)")
                .addStatement("return typeAdapterFactory")
                .endControlFlow()
                .addStatement("TypeAdapterFactory result = null");

        SwitchCodeBlockBuilder subFactorySwitchBuilder = new SwitchCodeBlockBuilder()
                .beginSwitch("switch(packageToIndexMap.size())");

        for (int index = 0; index < generatedStagFactoryWrappers.size(); index++) {
            subFactorySwitchBuilder
                    .beginCase("case $L", index)
                    .addStatement("result = getTypeAdapterFactory($L.class, currentPackageName, $L)", generatedStagFactoryWrappers.get(index).representativeClassInfo.getClassAndPackage(), index)
                    .beginControlFlow("if (result != null)")
                    .addStatement("return result")
                    .endControlFlow();
        }

        subFactorySwitchBuilder
                .beginCase("default")
                .addStatement("return null")
                .endSwitch();

        getSubTypeAdapterMethodBuilder.addCode(subFactorySwitchBuilder.build());

        adapterFactoryBuilder.addMethod(getSubTypeAdapterMethodBuilder.build());

        String suppressWarningValue = "value";
        MethodSpec.Builder createMethodBuilder = MethodSpec.methodBuilder("create")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
                                       .addMember(suppressWarningValue, "\"unchecked\"")
                                       .addMember(suppressWarningValue, "\"rawtypes\"")
                                       .build())
                .addTypeVariable(genericTypeName)
                .returns(ParameterizedTypeName.get(ClassName.get(TypeAdapter.class), genericTypeName))
                .addParameter(Gson.class, "gson")
                .addParameter(ParameterizedTypeName.get(ClassName.get(TypeToken.class), genericTypeName),
                              "type")

                .addStatement("Class<? super T> clazz = type.getRawType()")
                .addStatement("String currentPackageName = getPackageName(clazz)")
                .beginControlFlow("if (currentPackageName == null)")
                .addStatement("return null")
                .endControlFlow()
                .addStatement("TypeAdapterFactory typeAdapterFactory = getSubFactory(currentPackageName)")
                .addStatement("return typeAdapterFactory != null ? typeAdapterFactory.create(gson, type) : null");

        adapterFactoryBuilder.addMethod(createMethodBuilder.build());

        return adapterFactoryBuilder.build();
    }

    public static class SubFactoriesInfo {

        @NotNull final ClassInfo representativeClassInfo;

        @NotNull final String classAndPackageName;

        public SubFactoriesInfo(@NotNull ClassInfo classInfo, @NotNull String classAndPackageName) {
            this.representativeClassInfo = classInfo;
            this.classAndPackageName = classAndPackageName;
        }
    }
} public static class SubFactoriesInfo {

    @NotNull final ClassInfo representativeClassInfo;

    @NotNull final String classAndPackageName;

    public SubFactoriesInfo(@NotNull ClassInfo classInfo, @NotNull String classAndPackageName) {
        this.representativeClassInfo = classInfo;
        this.classAndPackageName = classAndPackageName;
    }
} public class TypeAdapterGenerator extends AdapterGenerator {

    private static final String TYPE_ADAPTER_FIELD_PREFIX = "mTypeAdapter";

    @NotNull
    private final ClassInfo mInfo;
    @NotNull
    private final SupportedTypesModel mSupportedTypesModel;
    private boolean mEnableSerializeNulls;

    public TypeAdapterGenerator(@NotNull SupportedTypesModel supportedTypesModel, @NotNull ClassInfo info, boolean enableSerializeNulls) {
        mSupportedTypesModel = supportedTypesModel;
        mInfo = info;
        mEnableSerializeNulls = enableSerializeNulls;
    }

    @NotNull
    private static TypeMirror getReplacedTypeMirror(@NotNull TypeMirror type, @NotNull Map<TypeMirror, TypeMirror> fieldTypeVarsMap) {
        if (type.getKind() == TypeKind.TYPEVAR) {
            return fieldTypeVarsMap.get(type);
        } else if (type instanceof DeclaredType) {
            DeclaredType declaredType = (DeclaredType) type;
            List<? extends TypeMirror> typeMirrors = declaredType.getTypeArguments();
            TypeMirror[] args = new TypeMirror[typeMirrors.size()];
            for (int idx = 0; idx < typeMirrors.size(); idx++) {
                args[idx] = getReplacedTypeMirror(typeMirrors.get(idx), fieldTypeVarsMap);
                idx++;
            }
            return TypeUtils.getDeclaredType(ElementUtils.getTypeElementFromQualifiedName(declaredType.asElement().toString()), args);
        } else {
            return type;
        }
    }

    /**
     * This is used to generate the type token code for the types that are unknown.
     */
    @Nullable
    private static String getTypeTokenCode(@NotNull TypeMirror fieldType,
                                           @NotNull StagGenerator stagGenerator,
                                           @NotNull Map<TypeMirror, String> typeVarsMap,
                                           @NotNull AdapterFieldInfo adapterFieldInfo) {
        if (fieldType.getKind() == TypeKind.TYPEVAR) {
            return adapterFieldInfo.updateAndGetTypeTokenFieldName(fieldType, "(com.google.gson.reflect.TypeToken<" + fieldType.toString() + ">) com.google.gson.reflect.TypeToken.get(" + typeVarsMap.get(fieldType) + ")");
        } else if (!TypeUtils.isParameterizedType(fieldType)) {
            ClassInfo classInfo = stagGenerator.getKnownClass(fieldType);
            if (classInfo != null) {
                return classInfo.getTypeAdapterQualifiedClassName() + ".TYPE_TOKEN";
            } else {
                return adapterFieldInfo.updateAndGetTypeTokenFieldName(fieldType, "com.google.gson.reflect.TypeToken.get(" + fieldType.toString() + ".class)");
            }
        } else if (fieldType instanceof DeclaredType) {
            /*
             * If it is of ParameterizedType, {@link com.vimeo.stag.utils.ParameterizedTypeUtil} is used to get the
             * type token of the parameter type.
             */
            DeclaredType declaredFieldType = (DeclaredType) fieldType;
            List<? extends TypeMirror> typeMirrors = ((DeclaredType) fieldType).getTypeArguments();
            StringBuilder result = new StringBuilder("(com.google.gson.reflect.TypeToken<" + fieldType.toString() + ">)com.google.gson.reflect.TypeToken.getParameterized(" +
                    declaredFieldType.asElement().toString() + ".class");

            /*
             * Iterate through all the types from the typeArguments and generate type token code accordingly
             */
            Map<TypeMirror, TypeMirror> fieldTypeVarsMap = new LinkedHashMap<>(typeMirrors.size());
            List<? extends TypeMirror> classArguments = TypeUtils.getTypeArguments(declaredFieldType.asElement().asType());

            int paramIndex = 0;
            for (TypeMirror parameterTypeMirror : typeMirrors) {
                TypeMirror classArg = classArguments != null && classArguments.size() > paramIndex
                        ? classArguments.get(paramIndex)
                        : null;
                if (classArg != null) {
                    fieldTypeVarsMap.put(classArg, parameterTypeMirror);
                }
                if (parameterTypeMirror.getKind() == TypeKind.WILDCARD) {
                    String upperBoundString = "";
                    if (classArg != null && classArg.getKind() == TypeKind.TYPEVAR) {
                        TypeVariable typeVariable = (TypeVariable) classArg;
                        TypeMirror upperBound = typeVariable.getUpperBound();
                        if (TypeUtils.isParameterizedType(upperBound)) {
                            TypeMirror replacedQualifiedType = getReplacedTypeMirror(upperBound, fieldTypeVarsMap);
                            fieldTypeVarsMap.put(classArg, replacedQualifiedType);
                            upperBoundString += getTypeTokenCode(replacedQualifiedType, stagGenerator, typeVarsMap, adapterFieldInfo) + ".getType()";
                        } else {
                            upperBoundString += upperBound.toString() + ".class";
                            fieldTypeVarsMap.put(classArg, upperBound);
                        }
                    }
                    result.append(", " + "com.vimeo.stag.Types.getWildcardType(new java.lang.reflect.Type[]{").append(upperBoundString).append("} , new java.lang.reflect.Type[]{})");
                } else if (parameterTypeMirror.getKind() != TypeKind.TYPEVAR && !TypeUtils.isParameterizedType(parameterTypeMirror)) {
                    // Optimize so that we do not have to call TypeToken.getType()
                    // When the class is non parametrized and we can call xxxxx.class directly
                    result.append(", ").append(parameterTypeMirror.toString()).append(".class");
                } else {
                    result.append(", ").append(getTypeTokenCode(parameterTypeMirror, stagGenerator, typeVarsMap, adapterFieldInfo)).append(".getType()");
                }
                paramIndex++;
            }
            result.append(")");
            return adapterFieldInfo.updateAndGetTypeTokenFieldName(fieldType, result.toString());
        } else {
            return adapterFieldInfo.updateAndGetTypeTokenFieldName(fieldType, "com.google.gson.reflect.TypeToken.get(" + fieldType.toString() + ".class)");
        }
    }

    @NotNull
    private static TypeName getAdapterFieldTypeName(@NotNull TypeMirror type) {
        TypeName typeName = TypeVariableName.get(type);
        return ParameterizedTypeName.get(ClassName.get(TypeAdapter.class), typeName);
    }

    @NotNull
    private static TypeName getTypeTokenFieldTypeName(@NotNull TypeMirror type) {
        TypeName typeName = TypeVariableName.get(type);
        return ParameterizedTypeName.get(ClassName.get(TypeToken.class), typeName);
    }

    @NotNull
    private static String getInitializationCodeForKnownJsonAdapterType(@NotNull ExecutableElement adapterType,
                                                                       @NotNull StagGenerator stagGenerator,
                                                                       @NotNull Map<TypeMirror, String> typeVarsMap,
                                                                       @NotNull MethodSpec.Builder constructorBuilder,
                                                                       @NotNull TypeMirror fieldType,
                                                                       @NotNull TypeUtils.JsonAdapterType jsonAdapterType,
                                                                       @NotNull AdapterFieldInfo adapterFieldInfo,
                                                                       boolean isNullSafe,
                                                                       @NotNull String keyFieldName) {
        String fieldAdapterAccessor = "new " + FileGenUtils.escapeStringForCodeBlock(adapterType.getEnclosingElement().toString());
        if (jsonAdapterType == TypeUtils.JsonAdapterType.TYPE_ADAPTER) {
            ArrayList<String> constructorParameters = new ArrayList<>();
            if (!adapterType.getParameters().isEmpty()) {
                for (VariableElement parameter : adapterType.getParameters()) {
                    if (parameter.asType().toString().equals(TypeUtils.className(Gson.class))) {
                        constructorParameters.add("gson");
                    } else {
                        throw new IllegalStateException("Not supported " + parameter.asType() + "parameter for @JsonAdapter value");
                    }
                }
            }


            StringBuilder constructorParameterStr = new StringBuilder("(");
            for (int i = 0; i < constructorParameters.size(); i++) {
                constructorParameterStr.append(constructorParameters.get(i));
                if (i != constructorParameters.size() - 1) {
                    constructorParameterStr.append(",");
                }
            }
            constructorParameterStr.append(")");
            fieldAdapterAccessor += constructorParameterStr;
        } else if (jsonAdapterType == TypeUtils.JsonAdapterType.TYPE_ADAPTER_FACTORY) {
            String typeTokenAccessorCode = getTypeTokenCode(fieldType, stagGenerator, typeVarsMap, adapterFieldInfo);
            fieldAdapterAccessor += "().create(gson, " + typeTokenAccessorCode + ")";
        } else if (jsonAdapterType == TypeUtils.JsonAdapterType.JSON_SERIALIZER
                || jsonAdapterType == TypeUtils.JsonAdapterType.JSON_DESERIALIZER
                || jsonAdapterType == TypeUtils.JsonAdapterType.JSON_SERIALIZER_DESERIALIZER) {
            String serializer = null, deserializer = null;

            if (jsonAdapterType == TypeUtils.JsonAdapterType.JSON_SERIALIZER_DESERIALIZER) {
                String varName = keyFieldName + "SerializerDeserializer";
                String initializer = adapterType.getEnclosingElement().toString() + " " + varName + " = " +
                        "new " + adapterType;
                constructorBuilder.addStatement(initializer);
                serializer = varName;
                deserializer = varName;
            } else if (jsonAdapterType == TypeUtils.JsonAdapterType.JSON_SERIALIZER) {
                serializer = "new " + adapterType;
            } else if (jsonAdapterType == TypeUtils.JsonAdapterType.JSON_DESERIALIZER) {
                deserializer = "new " + adapterType;
            }
            String typeTokenAccessorCode = getTypeTokenCode(fieldType, stagGenerator, typeVarsMap, adapterFieldInfo);
            fieldAdapterAccessor = "new " + TypeVariableName.get(TreeTypeAdapter.class) + "(" + serializer + ", " + deserializer + ", gson, " + typeTokenAccessorCode + ", null)";
        } else {
            throw new IllegalArgumentException(
                    "@JsonAdapter value must be TypeAdapter, TypeAdapterFactory, "
                            + "JsonSerializer or JsonDeserializer reference.");
        }
        String adapterCode = getCleanedFieldInitializer(fieldAdapterAccessor);
        if (isNullSafe) {
            adapterCode += ".nullSafe()";
        }
        return adapterCode;
    }

    private static String getCleanedFieldInitializer(String code) {
        return code.replace("mStagFactory", "stagFactory").replace("mGson", "gson");
    }

    /**
     * Returns the adapter code for the unknown types.
     */
    private static String getAdapterForUnknownGenericType(@NotNull TypeMirror fieldType,
                                                          @NotNull StagGenerator stagGenerator,
                                                          @NotNull Map<TypeMirror, String> typeVarsMap,
                                                          @NotNull AdapterFieldInfo adapterFieldInfo) {

        String fieldName = adapterFieldInfo.getFieldName(fieldType);
        if (fieldName == null) {
            fieldName = TYPE_ADAPTER_FIELD_PREFIX + adapterFieldInfo.size();
            String fieldInitializationCode = "gson.getAdapter(" +
                    getTypeTokenCode(fieldType, stagGenerator, typeVarsMap, adapterFieldInfo) + ")";
            adapterFieldInfo.addField(fieldType, fieldName, fieldInitializationCode);
        }
        return fieldName;
    }

    /**
     * Returns the adapter code for the known types.
     */
    private static String getAdapterAccessor(@NotNull TypeMirror fieldType,
                                             @NotNull StagGenerator stagGenerator,
                                             @NotNull Map<TypeMirror, String> typeVarsMap,
                                             @NotNull AdapterFieldInfo adapterFieldInfo) {

        String knownTypeAdapter = KnownTypeAdapterUtils.getKnownTypeAdapterForType(fieldType);

        if (knownTypeAdapter != null) {
            return knownTypeAdapter;
        }

        String fieldName = adapterFieldInfo.getFieldName(fieldType);
        if (fieldName != null) {
            return fieldName;
        }

        if (TypeUtils.isNativeArray(fieldType)) {
            /*
             * If the fieldType is of type native arrays such as String[] or int[]
             */
            TypeMirror arrayInnerType = TypeUtils.getArrayInnerType(fieldType);
            if (TypeUtils.isSupportedPrimitive(arrayInnerType.toString())) {
                return KnownTypeAdapterUtils.getNativePrimitiveArrayTypeAdapter(fieldType);
            } else {
                String adapterAccessor = getAdapterAccessor(arrayInnerType, stagGenerator, typeVarsMap,
                        adapterFieldInfo);
                String nativeArrayInstantiator =
                        KnownTypeAdapterUtils.getNativeArrayInstantiator(arrayInnerType);
                return "new " + TypeUtils.className(ArrayTypeAdapter.class) + "<" +
                        arrayInnerType.toString() + ">" +
                        "(" + adapterAccessor + ", " + nativeArrayInstantiator + ")";
            }
        } else if (TypeUtils.isSupportedList(fieldType)) {
            DeclaredType declaredType = (DeclaredType) fieldType;
            List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
            TypeMirror param = typeArguments.get(0);
            String paramAdapterAccessor = getAdapterAccessor(param, stagGenerator, typeVarsMap, adapterFieldInfo);
            String listInstantiator = KnownTypeAdapterUtils.getListInstantiator(fieldType);
            String adapterCode =
                    "new " + TypeUtils.className(KnownTypeAdapters.ListTypeAdapter.class) + "<" + param.toString() + "," +
                            fieldType.toString() + ">" +
                            "(" + paramAdapterAccessor + ", " + listInstantiator + ")";
            fieldName = TYPE_ADAPTER_FIELD_PREFIX + adapterFieldInfo.size();
            adapterFieldInfo.addField(fieldType, fieldName, adapterCode);
            return fieldName;

        } else if (TypeUtils.isSupportedMap(fieldType)) {
            DeclaredType declaredType = (DeclaredType) fieldType;
            String mapInstantiator = KnownTypeAdapterUtils.getMapInstantiator(fieldType);
            List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
            String keyAdapterAccessor;
            String valueAdapterAccessor;
            String arguments = "";
            if (typeArguments != null && typeArguments.size() == 2) {
                TypeMirror keyType = typeArguments.get(0);
                TypeMirror valueType = typeArguments.get(1);
                keyAdapterAccessor = getAdapterAccessor(keyType, stagGenerator, typeVarsMap, adapterFieldInfo);
                valueAdapterAccessor = getAdapterAccessor(valueType, stagGenerator, typeVarsMap, adapterFieldInfo);
                arguments = "<" + keyType.toString() + ", " + valueType.toString() + ", " +
                        fieldType.toString() + ">";
            } else {
                // If the map does not have any type arguments, use Object as type params in this case
                keyAdapterAccessor = "new com.vimeo.stag.KnownTypeAdapters.ObjectTypeAdapter(mGson)";
                valueAdapterAccessor = keyAdapterAccessor;
            }

            String adapterCode = "new " + TypeUtils.className(KnownTypeAdapters.MapTypeAdapter.class) + arguments +
                    "(" + keyAdapterAccessor + ", " + valueAdapterAccessor + ", " +
                    mapInstantiator + ")";
            fieldName = TYPE_ADAPTER_FIELD_PREFIX + adapterFieldInfo.size();
            adapterFieldInfo.addField(fieldType, fieldName, adapterCode);
            return fieldName;
        } else {
            return getAdapterForUnknownGenericType(fieldType, stagGenerator, typeVarsMap, adapterFieldInfo);
        }
    }

    @NotNull
    private static AdapterFieldInfo addAdapterFields(@NotNull StagGenerator stagGenerator,
                                                     @NotNull MethodSpec.Builder constructorBuilder,
                                                     @NotNull Map<FieldAccessor, TypeMirror> memberVariables,
                                                     @NotNull Map<TypeMirror, String> typeVarsMap) {

        AdapterFieldInfo result = new AdapterFieldInfo(memberVariables.size());
        for (Map.Entry<FieldAccessor, TypeMirror> entry : memberVariables.entrySet()) {
            FieldAccessor fieldAccessor = entry.getKey();
            TypeMirror fieldType = entry.getValue();

            String adapterAccessor = null;
            TypeMirror optionalJsonAdapter = fieldAccessor.getJsonAdapterType();
            if (optionalJsonAdapter != null) {
                ExecutableElement constructor = ElementUtils.getFirstConstructor(optionalJsonAdapter);
                if (constructor != null) {
                    TypeUtils.JsonAdapterType jsonAdapterType1 = TypeUtils.getJsonAdapterType(optionalJsonAdapter);
                    String initiazationCode = getInitializationCodeForKnownJsonAdapterType(constructor, stagGenerator,
                            typeVarsMap, constructorBuilder, fieldType,
                            jsonAdapterType1, result, fieldAccessor.isJsonAdapterNullSafe(), fieldAccessor.getJsonName());

                    String fieldName = TYPE_ADAPTER_FIELD_PREFIX + result.size();
                    result.addFieldToAccessor(fieldAccessor.getJsonName(), fieldName, fieldType, initiazationCode);
                } else {
                    throw new IllegalStateException("Unsupported @JsonAdapter value: " + optionalJsonAdapter);
                }
            } else if (KnownTypeAdapterUtils.hasNativePrimitiveTypeAdapter(fieldType)) {
                adapterAccessor = KnownTypeAdapterUtils.getNativePrimitiveTypeAdapter(fieldType);
            } else if (TypeUtils.containsTypeVarParams(fieldType)) {
                adapterAccessor = getAdapterForUnknownGenericType(fieldType, stagGenerator, typeVarsMap, result);
            } else {
                adapterAccessor = getAdapterAccessor(fieldType, stagGenerator, typeVarsMap, result);
            }

            if (adapterAccessor != null) {
                result.addTypeToAdapterAccessor(fieldType, adapterAccessor);
            }
        }
        return result;
    }

    /**
     * Generates the TypeSpec for the TypeAdapter
     * that this class generates.
     *
     * @return a valid TypeSpec that can be written
     * to a file or added to another class.
     */
    @Override
    @NotNull
    public TypeSpec createTypeAdapterSpec(@NotNull StagGenerator stagGenerator) {
        TypeMirror typeMirror = mInfo.getType();
        TypeName typeVariableName = TypeVariableName.get(typeMirror);

        List<? extends TypeMirror> typeArguments = mInfo.getTypeArguments();

        MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(Gson.class, "gson");

        String className = FileGenUtils.unescapeEscapedString(mInfo.getTypeAdapterClassName());
        TypeSpec.Builder adapterBuilder = TypeSpec.classBuilder(className)
                .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
                        .addMember("value", "\"unchecked\"")
                        .addMember("value", "\"rawtypes\"")
                        .build())
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .superclass(ParameterizedTypeName.get(ClassName.get(TypeAdapter.class), typeVariableName));

        Map<TypeMirror, String> typeVarsMap = new LinkedHashMap<>();

        int idx = 0;
        if (typeArguments != null) {
            for (TypeMirror innerTypeMirror : typeArguments) {
                if (innerTypeMirror.getKind() == TypeKind.TYPEVAR) {
                    TypeVariable typeVariable = (TypeVariable) innerTypeMirror;
                    String simpleName = typeVariable.asElement().getSimpleName().toString();
                    adapterBuilder.addTypeVariable(TypeVariableName.get(simpleName, TypeVariableName.get(typeVariable.getUpperBound())));
                    //If the classInfo has unknown types, pass type... as param in constructor.
                    String paramName = "type[" + String.valueOf(idx) + "]";
                    typeVarsMap.put(typeVariable, paramName);
                    idx++;
                }
            }
        }

        if (idx > 0) {
            constructorBuilder.addParameter(Type[].class, "type");
            constructorBuilder.varargs(true);
        } else {
            //Create Type token as a static public final member variable
            // to be used from outside and by other adapters
            adapterBuilder.addField(createTypeTokenSpec(typeMirror));
        }

        AnnotatedClass annotatedClass = mSupportedTypesModel.getSupportedType(typeMirror);
        if (annotatedClass == null) {
            throw new IllegalStateException("The AnnotatedClass class can't be null in TypeAdapterGenerator : " + typeMirror.toString());
        }
        Map<FieldAccessor, TypeMirror> memberVariables = annotatedClass.getMemberVariables();

        AdapterFieldInfo adapterFieldInfo =
                addAdapterFields(stagGenerator, constructorBuilder, memberVariables, typeVarsMap);

        MethodSpec writeMethod = WriteSpecGenerator.getWriteMethodSpec(typeVariableName, memberVariables, adapterFieldInfo, mEnableSerializeNulls);
        MethodSpec readMethod = ReadSpecGenerator.getReadMethodSpec(typeVariableName, memberVariables, adapterFieldInfo);

        adapterBuilder.addField(Gson.class, "mGson", Modifier.FINAL, Modifier.PRIVATE);
        constructorBuilder.addStatement("this.mGson = gson");

        for (Map.Entry<String, FieldInfo> fieldInfo : adapterFieldInfo.mTypeTokenAccessorFields.entrySet()) {
            String originalFieldName = FileGenUtils.unescapeEscapedString(fieldInfo.getValue().accessorVariable);
            TypeName typeName = getTypeTokenFieldTypeName(fieldInfo.getValue().type);
            constructorBuilder.addStatement(typeName.toString() + " " + originalFieldName + " = " + fieldInfo.getValue().initializationCode);
        }

        for (Map.Entry<String, FieldInfo> fieldInfo : adapterFieldInfo.mFieldAdapterAccessor.entrySet()) {
            String originalFieldName = FileGenUtils.unescapeEscapedString(fieldInfo.getValue().accessorVariable);
            TypeName typeName = getAdapterFieldTypeName(fieldInfo.getValue().type);
            adapterBuilder.addField(typeName, originalFieldName, Modifier.PRIVATE, Modifier.FINAL);
            constructorBuilder.addStatement("this." + originalFieldName + " = " + fieldInfo.getValue().initializationCode);
        }

        for (Map.Entry<String, FieldInfo> fieldInfo : adapterFieldInfo.mAdapterFields.entrySet()) {
            String originalFieldName = FileGenUtils.unescapeEscapedString(fieldInfo.getValue().accessorVariable);
            TypeName typeName = getAdapterFieldTypeName(fieldInfo.getValue().type);
            adapterBuilder.addField(typeName, originalFieldName, Modifier.PRIVATE, Modifier.FINAL);
            constructorBuilder.addStatement("this." + originalFieldName + " = " + fieldInfo.getValue().initializationCode);
        }

        adapterBuilder.addMethod(constructorBuilder.build());
        adapterBuilder.addMethod(writeMethod);
        adapterBuilder.addMethod(readMethod);

        return adapterBuilder.build();
    }

    private static class FieldInfo {

        @NotNull
        final TypeMirror type;
        @NotNull
        final String initializationCode;
        @NotNull
        final String accessorVariable;

        FieldInfo(@NotNull TypeMirror type, @NotNull String initializationCode, @NotNull String accessorVariable) {
            this.type = type;
            this.initializationCode = initializationCode;
            this.accessorVariable = accessorVariable;
        }
    }

    public static class AdapterFieldInfo {

        //FieldName -> Accessor Map
        @NotNull
        final Map<String, FieldInfo> mFieldAdapterAccessor;
        //Type.toString -> Accessor Map
        @NotNull
        final Map<String, FieldInfo> mAdapterFields;
        //Type.toString -> Type Token Accessor Map
        @NotNull
        final Map<String, FieldInfo> mTypeTokenAccessorFields;
        //Type.toString -> Accessor Map
        @NotNull
        private final Map<String, String> mAdapterAccessor;

        AdapterFieldInfo(int capacity) {
            mAdapterFields = new LinkedHashMap<>(capacity);
            mAdapterAccessor = new LinkedHashMap<>(capacity);
            mFieldAdapterAccessor = new LinkedHashMap<>(capacity);
            mTypeTokenAccessorFields = new LinkedHashMap<>();
        }

        public String getAdapterAccessor(@NotNull TypeMirror typeMirror, @NotNull String fieldName) {
            FieldInfo adapterAccessor = mFieldAdapterAccessor.get(fieldName);
            return adapterAccessor != null ? adapterAccessor.accessorVariable : mAdapterAccessor.get(typeMirror.toString());
        }

        String updateAndGetTypeTokenFieldName(@NotNull TypeMirror fieldType, @NotNull String initializationCode) {
            FieldInfo result = mTypeTokenAccessorFields.get(fieldType.toString());
            if (result == null) {
                result = new FieldInfo(fieldType, initializationCode, "typeToken" + mTypeTokenAccessorFields.size());
                mTypeTokenAccessorFields.put(fieldType.toString(), result);
            }
            return result.accessorVariable;
        }

        String getFieldName(@NotNull TypeMirror fieldType) {
            FieldInfo fieldInfo = mAdapterFields.get(fieldType.toString());
            return fieldInfo != null ? fieldInfo.accessorVariable : null;
        }

        int size() {
            return mAdapterFields.size() + mFieldAdapterAccessor.size();
        }

        void addField(@NotNull TypeMirror fieldType, @NotNull String fieldName, @NotNull String fieldInitializationCode) {
            mAdapterFields.put(fieldType.toString(), new FieldInfo(fieldType, fieldInitializationCode, fieldName));
        }

        void addTypeToAdapterAccessor(@NotNull TypeMirror typeMirror, String accessorCode) {
            mAdapterAccessor.put(typeMirror.toString(), accessorCode);
        }

        void addFieldToAccessor(@NotNull String fieldName, @NotNull String variableName, TypeMirror fieldType, @NotNull String fieldInitializationCode) {
            mFieldAdapterAccessor.put(fieldName, new FieldInfo(fieldType, fieldInitializationCode, variableName));
        }
    }
} public class AnnotatedClass {

    @NotNull
    public static Set<TypeMirror> annotatedClassToTypeMirror(@NotNull Collection<AnnotatedClass> annotatedClasses) {
        Set<TypeMirror> typeMirrors = new LinkedHashSet<>();
        for (AnnotatedClass annotatedClass : annotatedClasses) {
            typeMirrors.add(annotatedClass.getType());
        }

        return typeMirrors;
    }

    private static final String TAG = AnnotatedClass.class.getSimpleName();

    @NotNull private final TypeMirror mType;
    @NotNull private final TypeElement mElement;
    @NotNull private final LinkedHashMap<FieldAccessor, TypeMirror> mMemberVariables;
    @NotNull private final Notation mNamingNotation;

    AnnotatedClass(@NotNull SupportedTypesModel supportedTypesModel,
                   @NotNull TypeElement element,
                   @NotNull Notation namingNotation) {
        this(supportedTypesModel, element, namingNotation, null);
    }

    AnnotatedClass(@NotNull SupportedTypesModel supportedTypesModel,
                   @NotNull TypeElement element,
                   @NotNull Notation namingNotation,
                   @Nullable FieldOption childFieldOption) {
        mNamingNotation = namingNotation;
        mType = element.asType();
        mElement = element;
        Map<String, FieldAccessor> variableNames = new LinkedHashMap<>(element.getEnclosedElements().size());
        TypeMirror inheritedType = TypeUtils.getInheritedType(element);

        UseStag useStag = element.getAnnotation(UseStag.class);

        FieldOption fieldOption = useStag != null ? useStag.value() : null;
        if (fieldOption == null) {
            useStag = element.getAnnotation(UseStag.class);

            // The field option can be null if the inherited class was not annotated
            fieldOption = useStag != null ? useStag.value() : childFieldOption;
        }

        Preconditions.checkNotNull(fieldOption);

        mMemberVariables = new LinkedHashMap<>();

        if (inheritedType != null) {
            DebugLog.log(TAG, "\t\tInherited Type - " + inheritedType.toString());

            AnnotatedClass genericInheritedType = supportedTypesModel.addToKnownInheritedType(inheritedType, fieldOption);

            LinkedHashMap<FieldAccessor, TypeMirror> inheritedMemberVariables = TypeUtils.getConcreteMembers(inheritedType,
                                                                                                             genericInheritedType.getElement(),
                                                                                                             genericInheritedType.getMemberVariables());

            for (Map.Entry<FieldAccessor, TypeMirror> entry : inheritedMemberVariables.entrySet()) {
                addMemberVariable(entry.getKey(), entry.getValue(), variableNames);
            }
        }

        if (!TypeUtils.isEnum(element)) {
            for (Element enclosedElement : element.getEnclosedElements()) {
                if (enclosedElement instanceof VariableElement) {
                    addToSupportedTypes((VariableElement) enclosedElement, fieldOption, variableNames);
                }
            }
        }

    }

    private void addMemberVariable(@NotNull FieldAccessor element, @NotNull TypeMirror typeMirror,
                                   @NotNull Map<String, FieldAccessor> variableNames) {
        FieldAccessor previousElement = variableNames.put(element.createGetterCode(), element);
        if (previousElement != null) {
            mMemberVariables.remove(previousElement);
            DebugLog.log("Ignoring inherited Member variable with the same variable name in class" +
                         element.toString() + ", with variable name " + previousElement.asType().toString());
        }
        mMemberVariables.put(element, typeMirror);
    }

    private static boolean checkPrivateFinalModifiers(@NotNull VariableElement variableElement, @NotNull Set<Modifier> modifiers) {
        Preconditions.checkTrue(!modifiers.contains(Modifier.STATIC));

        if (modifiers.contains(Modifier.FINAL)) {
            MessagerUtils.reportError("Unable to access field \"" +
                                      variableElement.getSimpleName().toString() + "\" in class " +
                                      variableElement.getEnclosingElement().asType() +
                                      ", field must not be final.", variableElement);
        }

        return modifiers.contains(Modifier.FINAL) || modifiers.contains(Modifier.PRIVATE);
    }

    private void addToSupportedTypes(@NotNull VariableElement element, @NotNull FieldOption fieldOption,
                                     @NotNull Map<String, FieldAccessor> variableNames) {
        if (shouldIncludeField(element, fieldOption)) {
            Set<Modifier> modifiers = element.getModifiers();
            if (!modifiers.contains(Modifier.STATIC) && !modifiers.contains(Modifier.TRANSIENT)) {

                DebugLog.log(TAG, "\t\tMember variables - " + element.asType().toString());

                if (checkPrivateFinalModifiers(element, modifiers)) {
                    try {
                        addMemberVariable(new MethodFieldAccessor(element, mNamingNotation), element.asType(), variableNames);
                    } catch (UnsupportedOperationException exception) {
                        MessagerUtils.reportError(exception.getMessage(), element);
                    }
                } else {
                    addMemberVariable(new DirectFieldAccessor(element), element.asType(), variableNames);
                }
            }
        }
    }

    private boolean shouldIncludeField(@NotNull Element element, @NotNull FieldOption fieldOption) {
        switch (fieldOption) {
            case NONE:
                return false;
            case SERIALIZED_NAME:
                return element.getAnnotation(SerializedName.class) != null;
            case ALL:
                return true;
            default:
                throw new RuntimeException("Unknown field option provided for class " + mElement.asType());
        }
    }

    @NotNull
    public TypeMirror getType() {
        return mType;
    }

    @NotNull
    public TypeElement getElement() {
        return mElement;
    }

    /**
     * Returns a map of the member variables (Elements) to
     * their concrete types (TypeMirror). If the type of this
     * AnnotatedClass is generic, then the concrete types
     * will be made as concrete as possible based on the type
     * parameters of this class. If an Element was already
     * not generic, then the TypeMirror for that element
     * will contain the same exact type.
     *
     * @return a valid map of element to their un-generified
     * types.
     */
    @NotNull
    public LinkedHashMap<FieldAccessor, TypeMirror> getMemberVariables() {
        return new LinkedHashMap<>(mMemberVariables);
    }
} public final class SupportedTypesModel {

    @NotNull private final Map<String, AnnotatedClass> mSupportedTypesMap = new LinkedHashMap<>();
    @NotNull private final Map<String, AnnotatedClass> mKnownInheritedTypesMap = new LinkedHashMap<>();
    @NotNull private final Notation mNamingNotation;

    public SupportedTypesModel(@NotNull Notation namingNotation) {
        mNamingNotation = namingNotation;
    }

    /**
     * Tells the model that we support this type.
     *
     * @param object the annotated class that we
     *               should track.
     */
    private void addSupportedType(@NotNull AnnotatedClass object) {
        mSupportedTypesMap.put(TypeUtils.getOuterClassType(object.getType()), object);
    }

    /**
     * Retrieves the AnnotatedClass object for the
     * specific TypeMirror.
     *
     * @param type the type that maps to a specific
     *             AnnotatedClass.
     * @return the AnnotatedClass object associated
     * with the class type.
     */
    @Nullable
    public AnnotatedClass getSupportedType(@NotNull TypeMirror type) {
        return getSupportedType(TypeUtils.getOuterClassType(type));
    }

    /**
     * Retrieves the AnnotatedClass object for the
     * specific TypeMirror.
     *
     * @param type the type that maps to a specific
     *             AnnotatedClass.
     * @return the AnnotatedClass object associated
     * with the class type.
     */
    @Nullable
    private AnnotatedClass getSupportedType(@NotNull String type) {
        return mSupportedTypesMap.get(type);
    }

    /**
     * Adds an AnnotatedClass object for the
     * specific TypeMirror if it does not exist in the list of
     * known inherited types
     *
     * @param type the type that maps to a specific
     *             AnnotatedClass.
     * @return the AnnotatedClass object associated
     * with the class type.
     */
    @NotNull
    public AnnotatedClass addToKnownInheritedType(@NotNull TypeMirror type, @Nullable FieldOption childFieldOption) {
        String outerClassType = TypeUtils.getOuterClassType(type);
        AnnotatedClass model = getSupportedType(outerClassType);
        if (model == null) {
            model = mKnownInheritedTypesMap.get(outerClassType);
            if (model == null) {
                model = new AnnotatedClass(this, TypeUtils.safeTypeMirrorToTypeElement(type), mNamingNotation, childFieldOption);
                mKnownInheritedTypesMap.put(outerClassType, model);
            }
        }
        return model;
    }

    /**
     * Adds an AnnotatedClass object for the
     * specific TypeMirror if it does not exist.
     *
     * @param type the type that maps to a specific
     *             AnnotatedClass.
     */
    public void addSupportedType(@NotNull TypeMirror type) {
        String outerClassType = TypeUtils.getOuterClassType(type);
        AnnotatedClass model = getSupportedType(outerClassType);

        if (model == null) {
            model = mKnownInheritedTypesMap.get(outerClassType);
            if (model == null) {
                model = new AnnotatedClass(this, TypeUtils.safeTypeMirrorToTypeElement(type), mNamingNotation);
            }
            addSupportedType(model);
        }
    }

    /**
     * A set of all supported elements (these map 1 to 1
     * to an AnnotatedClass). This may return both generic
     * and concrete types.
     *
     * @return the set of supported types.
     */
    @NotNull
    public Collection<AnnotatedClass> getSupportedTypes() {
        return mSupportedTypesMap.values();
    }

} public enum Notation {
    STANDARD,
    HUNGARIAN
} public final class ElementUtils {

    @Nullable private static Elements sElementUtils;

    private ElementUtils() {
        throw new UnsupportedOperationException("This class is not instantiable");
    }

    public static void initialize(@NotNull Elements elementUtils) {
        sElementUtils = elementUtils;
    }

    @NotNull
    private static Elements getUtils() {
        Preconditions.checkNotNull(sElementUtils);
        return sElementUtils;
    }

    @Nullable
    public static TypeMirror getTypeFromQualifiedName(@NotNull String qualifiedName) {
        TypeElement typeElement = getTypeElementFromQualifiedName(qualifiedName);
        return typeElement != null ? typeElement.asType() : null;
    }

    @Nullable
    public static TypeMirror getTypeFromClass(@NotNull Class clazz) {
        return getTypeFromQualifiedName(clazz.getName());
    }

    @Nullable
    public static TypeElement getTypeElementFromQualifiedName(@NotNull String qualifiedName) {
        Elements elements = ElementUtils.getUtils();
        return elements.getTypeElement(qualifiedName);
    }

    @NotNull
    public static String getPackage(@NotNull TypeMirror type) {
        Element element = TypeUtils.unsafeTypeMirrorToTypeElement(type);
        PackageElement packageElement = getUtils().getPackageOf(element);
        return packageElement.getQualifiedName().toString();
    }

    /**
     * Determines whether or not the element provided is annotated with the annotation type
     * specified.
     *
     * @param annotationClass annotation class to search for
     * @param element         element to query
     * @param <T>             annotation type
     * @return {@code true} if the element is annotated, {@code false} otherwise
     */
    private static <T extends Annotation> boolean isAnnotatedWith(@NotNull Class<T> annotationClass,
                                                                  @NotNull Element element) {
        return element.getAnnotation(annotationClass) != null;
    }

    /**
     * Determines if an element is a supported type.
     *
     * @param element the element to check.
     * @return true if the element is supported
     * (class or enum), false otherwise.
     */
    public static boolean isSupportedElementKind(@Nullable Element element) {
        if (element == null) {
            return false;
        }
        ElementKind elementKind = element.getKind();
        return (elementKind == ElementKind.CLASS || elementKind == ElementKind.ENUM)
               && isAnnotatedWith(UseStag.class, element);
    }

    @Nullable
    public static ExecutableElement getFirstConstructor(@Nullable TypeMirror typeMirror) {
        Element typeElement = typeMirror != null ? TypeUtils.unsafeTypeMirrorToTypeElement(typeMirror) : null;
        if (typeElement != null) {
            for (Element element : typeElement.getEnclosedElements()) {
                if (element instanceof ExecutableElement) {
                    ExecutableElement executableElement = ((ExecutableElement) element);
                    Name name = executableElement.getSimpleName();
                    if (name.contentEquals("<init>")) {
                        return executableElement;
                    }
                }
            }
        }

        return null;
    }
} public final class FileGenUtils {

    private static final String CODE_BLOCK_ESCAPED_SEPARATOR = "$$";
    private static final String UNESCAPED_SEPARATOR = "$";

    private FileGenUtils() {
        throw new UnsupportedOperationException("This class is not instantiable");
    }

    /**
     * Writes a Java file to the file system after
     * deleting the previous copy.
     *
     * @param file  the file to write.
     * @param filer the Filer to use to do the writing.
     * @throws IOException throws an exception if we are unable
     *                     to write the file to the filesystem.
     */
    public static void writeToFile(@NotNull JavaFile file, @NotNull Filer filer) throws IOException {
        String fileName =
                file.packageName.isEmpty() ? file.typeSpec.name : file.packageName + '.' + file.typeSpec.name;
        List<Element> originatingElements = file.typeSpec.originatingElements;
        JavaFileObject filerSourceFile = filer.createSourceFile(fileName, originatingElements.toArray(
                new Element[originatingElements.size()]));
        filerSourceFile.delete();
        Writer writer = null;
        try {
            writer = filerSourceFile.openWriter();
            file.writeTo(writer);
        } catch (Exception e) {
            try {
                filerSourceFile.delete();
            } catch (Exception ignored) {
            }
            throw e;
        } finally {
            close(writer);
        }
    }

    @Nullable
    static CharSequence readResource(@NotNull Filer filer, @NotNull String generatedPackageName,
                                     @NotNull String resourceName) throws IOException {
        try {
            FileObject file =
                    filer.getResource(StandardLocation.CLASS_OUTPUT, generatedPackageName, resourceName);
            return file.getCharContent(false);
        } catch (FileNotFoundException e) {
            DebugLog.log("Resource not found: " + resourceName);
            return null;
        }
    }

    static void writeToResource(@NotNull Filer filer, @NotNull String generatedPackageName,
                                @NotNull String resourceName, @NotNull CharSequence content)
            throws IOException {
        FileObject file =
                filer.createResource(StandardLocation.CLASS_OUTPUT, generatedPackageName, resourceName);
        file.delete();
        Writer writer = null;
        try {
            writer = file.openWriter();
            writer.append(content);
            DebugLog.log("Wrote to resource '" + resourceName + "':\n" + content);
        } catch (Exception e) {
            try {
                file.delete();
            } catch (Exception ignored) {
            }
            throw e;
        } finally {
            close(writer);
        }
    }

    /**
     * Safely closes a closeable.
     *
     * @param closeable object to close.
     */
    static void close(@Nullable Closeable closeable) {
        if (closeable == null) {
            return;
        }
        try {
            closeable.close();
        } catch (IOException e) {
            // ignored
        }
    }

    /**
     * Takes a String input and escapes it for use
     * in the {@link CodeBlock} class.
     *
     * @param string the string to escape.
     * @return a String safe to use in a {@link CodeBlock}
     */
    @NotNull
    public static String escapeStringForCodeBlock(@NotNull String string) {
        return string.replace(UNESCAPED_SEPARATOR, CODE_BLOCK_ESCAPED_SEPARATOR);
    }

    /**
     * Takes a String input that was escaped for
     * use in the {@link CodeBlock} class and
     * unescapes it for normal use.
     *
     * @param string the String to unescape,
     * @return a String safe for normal use.
     */
    @NotNull
    public static String unescapeEscapedString(@NotNull String string) {
        return string.replace(CODE_BLOCK_ESCAPED_SEPARATOR, UNESCAPED_SEPARATOR);
    }
} public final class MessagerUtils {

    @Nullable
    private static Messager sMessager;

    private MessagerUtils() {
        throw new UnsupportedOperationException("This class is not instantiable");
    }

    /**
     * Initialize the Messager. Must be done before the messager can be used.
     *
     * @param messager the messager instance.
     */
    public static void initialize(@NotNull Messager messager) {
        sMessager = messager;
    }

    @NotNull
    private static Messager getMessager() {
        Preconditions.checkNotNull(sMessager);
        return sMessager;
    }

    /**
     * Report a fatal error to the compiler. Will halt compilation.
     *
     * @param message the message to display in the compilation output.
     * @param element the offending element.
     */
    public static void reportError(@NotNull String message, @NotNull Element element) {
        getMessager().printMessage(Kind.ERROR, message, element);
    }

} public final class TypeUtils {

    private static final String TAG = TypeUtils.class.getSimpleName();

    @NotNull
    private static final HashMap<String, String> PRIMITIVE_TO_OBJECT_MAP = new HashMap<>();

    @Nullable
    private static Types sTypeUtils;

    static {
        PRIMITIVE_TO_OBJECT_MAP.put(boolean.class.getName(), Boolean.class.getName());
        PRIMITIVE_TO_OBJECT_MAP.put(int.class.getName(), Integer.class.getName());
        PRIMITIVE_TO_OBJECT_MAP.put(short.class.getName(), Short.class.getName());
        PRIMITIVE_TO_OBJECT_MAP.put(double.class.getName(), Double.class.getName());
        PRIMITIVE_TO_OBJECT_MAP.put(long.class.getName(), Long.class.getName());
        PRIMITIVE_TO_OBJECT_MAP.put(float.class.getName(), Float.class.getName());
        PRIMITIVE_TO_OBJECT_MAP.put(char.class.getName(), Character.class.getName());
        PRIMITIVE_TO_OBJECT_MAP.put(byte.class.getName(), Byte.class.getName());
    }

    private TypeUtils() {
        throw new UnsupportedOperationException("This class is not instantiable");
    }

    public static void initialize(@NotNull Types typeUtils) {
        sTypeUtils = typeUtils;
    }

    @NotNull
    private static Types getUtils() {
        Preconditions.checkNotNull(sTypeUtils);
        return sTypeUtils;
    }

    /**
     * Creates the full class name including package
     * name for the given class. Anonymous classes
     * and classes with the '$' character in their names
     * are not supported.
     *
     * @param clazz the class to get the name of.
     * @return the class's name, without any dollar
     * signs for inner classes and with periods instead.
     */
    @NotNull
    public static String className(@NotNull Class clazz) {
        return clazz.getName().replace('$', '.');
    }

    /**
     * Retrieves the outer type of a parameterized class.
     * e.g. an ArrayList{@literal <T>} would be returned as
     * just ArrayList. If an interface is passed in, i.e. a
     * List, the underlying implementation will be returned,
     * i.e. ArrayList.
     *
     * @param type the type to get the outer class from/
     * @return the outer class of the type passed in, or the
     * type itself if it is not parameterized.
     */
    @NotNull
    public static String getOuterClassType(@NotNull TypeMirror type) {
        if (type instanceof DeclaredType) {
            return ((DeclaredType) type).asElement().toString();
        } else {
            return type.toString();
        }
    }

    /**
     * Determines whether or not the type has type parameters.
     *
     * @param type the type to check.
     * @return true if the type is not null and has type parameters,
     * false otherwise.
     */
    public static boolean isParameterizedType(@Nullable TypeMirror type) {
        List<? extends TypeMirror> typeArguments = getTypeArguments(type);
        return typeArguments != null && !typeArguments.isEmpty();
    }

    /**
     * Determines whether or not the type has type parameters.
     *
     * @param type the type to check.
     * @return true if the type is not null and has type parameters,
     * false otherwise.
     */
    @Nullable
    public static List<? extends TypeMirror> getTypeArguments(@Nullable TypeMirror type) {
        return type instanceof DeclaredType ? ((DeclaredType) type).getTypeArguments() : null;
    }

    /**
     * TypeMirrors should not be compared directly, but should use
     * {@link Types} in order to compare them.
     *
     * @param typeMirror1 the first type to compare.
     * @param typeMirror2 the second type to compare.
     * @return true if they are equal, false otherwise.
     */
    public static boolean areEqual(@Nullable TypeMirror typeMirror1, @Nullable TypeMirror typeMirror2) {
        if (typeMirror1 == null && typeMirror2 != null) {
            return false;
        } else if (typeMirror1 != null && typeMirror2 == null) {
            return false;
        } else if (typeMirror1 == typeMirror2) {
            return true;
        }
        return getUtils().isSameType(typeMirror1, typeMirror2);
    }

    /**
     * Determines whether or not the Element is a concrete type.
     * If the element is a generic type or contains generic type
     * arguments, this method will return false.
     *
     * @param element the element to check.
     * @return true if the element is not generic and
     * contains no generic type arguments, false otherwise.
     */
    public static boolean isConcreteType(@NotNull Element element) {
        return isConcreteType(element.asType());
    }

    /**
     * Determines whether or not the Element is a abstract type.
     *
     * @param element the element to check.
     * @return true if the element is abstract and
     * contains no generic type arguments, false otherwise.
     */
    public static boolean isAbstract(@Nullable Element element) {
        return element != null && element.getModifiers().contains(Modifier.ABSTRACT);
    }

    /**
     * Determines whether or not the Element is a abstract type.
     *
     * @param typeMirror the element to check.
     * @return true if the element is abstract and
     * contains no generic type arguments, false otherwise.
     */
    public static boolean isAbstract(@Nullable TypeMirror typeMirror) {
        return (typeMirror instanceof DeclaredType) && isAbstract(((DeclaredType) typeMirror).asElement());
    }

    /**
     * Determines whether or not the Element is a parameterized type.
     * If the element is a parameterized type or contains parameterized type
     * arguments, this method will return false.
     *
     * @param element the element to check.
     * @return true if the element is not generic and
     * contains no generic type arguments, false otherwise.
     */
    public static boolean isParameterizedType(@Nullable TypeElement element) {
        return element != null && isParameterizedType(element.asType());
    }

    /**
     * Determines whether or not the TypeMirror is a concrete type.
     * If the type is a generic type or contains generic type
     * arguments (i.e. a parameterized type), this method will
     * return false.
     *
     * @param typeMirror the element to check.
     * @return true if the type is not generic and
     * contains no generic type arguments, false otherwise.
     */
    public static boolean isConcreteType(@NotNull TypeMirror typeMirror) {
        if (typeMirror.getKind() == TypeKind.TYPEVAR) {
            return false;
        }
        if (isPrimitive(typeMirror, getUtils())) {
            return true;
        }
        if (typeMirror instanceof DeclaredType) {
            List<? extends TypeMirror> typeMirrors = ((DeclaredType) typeMirror).getTypeArguments();

            for (TypeMirror type : typeMirrors) {
                if (!isConcreteType(type)) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Determines where the the type mirrors contains type var params or not
     *
     * @param typeMirror the element to check.
     * @return true if it contains type variables
     */
    public static boolean containsTypeVarParams(@NotNull TypeMirror typeMirror) {
        if (typeMirror.getKind() == TypeKind.TYPEVAR) {
            return true;
        }

        if (typeMirror instanceof DeclaredType) {
            List<? extends TypeMirror> typeMirrors = ((DeclaredType) typeMirror).getTypeArguments();

            for (TypeMirror type : typeMirrors) {
                if (containsTypeVarParams(type)) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Gets the inherited type from the element. If
     * the inherited type is {@link Object} or {@link Enum},
     * then this method will return null.
     *
     * @param element the element to get the inherited type.
     * @return the inherited type, or null if the element
     * inherits from Object or Enum.
     */
    @Nullable
    public static TypeMirror getInheritedType(@NotNull TypeElement element) {
        TypeMirror typeMirror = element.getSuperclass();
        String className = typeMirror != null ? getClassNameFromTypeMirror(typeMirror) : null;
        if (!Object.class.getName().equals(className) && !Enum.class.getName().equals(className)) {
            return typeMirror;
        }
        return null;
    }

    /**
     * Determines whether the element is of the enum type or not.
     *
     * @param element the element to check.
     * @return true if the element inherits from an enum, false otherwise.
     */
    public static boolean isEnum(@NotNull TypeElement element) {
        TypeMirror typeMirror = element.getSuperclass();
        String className = typeMirror != null ? getClassNameFromTypeMirror(typeMirror) : null;

        return Enum.class.getName().equals(className);
    }

    /**
     * Retrieves a Map of the inherited concrete member variables of an Element. This takes all the
     * member variables that were inherited from the generic parent class and evaluates what their concrete
     * type will be based on the concrete inherited type. For instance, take the following code example:
     * <pre><code>
     * {@literal Factory<T>} {
     *
     *  {@literal @UseStag}
     *   public T data;
     *
     * }
     *
     * VideoFactory extends {@literal Factory<Video>}{
     *
     *   // other variables in here
     *
     * }
     * </code></pre>
     * In this example, VideoFactory has a public member variable T that is of type Video.
     * Since the Factory class has the UseStag annotation, we cannot just generate
     * parsing code for the Factory class, since it is generic and we need concrete types.
     * Instead when we generate the adapter for VideoFactory, we crawl the inheritance
     * hierarchy gathering the member variables. When we get to VideoFactory, we see it
     * has one member variable, T. We then look at the inherited type, Factory{@literal <Video>},
     * and compare it to the original type, Factory{@literal <T>}, and then infer the type
     * of T to be Video.
     *
     * @param concreteInherited the type inherited for the class you are using, in the example,
     *                          this would be Factory{@literal <Video>}
     * @param genericInherited  the raw type inherited for the class you are using, in the example,
     *                          this would be Factory{@literal <T>}
     * @param members           the member variable map of the field (Element) to their concrete
     *                          type (TypeMirror). This should be retrieved by calling getConcreteMembers
     *                          on the inherited class.
     * @return returns a LinkedHashMap of the member variables mapped to their concrete types for the concrete
     * inherited class. (to maintain the ordering)
     */
    @NotNull
    public static LinkedHashMap<FieldAccessor, TypeMirror> getConcreteMembers(@NotNull TypeMirror concreteInherited,
                                                                              @NotNull TypeElement genericInherited,
                                                                              @NotNull Map<FieldAccessor, TypeMirror> members) {

        DebugLog.log(TAG, "Inherited concrete type: " + concreteInherited.toString());
        DebugLog.log(TAG, "Inherited generic type: " + genericInherited.asType().toString());
        List<? extends TypeMirror> concreteTypes = getParameterizedTypes(concreteInherited);
        List<? extends TypeMirror> inheritedTypes = getParameterizedTypes(genericInherited);

        LinkedHashMap<FieldAccessor, TypeMirror> map = new LinkedHashMap<>();

        for (Entry<FieldAccessor, TypeMirror> member : members.entrySet()) {

            DebugLog.log(TAG, "\t\tEvaluating member - " + member.getValue().toString());

            if (isConcreteType(member.getValue())) {

                DebugLog.log(TAG, "\t\t\tConcrete Type: " + member.getValue().toString());
                map.put(member.getKey(), member.getValue());

            } else {

                if (isParameterizedType(member.getValue())) {

                    // HashMap<String, T> ...
                    TypeMirror resolvedType = resolveTypeVars(member.getValue(), inheritedTypes, concreteTypes);
                    map.put(member.getKey(), resolvedType);

                    DebugLog.log(TAG, "\t\t\tGeneric Parameterized Type - " + member.getValue().toString() +
                            " resolved to - " + resolvedType.toString());
                } else {

                    int index = inheritedTypes.indexOf(member.getKey().asType());
                    TypeMirror concreteType = concreteTypes.get(index);
                    map.put(member.getKey(), concreteType);

                    DebugLog.log(TAG, "\t\t\tGeneric Type - " + member.getValue().toString() +
                            " resolved to - " + concreteType.toString());
                }
            }
        }
        return map;
    }

    /**
     * Gets the primitive type mirror type.
     *
     * @param typeKind the type kind to get the primitive for.
     * @return the primitive type, or null if the type is not primitive.
     */
    @Nullable
    public static TypeMirror getPrimitive(@NotNull TypeKind typeKind) {
        try {
            return getUtils().getPrimitiveType(typeKind);
        } catch (Exception ignored) {
            return null;
        }
    }

    private static boolean isPrimitive(@NotNull TypeMirror type, @NotNull Types utils) {
        try {
            utils.getPrimitiveType(type.getKind());
            return true;
        } catch (IllegalArgumentException ignored) {
            return false;
        }
    }

    @NotNull
    private static TypeMirror resolveTypeVars(@NotNull TypeMirror element,
                                              @NotNull final List<? extends TypeMirror> inheritedTypes,
                                              @NotNull final List<? extends TypeMirror> concreteTypes) {
        if (isConcreteType(element)) {
            return element;
        }

        if (element.getKind() == TypeKind.TYPEVAR) {
            int index = inheritedTypes.indexOf(element);
            return concreteTypes.get(index);
        }

        Types types = getUtils();
        List<? extends TypeMirror> typeMirrors = ((DeclaredType) element).getTypeArguments();
        TypeElement typeElement = (TypeElement) types.asElement(element);
        List<TypeMirror> concreteGenericTypes = new ArrayList<>(typeMirrors.size());
        for (TypeMirror type : typeMirrors) {
            concreteGenericTypes.add(resolveTypeVars(type, inheritedTypes, concreteTypes));
        }
        TypeMirror[] concreteTypeArray =
                concreteGenericTypes.toArray(new TypeMirror[concreteGenericTypes.size()]);
        return types.getDeclaredType(typeElement, concreteTypeArray);
    }

    @NotNull
    private static List<? extends TypeMirror> getParameterizedTypes(@NotNull TypeElement element) {
        return ((DeclaredType) element.asType()).getTypeArguments();
    }

    @NotNull
    private static List<? extends TypeMirror> getParameterizedTypes(@NotNull TypeMirror typeMirror) {
        return ((DeclaredType) typeMirror).getTypeArguments();
    }

    /**
     * Method to check if the {@link TypeMirror} is of primitive type
     *
     * @param type TypeMirror type
     * @return true if the type represented as a string is a primitive type that's supported, false
     * otherwise.
     */
    public static boolean isSupportedPrimitive(@NotNull String type) {
        return PRIMITIVE_TO_OBJECT_MAP.containsKey(type);
    }

    /**
     * Method to check if the {@link TypeMirror} is of {@link ArrayType}
     *
     * @param type TypeMirror type
     * @return true if the type is a native array type, false otherwise.
     */
    public static boolean isNativeArray(@NotNull TypeMirror type) {
        return (type instanceof ArrayType);
    }

    /**
     * Method to check if the {@link TypeMirror} is of {@link List} type
     *
     * @param type TypeMirror type
     * @return true if the type is a {@link List}, {@link ArrayList}, or {@link Collection},
     * false otherwise.
     */
    public static boolean isSupportedList(@NotNull TypeMirror type) {
        String outerClassType = TypeUtils.getOuterClassType(type);
        return outerClassType.equals(ArrayList.class.getName()) ||
                outerClassType.equals(List.class.getName()) ||
                outerClassType.equals(Collection.class.getName());
    }

    /**
     * Method to check if the {@link TypeMirror} is of {@link Map} type
     *
     * @param type TypeMirror type
     * @return true if the type is a supported map type, false otherwise.
     */
    public static boolean isSupportedMap(@Nullable TypeMirror type) {
        if (type == null) {
            return false;
        }
        String outerClassType = TypeUtils.getOuterClassType(type);
        //noinspection LiteralAsArgToStringEquals
        return outerClassType.equals(Map.class.getName()) ||
                outerClassType.equals(HashMap.class.getName()) ||
                outerClassType.equals(ConcurrentHashMap.class.getName()) ||
                outerClassType.equals("android.util.ArrayMap") ||
                outerClassType.equals("android.support.v4.util.ArrayMap") ||
                outerClassType.equals(LinkedHashMap.class.getName());
    }

    /**
     * Returns the inner {@link TypeMirror} for a given {@link TypeMirror}
     */
    @NotNull
    public static TypeMirror getArrayInnerType(@NotNull TypeMirror type) {
        return (type instanceof ArrayType) ? ((ArrayType) type).getComponentType() : ((DeclaredType) type).getTypeArguments()
                .get(0);
    }

    @NotNull
    public static String getClassNameFromTypeMirror(@NotNull TypeMirror typeMirror) {
        String classAndPackage = typeMirror.toString();

        // This is done to avoid the generic template from being included in the file name
        // to be generated (since it will be an invalid file name)
        int idx = classAndPackage.indexOf("<");
        if (idx > 0) {
            classAndPackage = classAndPackage.substring(0, idx);
        }

        return classAndPackage;
    }

    /**
     * Convert the provided {@link TypeMirror} into an {@link Element} instance.  This method
     * call assumes that the provided {@link TypeMirror} is constrained to only known supported
     * types.  As a result it will guarantee non-null result values.
     *
     * @param typeMirror type mirror to convert
     * @return TypeElement representation of the type mirror
     */
    @NotNull
    public static TypeElement safeTypeMirrorToTypeElement(@NotNull TypeMirror typeMirror) {
        TypeElement element = unsafeTypeMirrorToTypeElement(typeMirror);
        // unsafeTypeMirrorToTypeElement may return null but not in the scenarios we are specifically using it for
        if (element == null) {
            throw new IllegalStateException("Supported type could not be converted into an Element");
        }
        return element;
    }

    /**
     * Convert the provided {@link TypeMirror} into an {@link Element} instance.
     *
     * @param typeMirror TypeMirror to convert
     * @return TypeElement representation of the TypeMirror
     */
    @Nullable
    public static TypeElement unsafeTypeMirrorToTypeElement(@NotNull TypeMirror typeMirror) {
        return (TypeElement) getUtils().asElement(typeMirror);
    }


    public enum JsonAdapterType {
        NONE,
        TYPE_ADAPTER,
        TYPE_ADAPTER_FACTORY,
        JSON_SERIALIZER,
        JSON_DESERIALIZER,
        JSON_SERIALIZER_DESERIALIZER

    }

    /**
     * Return the type of JsonAdapter {@link TypeMirror}
     *
     * @param type TypeMirror type
     * @return {@link JsonAdapterType}
     */
    @NotNull
    public static JsonAdapterType getJsonAdapterType(@NotNull TypeMirror type) {
        Types types = getUtils();
        if (types.isSubtype(type, getDeclaredTypeForParameterizedClass(TypeAdapter.class.getName()))) {
            return JsonAdapterType.TYPE_ADAPTER;
        } else if (types.isAssignable(type, ElementUtils.getTypeFromQualifiedName(TypeAdapterFactory.class.getName()))) {
            return JsonAdapterType.TYPE_ADAPTER_FACTORY;
        } else {
            boolean isDeserializer = types.isSubtype(type, getDeclaredTypeForParameterizedClass(JsonDeserializer.class.getName()));
            boolean isSerializer = types.isSubtype(type, getDeclaredTypeForParameterizedClass(JsonSerializer.class.getName()));
            if (isSerializer && isDeserializer) {
                return JsonAdapterType.JSON_SERIALIZER_DESERIALIZER;
            } else if (isSerializer) {
                return JsonAdapterType.JSON_SERIALIZER;
            } else if (isDeserializer) {
                return JsonAdapterType.JSON_DESERIALIZER;
            } else {
                return JsonAdapterType.NONE;
            }
        }
    }

    @NotNull
    private static DeclaredType getDeclaredTypeForParameterizedClass(@NotNull String className) {
        WildcardType wildcardType = getUtils().getWildcardType(null, null);
        TypeMirror[] types = {wildcardType};
        return getUtils().getDeclaredType(ElementUtils.getTypeElementFromQualifiedName(className), types);
    }


    @NotNull
    public static DeclaredType getDeclaredType(TypeElement typeElem, TypeMirror... typeArgs) {
        Types types = getUtils();
        return types.getDeclaredType(typeElem, typeArgs);
    }

} @SuppressWarnings("UseOfSystemOutOrSystemErr")
public class ConsoleLogger implements Logger {

    @Override
    public void log(@NotNull String message) {
        System.out.println(message);
    }

} public final class DebugLog {

    private static final String TAG = "Stag";
    @Nullable
    private static Logger sLogger;

    private DebugLog() {
        throw new UnsupportedOperationException("This class is not instantiable");
    }

    /**
     * Initialize the log utils with the logging instance.
     *
     * @param logger the logging instance, if null is provided, logging will result in a crash.
     */
    public static void initialize(@Nullable Logger logger) {
        sLogger = logger;
    }

    @NotNull
    private static Logger safeLogger() {
        if (sLogger == null) {
            throw new IllegalStateException("initialize must be called first");
        }
        return sLogger;
    }

    /**
     * Log the provided message with the default log tag "Stag"
     *
     * @param message the message to log.
     */
    public static void log(@Nullable CharSequence message) {
        safeLogger().log(TAG + ": " + message);
    }

    /**
     * Log the provided message with an additional log tag.
     *
     * @param tag     the tag to add to the log.
     * @param message the message to log.
     */
    public static void log(@NotNull CharSequence tag, @Nullable CharSequence message) {
        safeLogger().log(TAG + ":" + tag + ": " + message);
    }

} public interface Logger {

    /**
     * Log the message to the output.
     *
     * @param message the message to log to the output.
     */
    void log(@NotNull String message);

} public class NoOpLogger implements Logger {

    @Override
    public void log(@NotNull String message) {
    }

}
import com.google.auto.service.AutoService;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.TypeSpec;
import com.vimeo.stag.UseStag;
import com.vimeo.stag.processor.generators.AdapterGenerator;
import com.vimeo.stag.processor.generators.EnumTypeAdapterGenerator;
import com.vimeo.stag.processor.generators.StagFactoryGenerator;
import com.vimeo.stag.processor.generators.StagGenerator;
import com.vimeo.stag.processor.generators.StagGenerator.SubFactoriesInfo;
import com.vimeo.stag.processor.generators.TypeAdapterGenerator;
import com.vimeo.stag.processor.generators.model.AnnotatedClass;
import com.vimeo.stag.processor.generators.model.ClassInfo;
import com.vimeo.stag.processor.generators.model.SupportedTypesModel;
import com.vimeo.stag.processor.generators.model.accessor.MethodFieldAccessor.Notation;
import com.vimeo.stag.processor.utils.ElementUtils;
import com.vimeo.stag.processor.utils.FileGenUtils;
import com.vimeo.stag.processor.utils.KnownTypeAdapterFactoriesUtils;
import com.vimeo.stag.processor.utils.MessagerUtils;
import com.vimeo.stag.processor.utils.TypeUtils;
import com.vimeo.stag.processor.utils.logging.ConsoleLogger;
import com.vimeo.stag.processor.utils.logging.DebugLog;
import com.vimeo.stag.processor.utils.logging.Logger;
import com.vimeo.stag.processor.utils.logging.NoOpLogger;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
/*
 * The MIT License (MIT)
 * <p/>
 * Copyright (c) 2016 Vimeo
 * <p/>
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * <p/>
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * <p/>
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.vimeo.stag.processor;





@AutoService(Processor.class)
@SupportedAnnotationTypes(value = {"com.vimeo.stag.UseStag"})
@SupportedOptions(value = {StagProcessor.OPTION_PACKAGE_NAME, StagProcessor.OPTION_DEBUG, StagProcessor.OPTION_HUNGARIAN_NOTATION, StagProcessor.OPTION_SERIALIZE_NULLS})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public final class StagProcessor extends AbstractProcessor {

    static final String OPTION_DEBUG = "stagDebug";
    static final String OPTION_PACKAGE_NAME = "stagGeneratedPackageName";
    static final String OPTION_HUNGARIAN_NOTATION = "stagAssumeHungarianNotation";
    static final String OPTION_SERIALIZE_NULLS = "stag.serializeNulls";
    private static final String DEFAULT_GENERATED_PACKAGE_NAME = "com.vimeo.stag.generated";
    private boolean mHasBeenProcessed;

    private static boolean getDebugBoolean(@NotNull ProcessingEnvironment processingEnvironment) {
        String debugString = processingEnvironment.getOptions().get(OPTION_DEBUG);
        if (debugString != null) {
            return Boolean.valueOf(debugString);
        }
        return false;
    }

    private static boolean getAssumeHungarianNotation(@NotNull ProcessingEnvironment processingEnvironment) {
        String debugString = processingEnvironment.getOptions().get(OPTION_HUNGARIAN_NOTATION);
        if (debugString != null) {
            return Boolean.valueOf(debugString);
        }
        return false;
    }

    private static boolean isSerializeNullsEnabled(@NotNull ProcessingEnvironment processingEnvironment) {
        String debugString = processingEnvironment.getOptions().get(OPTION_SERIALIZE_NULLS);
        if (debugString != null) {
            return Boolean.valueOf(debugString);
        }
        return false;
    }

    @NotNull
    private static String getOptionalPackageName(@NotNull ProcessingEnvironment processingEnvironment) {
        String packageName = processingEnvironment.getOptions().get(OPTION_PACKAGE_NAME);
        if (packageName == null || packageName.isEmpty()) {
            packageName = DEFAULT_GENERATED_PACKAGE_NAME;
        }
        return packageName;
    }

    /**
     * Adds all classes annotated with {@link UseStag}
     * to the supported type model. It does this recursively
     * for unsupported types. Supported types handle their
     * own enclosed element adding. Unsupported types that
     * could be annotated are @interface and interface. Enums
     * and classes are supported.
     *
     * @param supportedTypesModel the supported types model
     * @param useStagElement      the element to add to the
     *                            supported type model.
     */
    private static void processSupportedElements(@NotNull SupportedTypesModel supportedTypesModel,
                                                 @NotNull Element useStagElement) {
        if (ElementUtils.isSupportedElementKind(useStagElement)) {
            TypeMirror rootType = useStagElement.asType();
            DebugLog.log("Annotated type: " + rootType + "\n");
            supportedTypesModel.addSupportedType(rootType);
        }

        List<? extends Element> enclosedElements = useStagElement.getEnclosedElements();
        for (Element element : enclosedElements) {
            processSupportedElements(supportedTypesModel, element);
        }
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        // Always try to support the latest Java version
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        if (mHasBeenProcessed) {
            return true;
        }

        mHasBeenProcessed = true;

        final Logger logger;
        if (getDebugBoolean(processingEnv)) {
            logger = new ConsoleLogger();
        } else {
            logger = new NoOpLogger();
        }

        DebugLog.initialize(logger);

        String packageName = getOptionalPackageName(processingEnv);

        boolean assumeHungarianNotation = getAssumeHungarianNotation(processingEnv);
        boolean enableSerializeNulls = isSerializeNullsEnabled(processingEnv);

        TypeUtils.initialize(processingEnv.getTypeUtils());
        ElementUtils.initialize(processingEnv.getElementUtils());
        MessagerUtils.initialize(processingEnv.getMessager());

        Notation notation = assumeHungarianNotation ? Notation.HUNGARIAN : Notation.STANDARD;

        SupportedTypesModel supportedTypesModel = new SupportedTypesModel(notation);

        DebugLog.log("\nBeginning @UseStag annotation processing\n");

        // Pick up the classes annotated with @UseStag
        Set<? extends Element> useStagElements = roundEnv.getElementsAnnotatedWith(UseStag.class);
        for (Element useStagElement : useStagElements) {
            processSupportedElements(supportedTypesModel, useStagElement);
        }

        try {
            Set<TypeMirror> supportedTypes = AnnotatedClass.annotatedClassToTypeMirror(supportedTypesModel.getSupportedTypes());