/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.codehaus.groovy.transform.trait;

import groovy.lang.GeneratedGroovyProxy;
import groovy.transform.SelfType;
import groovy.transform.Trait;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.ast.AnnotationNode;
import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.FieldNode;
import org.codehaus.groovy.ast.InnerClassNode;
import org.codehaus.groovy.ast.MethodNode;
import org.codehaus.groovy.ast.expr.ClassExpression;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ListExpression;
import org.codehaus.groovy.ast.tools.GenericsUtils;
import org.codehaus.groovy.classgen.asm.BytecodeHelper;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.objectweb.asm.Opcodes;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;

/**
 * A collection of utility methods used to deal with traits.
 *
 * @author Cédric Champeau
 * @since 2.3.0
 */
public abstract class Traits {
    public static final ClassNode IMPLEMENTED_CLASSNODE = ClassHelper.make(Implemented.class);
    public static final ClassNode TRAITBRIDGE_CLASSNODE = ClassHelper.make(TraitBridge.class);
    public static final Class TRAIT_CLASS = Trait.class;
    public static final ClassNode TRAIT_CLASSNODE = ClassHelper.make(TRAIT_CLASS);
    public static final ClassNode GENERATED_PROXY_CLASSNODE = ClassHelper.make(GeneratedGroovyProxy.class);
    public static final ClassNode SELFTYPE_CLASSNODE = ClassHelper.make(SelfType.class);

    static final String TRAIT_TYPE_NAME = "@" + TRAIT_CLASSNODE.getNameWithoutPackage();
    static final String TRAIT_HELPER = "$Trait$Helper";
    static final String FIELD_HELPER = "$Trait$FieldHelper";
    static final String DIRECT_SETTER_SUFFIX = "$set";
    static final String DIRECT_GETTER_SUFFIX = "$get";
    static final String INIT_METHOD = "$init$";
    static final String STATIC_INIT_METHOD = "$static$init$";
    public static final String THIS_OBJECT = "$self";
    public static final String STATIC_THIS_OBJECT = "$static$self";
    static final String STATIC_FIELD_PREFIX = "$static";
    static final String FIELD_PREFIX = "$ins";
    static final String PUBLIC_FIELD_PREFIX = "$0";
    static final String PRIVATE_FIELD_PREFIX = "$1";
    // TODO decide if we should support VOLATILE
//    def hex(s) {new BigInteger(s, 16).intValue()}
//    def optionals = [[0, 1], [0, 1], [0, 1], [0, 1]].combinations{ a, b, c, d ->
//            (a ? hex('80') : 0) + (b ? hex('10') : 0) + (c ? hex('8') : 0) + (d ? hex('2') : hex('1'))
//    }.sort()
    static final List<Integer> FIELD_PREFIXES = Arrays.asList(1, 2, 9, 10, 17, 18, 25, 26, 129, 130, 137, 138, 145, 146, 153, 154);
    static final int FIELD_PREFIX_MASK = Opcodes.ACC_PRIVATE | Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC | Opcodes.ACC_FINAL | Opcodes.ACC_TRANSIENT;
    static final String SUPER_TRAIT_METHOD_PREFIX = "trait$super$";

    static String fieldHelperClassName(final ClassNode traitNode) {
        return traitNode.getName() + FIELD_HELPER;
    }

    static String helperGetterName(final FieldNode field) {
        return remappedFieldName(unwrapOwner(field.getOwner()), field.getName()) + DIRECT_GETTER_SUFFIX;
    }

    static String helperSetterName(final FieldNode field) {
        return remappedFieldName(unwrapOwner(field.getOwner()), field.getName()) + DIRECT_SETTER_SUFFIX;
    }

    static String helperClassName(final ClassNode traitNode) {
        return traitNode.getName() + TRAIT_HELPER;
    }

    static String remappedFieldName(final ClassNode traitNode, final String name) {
        return traitNode.getName().replace('.','_')+"__"+name;
    }

    private static ClassNode unwrapOwner(ClassNode owner) {
        if (ClassHelper.CLASS_Type.equals(owner) && owner.getGenericsTypes()!=null && owner.getGenericsTypes().length==1) {
            return owner.getGenericsTypes()[0].getType();
        }
        return owner;
    }

    public static ClassNode findHelper(final ClassNode trait) {
        return findHelpers(trait).getHelper();
    }

    public static ClassNode findFieldHelper(final ClassNode trait) {
        return findHelpers(trait).getFieldHelper();
    }

    static TraitHelpersTuple findHelpers(final ClassNode trait) {
        ClassNode helperClassNode = null;
        ClassNode fieldHelperClassNode = null;
        Iterator<InnerClassNode> innerClasses = trait.redirect().getInnerClasses();
        if (innerClasses != null && innerClasses.hasNext()) {
            // trait defined in same source unit
            while (innerClasses.hasNext()) {
                ClassNode icn = innerClasses.next();
                if (icn.getName().endsWith(Traits.FIELD_HELPER)) {
                    fieldHelperClassNode = icn;
                } else if (icn.getName().endsWith(Traits.TRAIT_HELPER)) {
                    helperClassNode = icn;
                }
            }
        } else {
            // precompiled trait
            try {
                final ClassLoader classLoader = trait.getTypeClass().getClassLoader();
                String helperClassName = Traits.helperClassName(trait);
                helperClassNode = ClassHelper.make(Class.forName(helperClassName, false, classLoader));
                try {
                    fieldHelperClassNode = ClassHelper.make(classLoader.loadClass(Traits.fieldHelperClassName(trait)));
                } catch (ClassNotFoundException e) {
                    // not a problem, the field helper may be absent
                }
            } catch (ClassNotFoundException e) {
                throw new GroovyBugError("Couldn't find trait helper classes on compile classpath!",e);
            }
        }
        return new TraitHelpersTuple(helperClassNode,  fieldHelperClassNode);
    }

    /**
     * Returns true if the specified class node is a trait.
     * @param cNode a class node to test
     * @return true if the classnode represents a trait
     */
    public static boolean isTrait(final ClassNode cNode) {
        return cNode != null && isAnnotatedWithTrait(cNode);
    }

    /**
     * Returns true if the specified class is a trait.
     * @param clazz a class to test
     * @return true if the classnode represents a trait
     */
    public static boolean isTrait(final Class clazz) {
        return clazz!=null && clazz.getAnnotation(Trait.class)!=null;
    }


    /**
     * Returns true if the specified class node is annotated with the {@link Trait} interface.
     * @param cNode a class node
     * @return true if the specified class node is annotated with the {@link Trait} interface.
     */
    public static boolean isAnnotatedWithTrait(final ClassNode cNode) {
        List<AnnotationNode> traitAnn = cNode.getAnnotations(Traits.TRAIT_CLASSNODE);
        return traitAnn != null && !traitAnn.isEmpty();
    }

    /**
     * Indicates whether a method in a trait interface has a default implementation.
     * @param method a method node
     * @return true if the method has a default implementation in the trait
     */
    public static boolean hasDefaultImplementation(final MethodNode method) {
        return !method.getAnnotations(IMPLEMENTED_CLASSNODE).isEmpty();
    }

    /**
     * Indicates whether a method in a trait interface has a default implementation.
     * @param method a method node
     * @return true if the method has a default implementation in the trait
     */
    public static boolean hasDefaultImplementation(final Method method) {
        return method.getAnnotation(Implemented.class)!=null;
    }

    /**
     * Reflection API to indicate whether some method is a bridge method to the default implementation
     * of a trait.
     * @param someMethod a method node
     * @return null if it is not a method implemented in a trait. If it is, returns the method from the trait class.
     */
    public static boolean isBridgeMethod(Method someMethod) {
        TraitBridge annotation = someMethod.getAnnotation(TraitBridge.class);
        return annotation!=null;
    }

    /**
     * Reflection API to find the method corresponding to the default implementation of a trait, given a bridge method.
     * @param someMethod a method node
     * @return null if it is not a method implemented in a trait. If it is, returns the method from the trait class.
     */
    public static Method getBridgeMethodTarget(Method someMethod) {
        TraitBridge annotation = someMethod.getAnnotation(TraitBridge.class);
        if (annotation==null) {
            return null;
        }
        Class aClass = annotation.traitClass();
        String desc = annotation.desc();
        for (Method method : aClass.getDeclaredMethods()) {
            String methodDescriptor = BytecodeHelper.getMethodDescriptor(method.getReturnType(), method.getParameterTypes());
            if (desc.equals(methodDescriptor)) {
                return method;
            }
        }
        return null;
    }


    /**
     * Converts a class implementing some trait into a target class. If the trait is a dynamic proxy and
     * that the target class is assignable to the target object of the proxy, then the target object is
     * returned. Otherwise, falls back to {@link org.codehaus.groovy.runtime.DefaultGroovyMethods#asType(java.lang.Object, Class)}
     * @param self an object to be coerced to some class
     * @param clazz the class to be coerced to
     * @return the object coerced to the target class, or the proxy instance if it is compatible with the target class.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getAsType(Object self, Class<T> clazz) {
        if (self instanceof GeneratedGroovyProxy) {
            Object proxyTarget = ((GeneratedGroovyProxy)self).getProxyTarget();
            if (clazz.isAssignableFrom(proxyTarget.getClass())) {
                return (T) proxyTarget;
            }
        }
        return DefaultGroovyMethods.asType(self, clazz);
    }

    /**
     * Returns the name of a method without the super trait specific prefix. If the method name
     * doesn't correspond to a super trait method call, the result will be null.
     * @param origName the name of a method
     * @return null if the name doesn't start with the super trait prefix, otherwise the name without the prefix
     */
    public static String[] decomposeSuperCallName(String origName) {
        if (origName.contains(SUPER_TRAIT_METHOD_PREFIX)) {
            int endIndex = origName.indexOf(SUPER_TRAIT_METHOD_PREFIX);
            String tName = origName.substring(0, endIndex).replace('_','.').replace("..","_");
            String fName = origName.substring(endIndex+SUPER_TRAIT_METHOD_PREFIX.length());
            return new String[]{tName, fName};
        }
        return null;
    }

    /**
     * Collects all interfaces of a class node, but reverses the order of the declaration of direct interfaces
     * of this class node. This is used to make sure a trait implementing A,B where both A and B have the same
     * method will take the method from B (latest), aligning the behavior with categories.
     * @param cNode a class node
     * @param interfaces ordered set of interfaces
     */
    public static LinkedHashSet<ClassNode> collectAllInterfacesReverseOrder(ClassNode cNode, LinkedHashSet<ClassNode> interfaces) {
        if (cNode.isInterface())
            interfaces.add(cNode);

        ClassNode[] directInterfaces = cNode.getInterfaces();
        for (int i = directInterfaces.length-1; i >=0 ; i--) {
            final ClassNode anInterface = directInterfaces[i];
            interfaces.add(GenericsUtils.parameterizeType(cNode,anInterface));
            collectAllInterfacesReverseOrder(anInterface, interfaces);
        }
        return interfaces;
    }

    /**
     * Collects all the self types that a type should extend or implement, given
     * the traits is implements. Collects from interfaces and superclasses too.
     * @param receiver a class node that may implement a trait
     * @param selfTypes a collection where the list of self types will be written
     * @return the selfTypes collection itself
     * @since 2.4.0
     */
    public static LinkedHashSet<ClassNode> collectSelfTypes(
            ClassNode receiver,
            LinkedHashSet<ClassNode> selfTypes) {
        return collectSelfTypes(receiver, selfTypes, true, true);
    }

    /**
     * Collects all the self types that a type should extend or implement, given
     * the traits is implements.
     * @param receiver a class node that may implement a trait
     * @param selfTypes a collection where the list of self types will be written
     * @param checkInterfaces should the interfaces that the node implements be collected too
     * @param checkSuper should we collect from the superclass too
     * @return the selfTypes collection itself
     * @since 2.4.0
     */
    public static LinkedHashSet<ClassNode> collectSelfTypes(
            ClassNode receiver,
            LinkedHashSet<ClassNode> selfTypes,
            boolean checkInterfaces,
            boolean checkSuper) {
        if (Traits.isTrait(receiver)) {
            List<AnnotationNode> annotations = receiver.getAnnotations(SELFTYPE_CLASSNODE);
            for (AnnotationNode annotation : annotations) {
                Expression value = annotation.getMember("value");
                if (value instanceof ClassExpression) {
                    selfTypes.add(value.getType());
                } else if (value instanceof ListExpression) {
                    List<Expression> expressions = ((ListExpression) value).getExpressions();
                    for (Expression expression : expressions) {
                        if (expression instanceof ClassExpression) {
                            selfTypes.add(expression.getType());
                        }
                    }
                }
            }
        }
        if (checkInterfaces) {
            ClassNode[] interfaces = receiver.getInterfaces();
            for (ClassNode anInterface : interfaces) {
                collectSelfTypes(anInterface, selfTypes, true, checkSuper);
            }
        }

        if (checkSuper) {
            ClassNode superClass = receiver.getSuperClass();
            if (superClass != null) {
                collectSelfTypes(superClass, selfTypes, checkInterfaces, true);
            }
        }
        return selfTypes;
    }

    static String getSuperTraitMethodName(ClassNode trait, String method) {
        return trait.getName().replace("_","__").replace('.','_')+SUPER_TRAIT_METHOD_PREFIX+method;
    }

    /**
     * Internal annotation used to indicate which methods in a trait interface have a
     * default implementation.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public static @interface Implemented {}

    /**
     * Internal annotation used to indicate that a method is a bridge method to a trait
     * default implementation.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
     public static @interface TraitBridge {
        /**
         * @return the trait class
         */
        Class traitClass();

        /**
         * @return The method descriptor of the method from the trait
         */
        String desc();
    }

}
