/*
 * 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.asm.util;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.MethodNode;
import org.spongepowered.asm.mixin.injection.selectors.ITargetSelectorByName;

import com.google.common.base.Strings;

/**
 * Generates callback signature for callback pretty-print
 */
public class SignaturePrinter {

    /**
     * Method name 
     */
    private final String name;
    
    /**
     * The return type of the <b>target</b> method (used to fill in the generic
     * bound on a CallbackInfoReturnable. 
     */
    private final Type returnType;
    
    /**
     * Argument type array (sparse, with no TOP entries) 
     */
    private final Type[] argTypes;
    
    /**
     * Argument names 
     */
    private final String[] argNames;
    
    /**
     * Method prefix
     */
    private String modifiers = "private void";
    
    /**
     * Fully qualify class names 
     */
    private boolean fullyQualified;
    
    public SignaturePrinter(MethodNode method) {
        this(method.name, Type.VOID_TYPE, Type.getArgumentTypes(method.desc));
        this.setModifiers(method);
    }
    
    public SignaturePrinter(MethodNode method, String[] argNames) {
        this(method.name, Type.VOID_TYPE, Type.getArgumentTypes(method.desc), argNames);
        this.setModifiers(method);
    }
    
    public SignaturePrinter(ITargetSelectorByName member) {
        this(member.getName(), member.getDesc());
    }

    public SignaturePrinter(String name, String desc) {
        this(name, Type.getReturnType(desc), Type.getArgumentTypes(desc));
    }
    
    public SignaturePrinter(Type[] args) {
        this(null, null, args);
    }

    public SignaturePrinter(Type returnType, Type[] args) {
        this(null, returnType, args);
    }
    
    public SignaturePrinter(String name, Type returnType, Type[] args) {
        this.name = name;
        this.returnType = returnType;
        this.argTypes = new Type[args.length];
        this.argNames = new String[args.length];
        for (int l = 0, v = 0; l < args.length; l++) {
            if (args[l] != null) {
                this.argTypes[l] = args[l];
                this.argNames[l] = "var" + v++; 
            }
        }
    }

    public SignaturePrinter(String name, Type returnType, LocalVariableNode[] args) {
        this.name = name;
        this.returnType = returnType;
        this.argTypes = new Type[args.length];
        this.argNames = new String[args.length];
        for (int l = 0; l < args.length; l++) {
            if (args[l] != null) {
                this.argTypes[l] = Type.getType(args[l].desc);
                this.argNames[l] = args[l].name;
            }
        }
    }
    
    public SignaturePrinter(String name, Type returnType, Type[] argTypes, String[] argNames) {
        this.name = name;
        this.returnType = returnType;
        this.argTypes = argTypes;
        this.argNames = argNames;
    }
    
    /**
     * Return only the arguments portion of this signature as a Java-style block
     */
    public String getFormattedArgs() {
        return this.appendArgs(new StringBuilder(), true, true).toString();
    }
    
    /**
     * Get string representation of this signature's return type
     */
    public String getReturnType() {
        return SignaturePrinter.getTypeName(this.returnType, false, this.fullyQualified);
    }

    /**
     * Set modifiers on this signature using the supplied method node
     * 
     * @param method method node to read modifiers from
     */
    public void setModifiers(MethodNode method) {
        String returnType = SignaturePrinter.getTypeName(Type.getReturnType(method.desc), false, this.fullyQualified);
        String staticType = (method.access & Opcodes.ACC_STATIC) != 0 ? "static " : "";
        if ((method.access & Opcodes.ACC_PUBLIC) != 0) {
            this.setModifiers("public " + staticType + returnType);
        } else if ((method.access & Opcodes.ACC_PROTECTED) != 0) {
            this.setModifiers("protected " + staticType + returnType);
        } else if ((method.access & Opcodes.ACC_PRIVATE) != 0) {
            this.setModifiers("private " + staticType + returnType);
        } else {
            this.setModifiers(staticType + returnType);
        }
    }
    
    /**
     * Set modifiers on this signature explicitly. Use the special token
     * <tt>${returnType}</tt> to insert the return type into the modifier
     * string.
     * 
     * @param modifiers modifiers to prepend
     * @return fluent interface
     */
    public SignaturePrinter setModifiers(String modifiers) {
        this.modifiers = modifiers.replace("${returnType}", this.getReturnType());
        return this;
    }
    
    /**
     * Set whether this signature generates fully-qualified class output, mainly
     * used when generating signatures for Mirror
     * 
     * @param fullyQualified new value for fully-qualified
     * @return fluent interface
     */
    public SignaturePrinter setFullyQualified(boolean fullyQualified) {
        this.fullyQualified = fullyQualified;
        return this;
    }
    
    /**
     * Get whether this printer will fully-qualify class names in generated
     * signatures
     */
    public boolean isFullyQualified() {
        return this.fullyQualified;
    }
    
    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        String name = this.name != null ? this.name : "method";
        return this.appendArgs(new StringBuilder().append(this.modifiers).append(" ").append(name), false, true).toString();
    }
    
    /**
     * Return this signature in descriptor format (return type after args)
     */
    public String toDescriptor() {
        StringBuilder args = this.appendArgs(new StringBuilder(), true, false);
        return args.append(SignaturePrinter.getTypeName(this.returnType, false, this.fullyQualified)).toString();
    }
    
    private StringBuilder appendArgs(StringBuilder sb, boolean typesOnly, boolean pretty) {
        sb.append('(');
        for (int var = 0; var < this.argTypes.length; var++) {
            if (this.argTypes[var] == null) {
                continue;
            } else if (var > 0) {
                sb.append(',');
                if (pretty) {
                    sb.append(' ');
                }
            }
            try {
                String name = typesOnly ? null : var < this.argNames.length && !Strings.isNullOrEmpty(this.argNames[var])
                        ? this.argNames[var] : "unnamed" + var;
                this.appendType(sb, this.argTypes[var], name);
            } catch (Exception ex) {
//                System.err.printf("\n\n>>> argTypes=%d, argNames=%d\n\n", this.argTypes.length, this.argNames.length);
                throw new RuntimeException(ex);
            }
        }
        return sb.append(")");
    }
    
    private StringBuilder appendType(StringBuilder sb, Type type, String name) {
        switch (type.getSort()) {
            case Type.ARRAY:
                return SignaturePrinter.appendArraySuffix(this.appendType(sb, SignaturePrinter.getElementType(type), name), type);
            case Type.OBJECT:
                return this.appendType(sb, SignaturePrinter.getClassName(type), name);
            default:
                sb.append(SignaturePrinter.getTypeName(type, false, this.fullyQualified));
                if (name != null) {
                    sb.append(' ').append(name);
                }
                return sb;
        }
    }

    private StringBuilder appendType(StringBuilder sb, String typeName, String name) {
        if (!this.fullyQualified) {
            typeName = typeName.substring(typeName.lastIndexOf('.') + 1);
        }
        sb.append(typeName);
        if (typeName.endsWith("CallbackInfoReturnable")) {
            sb.append('<').append(SignaturePrinter.getTypeName(this.returnType, true, this.fullyQualified)).append('>');
        }
        if (name != null) {
            sb.append(' ').append(name);
        }
        return sb;
    }

    /**
     * Get the source code name for the specified type
     * 
     * @param type Type to generate a friendly name for
     * @return String representation of the specified type, eg "int" for an
     *         integer primitive or "String" for java.lang.String
     */
    public static String getTypeName(Type type) {
        return SignaturePrinter.getTypeName(type, false, true);
    }

    /**
     * Get the source code name for the specified type
     * 
     * @param type Type to generate a friendly name for
     * @param box True to return the equivalent boxing type for primitives 
     * @return String representation of the specified type, eg "int" for an
     *         integer primitive or "String" for java.lang.String
     */
    public static String getTypeName(Type type, boolean box) {
        return SignaturePrinter.getTypeName(type, box, false);
    }

    /**
     * Get the source code name for the specified type
     * 
     * @param type Type to generate a friendly name for
     * @param box True to return the equivalent boxing type for primitives
     * @param fullyQualified fully-qualify class names 
     * @return String representation of the specified type, eg "int" for an
     *         integer primitive or "String" for java.lang.String
     */
    public static String getTypeName(Type type, boolean box, boolean fullyQualified) {
        if (type == null) {
            return "{null?}";
        }
        switch (type.getSort()) {
            case Type.VOID:    return box ? "Void"      : "void";
            case Type.BOOLEAN: return box ? "Boolean"   : "boolean";
            case Type.CHAR:    return box ? "Character" : "char";
            case Type.BYTE:    return box ? "Byte"      : "byte";
            case Type.SHORT:   return box ? "Short"     : "short";
            case Type.INT:     return box ? "Integer"   : "int";
            case Type.FLOAT:   return box ? "Float"     : "float";
            case Type.LONG:    return box ? "Long"      : "long";
            case Type.DOUBLE:  return box ? "Double"    : "double";
            case Type.ARRAY:  
                return SignaturePrinter.getTypeName(SignaturePrinter.getElementType(type), box, fullyQualified) + SignaturePrinter.arraySuffix(type);
            case Type.OBJECT:
                String typeName = SignaturePrinter.getClassName(type);
                if (!fullyQualified) {
                    typeName = typeName.substring(typeName.lastIndexOf('.') + 1);
                }
                return typeName;
            default:
                return "Object";
        }
    }
    
    private static Type getElementType(Type type) {
        try {
            return type.getElementType();
        } catch (Exception ex) {
            return Type.getObjectType("InvalidType");
        }
    }

    private static String getClassName(Type type) {
        try {
            return type.getClassName();
        } catch (Exception ex) {
            return "InvalidType";
        }
    }
    
    private static String arraySuffix(Type type) {
        return Strings.repeat("[]", type.getDimensions());
    }
    
    private static StringBuilder appendArraySuffix(StringBuilder sb, Type type) {
        for (int i = 0; i < type.getDimensions(); i++) {
            sb.append("[]");
        }
        return sb;
    }
}
