package com.jsoniter.output;

import com.jsoniter.*;
import com.jsoniter.CodegenAccess;
import com.jsoniter.spi.*;

import java.lang.reflect.Method;
import java.util.*;

class CodegenImplObject {
    public static CodegenResult genObject(Class clazz) {

        CodegenResult ctx = new CodegenResult();
        ClassDescriptor desc = JsoniterSpi.getEncodingClassDescriptor(clazz, false);
        HashMap<String, Binding> bindings = new HashMap<String, Binding>();
        for (Binding binding : desc.allEncoderBindings()) {
            for (String toName : binding.toNames) {
                bindings.put(toName, binding);
            }
        }
        ArrayList<String> toNames = new ArrayList<String>(bindings.keySet());
        Collections.sort(toNames, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                int x = CodegenAccess.calcHash(o1);
                int y = CodegenAccess.calcHash(o2);
                return (x < y) ? -1 : ((x == y) ? 0 : 1);
            }
        });
        ctx.append(String.format("public static void encode_(%s obj, com.jsoniter.output.JsonStream stream) throws java.io.IOException {", clazz.getCanonicalName()));
        if (hasFieldOutput(desc)) {
            int notFirst = 0;
            ctx.buffer('{');
            for (String toName : toNames) {
                notFirst = genField(ctx, bindings.get(toName), toName, notFirst);
            }
            for (Method unwrapper : desc.unWrappers) {
                notFirst = appendComma(ctx, notFirst);
                ctx.append(String.format("obj.%s(stream);", unwrapper.getName()));
            }
            ctx.buffer('}');
        } else {
            ctx.buffer("{}");
        }
        ctx.append("}");
        return ctx;
    }


    private static boolean hasFieldOutput(ClassDescriptor desc) {
        if (!desc.unWrappers.isEmpty()) {
            return true;
        }
        for (Binding binding : desc.allEncoderBindings()) {
            if (binding.toNames.length > 0) {
                return true;
            }
        }
        return false;
    }

    private static int genField(CodegenResult ctx, Binding binding, String toName, int notFirst) {
        String fieldCacheKey = binding.encoderCacheKey();
        Encoder encoder = JsoniterSpi.getEncoder(fieldCacheKey);
        boolean isCollectionValueNullable = binding.isCollectionValueNullable;
        Class valueClazz;
        String valueAccessor;
        if (binding.field != null) {
            valueClazz = binding.field.getType();
            valueAccessor = "obj." + binding.field.getName();
        } else {
            valueClazz = binding.method.getReturnType();
            valueAccessor = "obj." + binding.method.getName() + "()";
        }
        if (!supportCollectionValueNullable(valueClazz)) {
            isCollectionValueNullable = true;
        }
        boolean nullable = !valueClazz.isPrimitive();
        if (!binding.isNullable) {
            nullable = false;
        }
        if (nullable) {
            if (binding.shouldOmitNull) {
                if (notFirst == 0) { // no previous field
                    notFirst = 2; // maybe
                    ctx.append("boolean notFirst = false;");
                }
                ctx.append(String.format("if (%s != null) {", valueAccessor));
                notFirst = appendComma(ctx, notFirst);
                ctx.append(CodegenResult.bufferToWriteOp("\"" + toName + "\":"));
            } else {
                notFirst = appendComma(ctx, notFirst);
                ctx.buffer('"');
                ctx.buffer(toName);
                ctx.buffer('"');
                ctx.buffer(':');
                ctx.append(String.format("if (%s == null) { stream.writeNull(); } else {", valueAccessor));
            }
        } else {
            notFirst = appendComma(ctx, notFirst);
            ctx.buffer('"');
            ctx.buffer(toName);
            ctx.buffer('"');
            ctx.buffer(':');
        }
        if (encoder == null) {
            CodegenImplNative.genWriteOp(ctx, valueAccessor, binding.valueType, nullable, isCollectionValueNullable);
        } else {
            ctx.append(String.format("com.jsoniter.output.CodegenAccess.writeVal(\"%s\", %s, stream);",
                    fieldCacheKey, valueAccessor));
        }
        if (nullable) {
            ctx.append("}");
        }
        return notFirst;
    }

    private static int appendComma(CodegenResult ctx, int notFirst) {
        if (notFirst == 1) { // definitely not first
            ctx.buffer(',');
        } else if (notFirst == 2) { // maybe not first, previous field is omitNull
            ctx.append("if (notFirst) { stream.write(','); } else { notFirst = true; }");
        } else { // this is the first, do not write comma
            notFirst = 1;
        }
        return notFirst;
    }


    private static boolean supportCollectionValueNullable(Class clazz) {
        if (clazz.isArray()) {
            return true;
        }
        if (Map.class.isAssignableFrom(clazz)) {
            return true;
        }
        if (Collection.class.isAssignableFrom(clazz)) {
            return true;
        }
        return false;
    }
}
