/*
 * 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.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;

import org.spongepowered.asm.mixin.injection.selectors.ITargetSelectorByName;
import org.spongepowered.asm.obfuscation.mapping.common.MappingMethod;
import org.spongepowered.asm.util.Bytecode;
import org.spongepowered.asm.util.asm.IAnnotationHandle;
import org.spongepowered.tools.obfuscation.mirror.mapping.MappingMethodResolvable;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;

/**
 * A wrapper for TypeElement which gives us a soft-failover mechanism when
 * dealing with classes that are inaccessible via mirror (such as anonymous
 * inner classes).
 */
public class TypeHandle {
    
    /**
     * Internal class name (FQ) 
     */
    private final String name;
    
    /**
     * Enclosing package, used on imaginary elements to perform at least
     * rudimentary validation
     */
    private final PackageElement pkg;
    
    /**
     * Actual type element, this is null for inaccessible classes
     */
    private final TypeElement element;
    
    /**
     * Reference to this handle, for serialisation 
     */
    private TypeReference reference;

    /**
     * Ctor for imaginary elements, require the enclosing package and the FQ
     * name
     * 
     * @param pkg Package
     * @param name FQ class name
     */
    public TypeHandle(PackageElement pkg, String name) {
        this.name = name.replace('.', '/');
        this.pkg = pkg;
        this.element = null;
    }
    
    /**
     * Ctor for real elements
     * 
     * @param element ze element
     */
    public TypeHandle(TypeElement element) {
        this.pkg = TypeUtils.getPackage(element);
        this.name = TypeUtils.getInternalName(element);
        this.element = element;
    }
    
    /**
     * Ctor for real elements, instanced via a type mirror
     * 
     * @param type type
     */
    public TypeHandle(DeclaredType type) {
        this((TypeElement)type.asElement());
    }
    
    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public final String toString() {
        return this.name.replace('/', '.');
    }
    
    /**
     * Returns the fully qualified class name
     */
    public final String getName() {
        return this.name;
    }
    
    /**
     * Returns the simple class name
     */
    public final String getSimpleName() {
        return Bytecode.getSimpleName(this.name);
    }
    
    /**
     * Returns the enclosing package element
     */
    public final PackageElement getPackage() {
        return this.pkg;
    }

    /**
     * Returns the actual element (returns null for imaginary elements)
     */
    public final TypeElement getElement() {
        return this.element;
    }
    
    /**
     * Returns the actual element (returns simulated value for imaginary
     * elements)
     */
    protected TypeElement getTargetElement() {
        return this.element;
    }

    /**
     * Get an annotation handle for the specified annotation on this type
     * 
     * @param annotationClass type of annotation to search for
     * @return new annotation handle, call <tt>exists</tt> on the returned
     *      handle to determine whether the annotation is present
     */
    public IAnnotationHandle getAnnotation(Class<? extends Annotation> annotationClass) {
        return AnnotationHandle.of(this.getTargetElement(), annotationClass);
    }

    /**
     * Returns enclosed elements (methods, fields, etc.)
     */
    protected final List<? extends Element> getEnclosedElements() {
        return TypeHandle.getEnclosedElements(this.getTargetElement());
    }
    
    /**
     * Returns enclosed elements (methods, fields, etc.) of a particular type
     * 
     * @param kind types of element to return
     * @param <T> list element type
     */
    protected <T extends Element> List<T> getEnclosedElements(ElementKind... kind) {
        return TypeHandle.getEnclosedElements(this.getTargetElement(), kind);
    }

    /**
     * Get whether the type handle can return a type mirror for the represented
     * type. This is true only for types returned by mirror and is not available
     * for simulated types or classpath types inaccessible via mirror (eg. anon
     * classes) 
     */
    public boolean hasTypeMirror() {
        return this.getTargetElement() != null;
    }

    /**
     * Returns the enclosed element as a type mirror, or null if this is an
     * imaginary type
     */
    public TypeMirror getTypeMirror() {
        return this.getTargetElement() != null ? this.getTargetElement().asType() : null;
    }
    
    /**
     * Returns the enclosed element's superclass if available, or null if this
     * class does not have a superclass
     */
    public TypeHandle getSuperclass() {
        TypeElement targetElement = this.getTargetElement();
        if (targetElement == null) {
            return null;
        }
        
        TypeMirror superClass = targetElement.getSuperclass();
        if (superClass == null || superClass.getKind() == TypeKind.NONE) {
            return null;
        }
        
        return new TypeHandle((DeclaredType)superClass);
    }
    
    /**
     * Get interfaces directly implemented by this type
     */
    public List<TypeHandle> getInterfaces() {
        if (this.getTargetElement() == null) {
            return Collections.<TypeHandle>emptyList();
        }
        
        Builder<TypeHandle> list = ImmutableList.<TypeHandle>builder();
        for (TypeMirror iface : this.getTargetElement().getInterfaces()) {
            list.add(new TypeHandle((DeclaredType)iface));
        }
        return list.build();
    }
    
    /**
     * Get methods in this type
     */
    public List<MethodHandle> getMethods() {
        List<MethodHandle> methods = new ArrayList<MethodHandle>();
        for (ExecutableElement method : this.<ExecutableElement>getEnclosedElements(ElementKind.METHOD)) {
            MethodHandle handle = new MethodHandle(this, method);
            methods.add(handle);
        }
        return methods;
    }

    /**
     * Get whether the element is probably public
     */
    public boolean isPublic() {
        TypeElement targetElement = this.getTargetElement();
        if (targetElement == null || !targetElement.getModifiers().contains(Modifier.PUBLIC)) {
            return false;
        }
        for (Element e = targetElement.getEnclosingElement(); e != null && e.getKind() != ElementKind.PACKAGE; e = e.getEnclosingElement()) {
            if (!e.getModifiers().contains(Modifier.PUBLIC)) {
                return false;
            }
        }
        return true;
    }
    
    /**
     * Get whether the element is imaginary (inaccessible via mirror or
     * classpath)
     */
    public boolean isImaginary() {
        return this.getTargetElement() == null;
    }
    
    /**
     * Get whether this handle is simulated
     */
    public boolean isSimulated() {
        return false;
    }
    
    /**
     * Get the TypeReference for this type, used for serialisation
     */
    public final TypeReference getReference() {
        if (this.reference == null) {
            this.reference = new TypeReference(this);
        }
        return this.reference;
    }

    /**
     * Return a method as a remapping candidate, usually returns a method owned
     * by this class but for simulated handles we resolve the reference in the
     * class hierarchy of the simulated target (eg. self)
     * 
     * @param name Method name
     * @param desc Method descriptor
     * @return this handle as a mapping method
     */
    public MappingMethod getMappingMethod(String name, String desc) {
        return new MappingMethodResolvable(this, name, desc);
    }

    /**
     * Find a descriptor for the supplied target selector
     * 
     * @param selector Target selector to use as search term
     * @return descriptor or null if no matching member could be located
     */
    public String findDescriptor(ITargetSelectorByName selector) {
        String desc = selector.getDesc();
        if (desc == null) {
            for (ExecutableElement method : this.<ExecutableElement>getEnclosedElements(ElementKind.METHOD)) {
                if (method.getSimpleName().toString().equals(selector.getName())) {
                    desc = TypeUtils.getDescriptor(method);
                    break;
                }
            }
        }
        return desc;
    }

    /**
     * Find a member field in this type which matches the name and declared type
     * of the supplied element
     * 
     * @param element Element to match
     * @return handle to the discovered field if matched or null if no match
     */
    public final FieldHandle findField(VariableElement element) {
        return this.findField(element, true);
    }
    
    /**
     * Find a member field in this type which matches the name and declared type
     * of the supplied element
     * 
     * @param element Element to match
     * @param matchCase True if case-sensitive comparison should be used
     * @return handle to the discovered field if matched or null if no match
     */
    public final FieldHandle findField(VariableElement element, boolean matchCase) {
        return this.findField(element.getSimpleName().toString(), TypeUtils.getTypeName(element.asType()), matchCase);
    }
    
    /**
     * Find a member field in this type which matches the name and declared type
     * specified
     * 
     * @param name Field name to search for
     * @param type Field descriptor (java-style)
     * @return handle to the discovered field if matched or null if no match
     */
    public final FieldHandle findField(String name, String type) {
        return this.findField(name, type, true);
    }
    
    /**
     * Find a member field in this type which matches the name and declared type
     * specified
     * 
     * @param name Field name to search for
     * @param type Field descriptor (java-style)
     * @param matchCase True if case-sensitive comparison should be used
     * @return handle to the discovered field if matched or null if no match
     */
    public FieldHandle findField(String name, String type, boolean matchCase) {
        String rawType = TypeUtils.stripGenerics(type);

        for (VariableElement field : this.<VariableElement>getEnclosedElements(ElementKind.FIELD)) {
            if (TypeHandle.compareElement(field, name, type, matchCase)) {
                return new FieldHandle(this.getTargetElement(), field);
            } else if (TypeHandle.compareElement(field, name, rawType, matchCase)) {
                return new FieldHandle(this.getTargetElement(), field, true);
            }                
        }
        
        return null;
    }
    
    /**
     * Find a member method in this type which matches the name and declared
     * type of the supplied element
     * 
     * @param element Element to match
     * @return handle to the discovered method if matched or null if no match
     */
    public final MethodHandle findMethod(ExecutableElement element) {
        return this.findMethod(element, true);
    }

    /**
     * Find a member method in this type which matches the name and declared
     * type of the supplied element
     * 
     * @param element Element to match
     * @param matchCase True if case-sensitive comparison should be used
     * @return handle to the discovered method if matched or null if no match
     */
    public final MethodHandle findMethod(ExecutableElement element, boolean matchCase) {
        return this.findMethod(element.getSimpleName().toString(), TypeUtils.getJavaSignature(element), matchCase);
    }

    /**
     * Find a member method in this type which matches the name and signature
     * specified
     * 
     * @param name Method name to search for
     * @param signature Method signature
     * @return handle to the discovered method if matched or null if no match
     */
    public final MethodHandle findMethod(String name, String signature) {
        return this.findMethod(name, signature, true);
    }
    
    /**
     * Find a member method in this type which matches the name and signature
     * specified
     * 
     * @param name Method name to search for
     * @param signature Method signature
     * @param matchCase True if case-sensitive comparison should be used
     * @return handle to the discovered method if matched or null if no match
     */
    public MethodHandle findMethod(String name, String signature, boolean matchCase) {
        String rawSignature = TypeUtils.stripGenerics(signature);
        return TypeHandle.findMethod(this, name, signature, rawSignature, matchCase);
    }

    protected static MethodHandle findMethod(TypeHandle target, String name, String signature, String rawSignature, boolean matchCase) {
        for (ExecutableElement method : TypeHandle.<ExecutableElement>getEnclosedElements(target.getTargetElement(),
                ElementKind.CONSTRUCTOR, ElementKind.METHOD)) {
            if (TypeHandle.compareElement(method, name, signature, matchCase) || TypeHandle.compareElement(method, name, rawSignature, matchCase)) {
                return new MethodHandle(target, method);
            }
        }
        return null;
    }
    
    protected static boolean compareElement(Element elem, String name, String type, boolean matchCase) {
        try {
            String elementName = elem.getSimpleName().toString();
            String elementType = TypeUtils.getJavaSignature(elem);
            String rawElementType = TypeUtils.stripGenerics(elementType);
            boolean compared = matchCase ? name.equals(elementName) : name.equalsIgnoreCase(elementName);
            return compared && (type.length() == 0 || type.equals(elementType) || type.equals(rawElementType));
        } catch (NullPointerException ex) {
            return false;
        }
    }

    @SuppressWarnings("unchecked")
    protected static <T extends Element> List<T> getEnclosedElements(TypeElement targetElement, ElementKind... kind) {
        if (kind == null || kind.length < 1) {
            return (List<T>)TypeHandle.getEnclosedElements(targetElement);
        }
        
        if (targetElement == null) {
            return Collections.<T>emptyList();
        }
        
        Builder<T> list = ImmutableList.<T>builder();
        for (Element elem : targetElement.getEnclosedElements()) {
            for (ElementKind ek : kind) {
                if (elem.getKind() == ek) {
                    list.add((T)elem);
                    break;
                }
            }
        }

        return list.build();
    }

    protected static List<? extends Element> getEnclosedElements(TypeElement targetElement) {
        return targetElement != null ? targetElement.getEnclosedElements() : Collections.<Element>emptyList();
    }

}
