/*
 * This file is part of Mixin, licensed under the MIT License (MIT).
 *
 * Copyright (c) SpongePowered <https://www.spongepowered.org>
 * Copyright (c) contributors
 *
 * 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:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * 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 org.spongepowered.tools.obfuscation.mirror;

import java.util.List;

import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;

import org.spongepowered.asm.util.Constants;
import org.spongepowered.asm.util.SignaturePrinter;

/**
 * Convenience functions for mirror types
 */
public abstract class TypeUtils {
    
    /**
     * Result type returned by {@link TypeUtils#isEquivalentType}
     */
    public enum Equivalency {
        
        /**
         * Types are not equivalent
         */
        NOT_EQUIVALENT,
        
        /**
         * Types are equivalent, but one type is raw 
         */
        EQUIVALENT_BUT_RAW,
        
        /**
         * Types are equivalent, but generic type parameters do not match 
         */
        BOUNDS_MISMATCH,
        
        /**
         * Types are equivalent, any generic type parameters are also equivalent
         * or both types are raw
         */
        EQUIVALENT
        
    }
    
    /**
     * Result bundle from a type equivalency check. See
     * {@link TypeUtils#isEquivalentType} for details
     */
    public static class EquivalencyResult {
        
        /**
         * Singleton for equivalent type results
         */
        static final EquivalencyResult EQUIVALENT = new EquivalencyResult(Equivalency.EQUIVALENT, "", 0);
        
        /**
         * The equivalency result type
         */
        public final Equivalency type;
        
        /**
         * Detail string for use in user-facing error messages describing the
         * nature of match failures
         */
        public final String detail;
        
        /**
         * For {@link Equivalency#EQUIVALENT_BUT_RAW} indicates which argument
         * was the raw one. This should only ever have the values 0 (for not
         * relevant), 1 or 2. It is up to the outer scope (the caller) to
         * determine whether to warn based on the rawness of the respective
         * "left" and "right" types.
         */
        public final int rawType;
        
        EquivalencyResult(Equivalency type, String detail, int rawType) {
            this.type = type;
            this.detail = detail;
            this.rawType = rawType;
        }
        
        @Override
        public String toString() {
            return this.detail;
        }
        
        static EquivalencyResult notEquivalent(String format, Object... args) {
            return new EquivalencyResult(Equivalency.NOT_EQUIVALENT, String.format(format, args), 0);
        }

        static EquivalencyResult boundsMismatch(String format, Object... args) {
            return new EquivalencyResult(Equivalency.BOUNDS_MISMATCH, String.format(format, args), 0);
        }
        
        static EquivalencyResult equivalentButRaw(int rawType) {
            return new EquivalencyResult(Equivalency.EQUIVALENT_BUT_RAW, String.format("Type %d is raw", rawType), rawType);
        }
        
    }
    
    /**
     * Number of times to recurse into TypeMirrors when trying to determine the
     * upper bound of a TYPEVAR 
     */
    private static final int MAX_GENERIC_RECURSION_DEPTH = 5;
    
    private static final String OBJECT_SIG = "java.lang.Object";

    // No instances for you
    private TypeUtils() {}

    /**
     * If the supplied type is a {@link DeclaredType}, return the package in
     * which it is declared
     * 
     * @param type type to find package for
     * @return package for supplied type or null
     */
    public static PackageElement getPackage(TypeMirror type) {
        if (!(type instanceof DeclaredType)) {
            return null;
        }
        return TypeUtils.getPackage((TypeElement)((DeclaredType)type).asElement());
    }
    
    /**
     * Return the package in which the specified type element is declared
     * @param type type to find package for
     * @return package for supplied type or null
     */
    public static PackageElement getPackage(TypeElement type) {
        Element parent = type.getEnclosingElement();
        while (parent != null && !(parent instanceof PackageElement)) {
            parent = parent.getEnclosingElement();
        }
        return (PackageElement)parent;
    }

    /**
     * Convenience method to convert element to string representation for error
     * messages
     * 
     * @param element Element to inspect
     * @return string representation of element name
     */
    public static String getElementType(Element element) {
        if (element instanceof TypeElement) {
            return "TypeElement";
        } else if (element instanceof ExecutableElement) {
            return "ExecutableElement";
        } else if (element instanceof VariableElement) {
            return "VariableElement";
        } else if (element instanceof PackageElement) {
            return "PackageElement";
        } else if (element instanceof TypeParameterElement) {
            return "TypeParameterElement";
        }
        
        return element.getClass().getSimpleName();
    }

    /**
     * Strip generic arguments from the supplied type descriptor
     * 
     * @param type type descriptor
     * @return type descriptor with generic args removed
     */
    public static String stripGenerics(String type) {
        StringBuilder sb = new StringBuilder();
        for (int pos = 0, depth = 0; pos < type.length(); pos++) {
            char c = type.charAt(pos);
            if (c == '<') {
                depth++;
            }
            if (depth == 0) {
                sb.append(c);
            } else if (c == '>') {
                depth--;
            }
        }
        return sb.toString();
    }
    
    /**
     * Get the name of the specified field
     * 
     * @param field field element
     * @return field name
     */
    public static String getName(VariableElement field) {
        return field != null ? field.getSimpleName().toString() : null;
    }
    
    /**
     * Get the name of the specified method
     * 
     * @param method method element
     * @return method name
     */
    public static String getName(ExecutableElement method) {
        return method != null ? method.getSimpleName().toString() : null;
    }

    /**
     * Get a java-style signature for the specified element (return type follows
     * args) eg:
     * 
     * <pre>(int,int)boolean</pre>
     * 
     * @param element element to generate java signature for
     * @return java signature
     */
    public static String getJavaSignature(Element element) {
        if (element instanceof ExecutableElement) {
            ExecutableElement method = (ExecutableElement)element;
            StringBuilder desc = new StringBuilder().append("(");
            boolean extra = false;
            for (VariableElement arg : method.getParameters()) {
                if (extra) {
                    desc.append(',');
                }
                desc.append(TypeUtils.getTypeName(arg.asType()));
                extra = true;
            }
            desc.append(')').append(TypeUtils.getTypeName(method.getReturnType()));
            return desc.toString();
        }
        return TypeUtils.getTypeName(element.asType());
    }
    
    /**
     * Get a java-style signature from the specified bytecode descriptor
     * 
     * @param descriptor descriptor to convert to java signature
     * @return java signature
     */
    public static String getJavaSignature(String descriptor) {
        return new SignaturePrinter("", descriptor).setFullyQualified(true).toDescriptor();
    }

    /**
     * Get the simple type name for the specified type
     * 
     * @param type type mirror
     * @return type name
     */
    public static String getSimpleName(TypeMirror type) {
        String name = TypeUtils.getTypeName(type);
        int pos = name.lastIndexOf('.');
        return pos > 0 ? name.substring(pos + 1) : name;
    }

    /**
     * Get the type name for the specified type
     * 
     * @param type type mirror
     * @return type name
     */
    public static String getTypeName(TypeMirror type) {
        switch (type.getKind()) {
            case ARRAY:    return TypeUtils.getTypeName(((ArrayType)type).getComponentType()) + "[]";
            case DECLARED: return TypeUtils.getTypeName((DeclaredType)type);
            case TYPEVAR:  return TypeUtils.getTypeName(TypeUtils.getUpperBound(type));
            case ERROR:    return TypeUtils.OBJECT_SIG;
            default:       return type.toString();
        }
    }

    /**
     * Get the type name for the specified type
     * 
     * @param type type mirror
     * @return type name
     */
    public static String getTypeName(DeclaredType type) {
        if (type == null) {
            return TypeUtils.OBJECT_SIG;
        }
        return TypeUtils.getInternalName((TypeElement)type.asElement()).replace('/', '.');
    }

    /**
     * Get a bytecode-style descriptor for the specified element 
     * 
     * @param element element to generate descriptor for
     * @return descriptor
     */
    public static String getDescriptor(Element element) {
        if (element instanceof ExecutableElement) {
            return TypeUtils.getDescriptor((ExecutableElement)element);
        } else if (element instanceof VariableElement) {
            return TypeUtils.getInternalName((VariableElement)element);
        }
        
        return TypeUtils.getInternalName(element.asType());
    }

    /**
     * Get a bytecode-style descriptor for the specified method 
     * 
     * @param method method to generate descriptor for
     * @return descriptor
     */
    public static String getDescriptor(ExecutableElement method) {
        if (method == null) {
            return null;
        }
        
        StringBuilder signature = new StringBuilder();
        
        for (VariableElement var : method.getParameters()) {
            signature.append(TypeUtils.getInternalName(var));
        }
        
        String returnType = TypeUtils.getInternalName(method.getReturnType());
        return String.format("(%s)%s", signature, returnType);
    }
    
    /**
     * Get a bytecode-style descriptor for the specified field 
     * 
     * @param field field to generate descriptor for
     * @return descriptor
     */
    public static String getInternalName(VariableElement field) {
        if (field == null) {
            return null;
        }
        return TypeUtils.getInternalName(field.asType());
    }
    
    /**
     * Get a bytecode-style descriptor for the specified type 
     * 
     * @param type type to generate descriptor for
     * @return descriptor
     */
    public static String getInternalName(TypeMirror type) {
        switch (type.getKind()) {
            case ARRAY:    return "[" + TypeUtils.getInternalName(((ArrayType)type).getComponentType());
            case DECLARED: return "L" + TypeUtils.getInternalName((DeclaredType)type) + ";";
            case TYPEVAR:  return "L" + TypeUtils.getInternalName(TypeUtils.getUpperBound(type)) + ";";
            case BOOLEAN:  return "Z";
            case BYTE:     return "B";
            case CHAR:     return "C";
            case DOUBLE:   return "D";
            case FLOAT:    return "F";
            case INT:      return "I";
            case LONG:     return "J";
            case SHORT:    return "S";
            case VOID:     return "V";
            // TODO figure out a better way to not crash when we get here
            case ERROR:    return Constants.OBJECT_DESC;
            default:
        }

        throw new IllegalArgumentException("Unable to parse type symbol " + type + " with " + type.getKind() + " to equivalent bytecode type");
    }

    /**
     * Get a bytecode-style name for the specified type
     * 
     * @param type type to get name for
     * @return bytecode-style name
     */
    public static String getInternalName(DeclaredType type) {
        if (type == null) {
            return Constants.OBJECT;
        }
        return TypeUtils.getInternalName((TypeElement)type.asElement());
    }

    /**
     * Get a bytecode-style name for the specified type element
     * 
     * @param element type element to get name for
     * @return bytecode-style name
     */
    public static String getInternalName(TypeElement element) {
        if (element == null) {
            return null;
        }
        StringBuilder reference = new StringBuilder();
        reference.append(element.getSimpleName());
        Element parent = element.getEnclosingElement();
        while (parent != null) {
            if (parent instanceof TypeElement) {
                reference.insert(0, "$").insert(0, parent.getSimpleName());
            } else if (parent instanceof PackageElement) {
                reference.insert(0, "/").insert(0, ((PackageElement)parent).getQualifiedName().toString().replace('.', '/'));
            }
            parent = parent.getEnclosingElement();
        }
        return reference.toString();
    }
    
    private static DeclaredType getUpperBound(TypeMirror type) {
        try {
            return TypeUtils.getUpperBound0(type, TypeUtils.MAX_GENERIC_RECURSION_DEPTH);
        } catch (IllegalStateException ex) {
            throw new IllegalArgumentException("Type symbol \"" + type + "\" is too complex", ex);
        } catch (IllegalArgumentException ex) {
            throw new IllegalArgumentException("Unable to compute upper bound of type symbol " + type, ex);
        }
    }

    private static DeclaredType getUpperBound0(TypeMirror type, int depth) {
        if (depth == 0) {
            throw new IllegalStateException("Generic symbol \"" + type + "\" is too complex, exceeded "
                    + TypeUtils.MAX_GENERIC_RECURSION_DEPTH + " iterations attempting to determine upper bound");
        }
        if (type instanceof DeclaredType) {
            return (DeclaredType)type;
        }
        if (type instanceof TypeVariable) {
            try {
                TypeMirror upper = ((TypeVariable)type).getUpperBound();
                return TypeUtils.getUpperBound0(upper, --depth);
            } catch (IllegalStateException ex) {
                throw ex;
            } catch (IllegalArgumentException ex) {
                throw ex;
            } catch (Exception ex) {
                throw new IllegalArgumentException("Unable to compute upper bound of type symbol " + type);
            }
        }
        return null;
    }
    
    private static String describeGenericBound(TypeMirror type) {
        if (type instanceof TypeVariable) {
            StringBuilder description = new StringBuilder("<");
            TypeVariable typeVar = (TypeVariable)type;
            description.append(typeVar.toString());
            TypeMirror lowerBound = typeVar.getLowerBound();
            if (lowerBound.getKind() != TypeKind.NULL) {
                description.append(" super ").append(lowerBound);
            }
            TypeMirror upperBound = typeVar.getUpperBound();
            if (upperBound.getKind() != TypeKind.NULL) {
                description.append(" extends ").append(upperBound);
            }
            return description.append(">").toString();
        }
        
        return type.toString();
    }

    /**
     * Get whether the target type is assignable to the specified superclass
     * 
     * @param processingEnv processing environment
     * @param targetType target type to check
     * @param superClass superclass type to check
     * @return true if targetType is assignable to superClass
     */
    public static boolean isAssignable(ProcessingEnvironment processingEnv, TypeMirror targetType, TypeMirror superClass) {
        boolean assignable = processingEnv.getTypeUtils().isAssignable(targetType, superClass);
        if (!assignable && targetType instanceof DeclaredType && superClass instanceof DeclaredType) {
            TypeMirror rawTargetType = TypeUtils.toRawType(processingEnv, (DeclaredType)targetType);
            TypeMirror rawSuperType = TypeUtils.toRawType(processingEnv, (DeclaredType)superClass);
            return processingEnv.getTypeUtils().isAssignable(rawTargetType, rawSuperType);
        }
        
        return assignable;
    }
    
    /**
     * Get whether the two supplied type mirrors represent the same type. For 
     * generic types the type arguments must also be equivalent in order to
     * fully satisfy the equivalence condition. If one of the supplied types is
     * a raw type then a relevant result indicated by <tt>EQUIVALENT_BUT_RAW
     * </tt> will be returned and the caller can inspect which argument (t1 or
     * t2) was raw by querying the <tt>rawType</tt> member.
     * 
     * @param processingEnv processing environment
     * @param t1 first type for comparison
     * @param t2 second type for comparison
     * @return true if the supplied types are equivalent
     */
    public static EquivalencyResult isEquivalentType(ProcessingEnvironment processingEnv, TypeMirror t1, TypeMirror t2) {
        if (t1 == null || t2 == null) {
            return EquivalencyResult.notEquivalent("Invalid types supplied: %s, %s", t1, t2);
        }
        
        if (processingEnv.getTypeUtils().isSameType(t1, t2)) {
            return EquivalencyResult.EQUIVALENT;
        }
        
        if (t1 instanceof TypeVariable && t2 instanceof TypeVariable) {
            t1 = TypeUtils.getUpperBound(t1);
            t2 = TypeUtils.getUpperBound(t2);
            if (processingEnv.getTypeUtils().isSameType(t1, t2)) {
                return EquivalencyResult.EQUIVALENT;
            }
        }
        
        if (t1 instanceof DeclaredType && t2 instanceof DeclaredType) {
            DeclaredType dtT1 = (DeclaredType)t1;
            DeclaredType dtT2 = (DeclaredType)t2;
            TypeMirror rawT1 = TypeUtils.toRawType(processingEnv, dtT1);
            TypeMirror rawT2 = TypeUtils.toRawType(processingEnv, dtT2);
            if (!processingEnv.getTypeUtils().isSameType(rawT1, rawT2)) {
                return EquivalencyResult.notEquivalent("Base types %s and %s are not compatible", rawT1, rawT2);
            }
            List<? extends TypeMirror> argsT1 = dtT1.getTypeArguments();
            List<? extends TypeMirror> argsT2 = dtT2.getTypeArguments();
            if (argsT1.size() != argsT2.size()) {
                if (argsT1.size() == 0) {
                    return EquivalencyResult.equivalentButRaw(1);
                }
                if (argsT2.size() == 0) {
                    return EquivalencyResult.equivalentButRaw(2);
                }
                return EquivalencyResult.notEquivalent("Mismatched generic argument counts %s<[%d]> and %s<[%d]>", rawT1, argsT1.size(), rawT2, argsT2.size());
            }

            for (int arg = 0; arg < argsT1.size(); arg++) {
                TypeMirror argT1 = argsT1.get(arg);
                TypeMirror argT2 = argsT2.get(arg);
                if (TypeUtils.isEquivalentType(processingEnv, argT1, argT2).type != Equivalency.EQUIVALENT) {
                    return EquivalencyResult.boundsMismatch("Generic bounds mismatch between %s and %s",
                            TypeUtils.describeGenericBound(argT1), TypeUtils.describeGenericBound(argT2));
                }
            }
            
            return EquivalencyResult.EQUIVALENT;
        }
        
        return EquivalencyResult.notEquivalent("%s and %s do not match", t1, t2);
    }

    private static TypeMirror toRawType(ProcessingEnvironment processingEnv, DeclaredType targetType) {
        if (targetType.getKind() == TypeKind.INTERSECTION) {
            return targetType;
        }
        Name qualifiedName = ((TypeElement)targetType.asElement()).getQualifiedName();
        TypeElement typeElement = processingEnv.getElementUtils().getTypeElement(qualifiedName);
        return typeElement != null ? typeElement.asType() : targetType;
    }
    
    /**
     * Get the ordinal visibility for the specified element
     * 
     * @param element element to inspect
     * @return visibility level or null if element is null
     */
    public static Visibility getVisibility(Element element) {
        if (element == null) {
            return null;
        }
        
        for (Modifier modifier : element.getModifiers()) {
            switch (modifier) {
                case PUBLIC: return Visibility.PUBLIC;
                case PROTECTED: return Visibility.PROTECTED;
                case PRIVATE: return Visibility.PRIVATE;
                default: break;
            }
        }
        
        return Visibility.PACKAGE;
    }
    
}