package act.util;

/*-
 * #%L
 * ACT Framework
 * %%
 * Copyright (C) 2014 - 2017 ActFramework
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import act.asm.*;
import act.data.annotation.Data;
import org.osgl.$;
import org.osgl.util.FastStr;
import org.osgl.util.S;

import java.util.List;

/**
 * A tool to enhance a object by generating common {@link Object}
 * methods, e.g. {@link Object#equals(Object)}
 */
public class DataObjectEnhancer extends AppByteCodeEnhancer<DataObjectEnhancer> {

    private ObjectMetaInfo metaInfo;

    public DataObjectEnhancer() {
        super(S.F.startsWith("act.").negate().or(S.F.startsWith("act.fsa")));
    }

    protected DataObjectEnhancer(ClassVisitor cv) {
        super($.F.<String>yes(), cv);
    }

    @Override
    protected Class<DataObjectEnhancer> subClass() {
        return DataObjectEnhancer.class;
    }

    @Override
    protected void reset() {
        this.metaInfo = null;
        super.reset();
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        Type type = Type.getObjectType(name);
        Type superType = null == superName ? null : Type.getObjectType(superName);
        metaInfo = new ObjectMetaInfo(type, superType);
        super.visit(version, access, name, signature, superName, interfaces);
    }

    private static final String EQ_IGNORE = Type.getType(EqualIgnore.class).getDescriptor();
    private static final String EQ_FORCE = Type.getType(EqualField.class).getDescriptor();

    @Override
    public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
        FieldVisitor fv = super.visitField(access, name, desc, signature, value);
        boolean isStatic = ((access & ACC_STATIC) != 0);
        if (isStatic) {
            return fv;
        }
        if (metaInfo.hasDataAnnotation()) {
            boolean isTransient = ((access & ACC_TRANSIENT) != 0);
            Type fieldType = Type.getType(desc);
            final ObjectMetaInfo.FieldMetaInfo fi = metaInfo.addField(name, fieldType, isTransient);
            return new FieldVisitor(ASM5, fv) {
                @Override
                public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                    if (EQ_IGNORE.equals(desc)) {
                        fi.setEqualIgnore();
                    } else if (EQ_FORCE.equals(desc)) {
                        fi.setEqualForce();
                    }
                    return super.visitAnnotation(desc, visible);
                }
            };
        }
        return fv;
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        AnnotationVisitor def = super.visitAnnotation(desc, visible);
        if (Type.getType(Data.class).getDescriptor().equals(desc)) {
            metaInfo.autoObjectAnnotationFound();
            return new AnnotationVisitor(ASM5, def) {
                @Override
                public void visit(String name, Object value) {
                    if ("callSuper".equals(name)) {
                        String s = String.valueOf(value);
                        if (Boolean.parseBoolean(s)) {
                            metaInfo.requireCallSuper();
                        }
                    }
                    super.visit(name, value);
                }
            };
        }
        return def;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        if (metaInfo.hasDataAnnotation()) {
            if (S.eq("hashCode", name) && S.eq("()I", desc)) {
                metaInfo.hashCodeMethodFound();
            } else if (S.eq("equals", name) && S.eq("(Ljava/lang/Object;)Z", desc)) {
                metaInfo.equalMethodFound();
            }
        }
        return mv;
    }

    @Override
    public void visitEnd() {
        if (metaInfo.shouldGenerateEqualsMethod()) {
            generateEqualsMethod();
        }
        if (metaInfo.shouldGenerateHashCodeMethod()) {
            generateHashCodeMethod();
        }
        super.visitEnd();
    }

    private void generateEqualsMethod() {
        MethodVisitor mv = equalsMethodBegin(this);
        // check if obj == this
        mv.visitVarInsn(ALOAD, 1); // obj
        mv.visitVarInsn(ALOAD, 0); // this
        Label lbl_continue_with_instanceof_check = new Label();
        mv.visitJumpInsn(IF_ACMPNE, lbl_continue_with_instanceof_check);
        mv.visitInsn(ICONST_1); // true
        mv.visitInsn(IRETURN); // return
        mv.visitLabel(lbl_continue_with_instanceof_check);
        // check if obj instance of the type
        mv.visitVarInsn(ALOAD, 1); // obj
        Type host = metaInfo.type();
        String hostInternalName = host.getInternalName();
        mv.visitTypeInsn(INSTANCEOF, hostInternalName);
        Label lbl_exit_with_false = new Label();
        mv.visitJumpInsn(IFEQ, lbl_exit_with_false); // if not instance of then return false
        // do type cast
        mv.visitVarInsn(ALOAD, 1); // obj
        mv.visitTypeInsn(CHECKCAST, hostInternalName);
        mv.visitVarInsn(ASTORE, 2); // store cast object to that
        // should we check super result?
        if (metaInfo.shouldCallSuper()) {
            mv.visitVarInsn(ALOAD, 0); // load this
            mv.visitVarInsn(ALOAD, 2); // load that
            mv.visitMethodInsn(INVOKESPECIAL, metaInfo.superType().getInternalName(), "equals", "(Ljava/lang/Object;)Z", false);
            mv.visitJumpInsn(IFEQ, lbl_exit_with_false);
        }
        // call Osgl.eq(that.f1, this.f1)
        List<ObjectMetaInfo.FieldMetaInfo> fields = metaInfo.fields();
        for (ObjectMetaInfo.FieldMetaInfo fi : fields) {
            fi.addEqualInstructions(host, mv, lbl_exit_with_false);
        }
        mv.visitInsn(ICONST_1); // true
        mv.visitInsn(IRETURN);
        mv.visitLabel(lbl_exit_with_false);
        mv.visitInsn(ICONST_0); // false
        mv.visitInsn(IRETURN);
        equalsMethodEnd(mv);
    }

    private int fieldCount(List<ObjectMetaInfo.FieldMetaInfo> fields) {
        int cnt = 0;
        for (ObjectMetaInfo.FieldMetaInfo field : fields) {
            if (field.eligible()) {
                cnt++;
            }
        }
        return cnt;
    }

    private void generateHashCodeMethod() {
        MethodVisitor mv = hashCodeMethodBegin(this);
        Type host = metaInfo.type();
        List<ObjectMetaInfo.FieldMetaInfo> fields = metaInfo.fields();
        int fieldCount = fieldCount(fields);
        if (fieldCount < 6) {
            int cnt = 0;
            for (ObjectMetaInfo.FieldMetaInfo fi : fields) {
                boolean added = fi.addHashCodeInstruction(host, mv);
                if (added) {
                    cnt++;
                }
            }
            if (metaInfo.shouldCallSuper()) {
                mv.visitVarInsn(ALOAD, 0);
                mv.visitMethodInsn(INVOKESPECIAL, metaInfo.superType().getInternalName(), "hashCode", "()I", false);
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
                cnt++;
            }
            hashCodeMethodEnd(mv, cnt);
        } else {
            mv.visitTypeInsn(NEW, "java/util/ArrayList");
            mv.visitInsn(DUP);
            mv.visitIntInsn(BIPUSH, fieldCount);
            mv.visitMethodInsn(INVOKESPECIAL, "java/util/ArrayList", "<init>", "(I)V", false);
            mv.visitVarInsn(ASTORE, 1);
            for (ObjectMetaInfo.FieldMetaInfo fi : fields) {
                if (!fi.eligible()) {
                    continue;
                }
                mv.visitVarInsn(ALOAD, 1);
                fi.addHashCodeInstruction(host, mv);
                mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true);
                mv.visitInsn(POP);
            }
            if (metaInfo.shouldCallSuper()) {
                mv.visitVarInsn(ALOAD, 0);
                mv.visitMethodInsn(INVOKESPECIAL, metaInfo.superType().getInternalName(), "hashCode", "()I", false);
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
            }
            mv.visitVarInsn(ALOAD, 1);
            mv.visitMethodInsn(INVOKESTATIC, "org/osgl/$", "hc", "(Ljava/lang/Object;)I", false);
            mv.visitInsn(IRETURN);
            mv.visitMaxs(0, 0);
            mv.visitEnd();
        }
    }

    static MethodVisitor equalsMethodBegin(ClassVisitor cw) {
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "equals", "(Ljava/lang/Object;)Z", null, null);
        mv.visitCode();
        return mv;
    }
    static void equalsMethodEnd(MethodVisitor mv) {
        mv.visitMaxs(0, 0); // just pass any number and have ASM to calculate
        mv.visitEnd();
    }
    static MethodVisitor hashCodeMethodBegin(ClassVisitor cw) {
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "hashCode", "()I", null, null);
        mv.visitCode();
        return mv;
    }
    static void hashCodeMethodEnd(MethodVisitor mv, int fieldCnt) {
        FastStr signature;
        if (fieldCnt < 6) {
            signature = FastStr.of("Ljava/lang/Object;").times(fieldCnt);
        } else {
            signature = FastStr.of("Ljava/lang/Object;").times(5).append("[Ljava/lang/Object;");
        }
        signature = signature.prepend("(").append(")I");
        mv.visitMethodInsn(INVOKESTATIC, "org/osgl/Osgl", "hc", signature.toString(), false);
        mv.visitInsn(IRETURN);
        mv.visitMaxs(0, 0); // just pass any number and have ASM to calculate
        mv.visitEnd();
    }

}
