/*******************************************************************************
 * Copyright (c) 2014 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the 
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0 
 * which accompanies this distribution. 
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at 
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     Dmitry Kornilov - Initial implementation
 ******************************************************************************/
package org.eclipse.persistence.internal.jpa.weaving;

import org.eclipse.persistence.dynamic.DynamicClassLoader;
import org.eclipse.persistence.dynamic.EclipseLinkClassWriter;
import org.eclipse.persistence.internal.libraries.asm.ClassWriter;
import org.eclipse.persistence.internal.libraries.asm.FieldVisitor;
import org.eclipse.persistence.internal.libraries.asm.Label;
import org.eclipse.persistence.internal.libraries.asm.MethodVisitor;
import org.eclipse.persistence.internal.libraries.asm.Opcodes;

/**
 * Generates a subclass of given collection implementing CollectionProxy interface.
 * {@see CollectionProxy}
 *
 * @author Dmitry Kornilov
 * @since EclipseLink 2.6.0
 */
public class CollectionProxyClassWriter implements EclipseLinkClassWriter, Opcodes {
    private static final String CLASS_NAME_SUFFIX = "CollectionProxy";
    private static final String INTERFACE = "org/eclipse/persistence/jpa/rs/util/CollectionProxy";

    private final String parentClassName;
    private final String entityName;
    private final String fieldName;

    /**
     * Creates a new CollectionProxyClassWriter for the given attribute of the given entity of given type.
     *
     * @param parentClassName the superclass name.
     * @param entityName entity name
     * @param fieldName entity attribute name
     */
    public CollectionProxyClassWriter(String parentClassName, String entityName, String fieldName) {
        this.parentClassName = parentClassName;
        this.entityName = entityName;
        this.fieldName = fieldName;
    }

    /**
     * Returns a class name for CollectionProxy based on parent class name and field name
     * to generate proxy for.
     * The name is constructed as _<className>_<fieldName>_RestCollectionProxy.
     *
     * @param entityName full class name (including package)
     * @param fieldName field name
     * @return Rest collection proxy name.
     */
    public static String getClassName(String entityName, String fieldName) {
        final int index = entityName.lastIndexOf('.');
        final String packageName = index >= 0 ? entityName.substring(0, index) : "";
        final String shortClassName = index >= 0 ? entityName.substring(index + 1) : entityName;
        return packageName + "._" + shortClassName + "_" + fieldName + "_" + CLASS_NAME_SUFFIX;
    }

    /**
     * Returns a class name for generated CollectionProxy.
     * {@see CollectionProxyClassWriter#getClassName(String, String)}
     *
     * @return Rest collection proxy name.
     */
    public String getClassName() {
        return getClassName(entityName, fieldName);
    }
    
    /**
     *  public class Proxy extends SuperType implements CollectionProxy {
     *      private List<LinkV2> links;
     *
     *      public CollectionProxy(Collection c) {
     *          super();
     *          this.addAll(c);
     *      }
     *
     *      @Override
     *      public List<LinkV2> getLinks() {
     *          return links;
     *      }
     *
     *      @Override
     *      public void setLinks(List<LinkV2> links) {
     *          this.links = links;
     *      }
     *  }
     *
     * @param loader
     * @param className
     * @return
     * @throws Exception
     */
    @Override
    public byte[] writeClass(DynamicClassLoader loader, String className) {

        final ClassWriter cw = new ClassWriter(0);
        MethodVisitor mv;

        // public class Proxy extends SuperType implements CollectionProxy
        cw.visit(V1_6, ACC_PUBLIC + ACC_SUPER, getASMClassName(), null, getASMParentClassName(), new String[]{INTERFACE});

        // private List<LinkV2> links;
        final FieldVisitor fv = cw.visitField(ACC_PRIVATE, "links", "Ljava/util/List;", "Ljava/util/List<Lorg/eclipse/persistence/internal/jpa/rs/metadata/model/LinkV2;>;", null);
        fv.visitEnd();

        // public CollectionProxy(Collection c) {
        //     super();
        //     this.addAll(c);
        // }
        {
            mv = cw.visitMethod(ACC_PUBLIC, "<init>", "(Ljava/util/Collection;)V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(15, l0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitMethodInsn(INVOKESPECIAL, getASMParentClassName(), "<init>", "()V", false);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(16, l1);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitMethodInsn(INVOKEVIRTUAL, getASMClassName(), "addAll", "(Ljava/util/Collection;)Z", false);
            mv.visitInsn(POP);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLineNumber(17, l2);
            mv.visitInsn(RETURN);
            Label l3 = new Label();
            mv.visitLabel(l3);
            mv.visitLocalVariable("this", "L" + getASMClassName() + ";", null, l0, l3, 0);
            mv.visitLocalVariable("c", "Ljava/util/Collection;", null, l0, l3, 1);
            mv.visitMaxs(2, 2);
            mv.visitEnd();
        }

        // @Override
        // public List<LinkV2> getLinks() {
        //    return links;
        // }
        {
            mv = cw.visitMethod(ACC_PUBLIC, "getLinks", "()Ljava/util/List;", "()Ljava/util/List<Lorg/eclipse/persistence/internal/jpa/rs/metadata/model/LinkV2;>;", null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(21, l0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitFieldInsn(GETFIELD, getASMClassName(), "links", "Ljava/util/List;");
            mv.visitInsn(ARETURN);
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLocalVariable("this", "L" + getASMClassName() + ";", null, l0, l1, 0);
            mv.visitMaxs(1, 1);
            mv.visitEnd();
        }

        // @Override
        // public void setLinks(List<LinkV2> links) {
        //    this.links = links;
        // }
        {
            mv = cw.visitMethod(ACC_PUBLIC, "setLinks", "(Ljava/util/List;)V", "(Ljava/util/List<Lorg/eclipse/persistence/internal/jpa/rs/metadata/model/LinkV2;>;)V", null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(26, l0);
            mv.visitVarInsn(ALOAD, 0);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitFieldInsn(PUTFIELD, getASMClassName(), "links", "Ljava/util/List;");
            Label l1 = new Label();
            mv.visitLabel(l1);
            mv.visitLineNumber(27, l1);
            mv.visitInsn(RETURN);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLocalVariable("this", "L" + getASMClassName()+ ";", null, l0, l2, 0);
            mv.visitLocalVariable("links", "Ljava/util/List;", "Ljava/util/List<Lorg/eclipse/persistence/internal/jpa/rs/metadata/model/LinkV2;>;", l0, l2, 1);
            mv.visitMaxs(2, 2);
            mv.visitEnd();
        }

        return cw.toByteArray();
    }

    @Override
    public boolean isCompatible(EclipseLinkClassWriter writer) {
        return getParentClassName().equals(writer.getParentClassName());
    }

    @Override
    public Class<?> getParentClass() {
        return null;
    }
    
    @Override
    public String getParentClassName() {
        return parentClassName;
    }

    private String getASMClassName() {
        return getClassName().replace('.', '/');
    }

    private String getASMParentClassName() {
        return parentClassName.replace('.', '/');
    }
}

