/*
 * Javassist, a Java-bytecode translator toolkit.
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License.  Alternatively, the contents of this file may be used under
 * the terms of the GNU Lesser General Public License Version 2.1 or later,
 * or the Apache License Version 2.0.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 */

package javassist.bytecode;

import java.util.Map;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtPrimitiveType;
import javassist.NotFoundException;

/**
 * A support class for dealing with descriptors.
 *
 * <p>See chapter 4.3 in "The Java Virtual Machine Specification (2nd ed.)"
 */
public class Descriptor {
    /**
     * Converts a class name into the internal representation used in
     * the JVM.
     *
     * <p>Note that <code>toJvmName(toJvmName(s))</code> is equivalent
     * to <code>toJvmName(s)</code>.
     */
    public static String toJvmName(String classname) {
        return classname.replace('.', '/');
    }

    /**
     * Converts a class name from the internal representation used in
     * the JVM to the normal one used in Java.
     * This method does not deal with an array type name such as
     * "[Ljava/lang/Object;" and "[I;".  For such names, use
     * <code>toClassName()</code>.
     *
     * @see #toClassName(String)
     */
    public static String toJavaName(String classname) {
        return classname.replace('/', '.');
    }

    /**
     * Returns the internal representation of the class name in the
     * JVM.
     */
    public static String toJvmName(CtClass clazz) {
        if (clazz.isArray())
            return of(clazz);
        return toJvmName(clazz.getName());
    }

    /**
     * Converts to a Java class name from a descriptor.
     *
     * @param descriptor        type descriptor.
     */
    public static String toClassName(String descriptor) {
        int arrayDim = 0;
        int i = 0;
        char c = descriptor.charAt(0);
        while (c == '[') {
            ++arrayDim;
            c = descriptor.charAt(++i);
        }

        String name;
        if (c == 'L') {
            int i2 = descriptor.indexOf(';', i++);
            name = descriptor.substring(i, i2).replace('/', '.');
            i = i2;
        }
        else if (c == 'V')
            name =  "void";
        else if (c == 'I')
            name = "int";
        else if (c == 'B')
            name = "byte";
        else if (c == 'J')
            name = "long";
        else if (c == 'D')
            name = "double";
        else if (c == 'F')
            name = "float";
        else if (c == 'C')
            name = "char";
        else if (c == 'S')
            name = "short";
        else if (c == 'Z')
            name = "boolean";
        else
            throw new RuntimeException("bad descriptor: " + descriptor);

        if (i + 1 != descriptor.length())
            throw new RuntimeException("multiple descriptors?: " + descriptor);

        if (arrayDim == 0)
            return name;
        StringBuilder sbuf = new StringBuilder(name);
        do {
            sbuf.append("[]");
        } while (--arrayDim > 0);

        return sbuf.toString();
    }

    /**
     * Converts to a descriptor from a Java class name
     */
    public static String of(String classname) {
        if (classname.equals("void"))
            return "V";
        else if (classname.equals("int"))
            return "I";
        else if (classname.equals("byte"))
            return "B";
        else if (classname.equals("long"))
            return "J";
        else if (classname.equals("double"))
            return "D";
        else if (classname.equals("float"))
            return "F";
        else if (classname.equals("char"))
            return "C";
        else if (classname.equals("short"))
            return "S";
        else if (classname.equals("boolean"))
            return "Z";
        else
            return "L" + toJvmName(classname) + ";";
    }

    /**
     * Substitutes a class name
     * in the given descriptor string.
     *
     * @param desc    descriptor string
     * @param oldname replaced JVM class name
     * @param newname substituted JVM class name
     *
     * @see Descriptor#toJvmName(String)
     */
    public static String rename(String desc, String oldname, String newname) {
        if (desc.indexOf(oldname) < 0)
            return desc;

        StringBuilder newdesc = new StringBuilder();
        int head = 0;
        int i = 0;
        for (;;) {
            int j = desc.indexOf('L', i);
            if (j < 0)
                break;
            else if (desc.startsWith(oldname, j + 1)
                     && desc.charAt(j + oldname.length() + 1) == ';') {
                newdesc.append(desc.substring(head, j));
                newdesc.append('L');
                newdesc.append(newname);
                newdesc.append(';');
                head = i = j + oldname.length() + 2;
            }
            else {
                i = desc.indexOf(';', j) + 1;
                if (i < 1)
                    break; // ';' was not found.
            }
        }

        if (head == 0)
            return desc;
        int len = desc.length();
        if (head < len)
            newdesc.append(desc.substring(head, len));

        return newdesc.toString();
    }

    /**
     * Substitutes class names in the given descriptor string
     * according to the given <code>map</code>.
     *
     * @param map a map between replaced and substituted
     *            JVM class names.
     * @see Descriptor#toJvmName(String)
     */
    public static String rename(String desc, Map<String,String> map) {
        if (map == null)
            return desc;

        StringBuilder newdesc = new StringBuilder();
        int head = 0;
        int i = 0;
        for (;;) {
            int j = desc.indexOf('L', i);
            if (j < 0)
                break;

            int k = desc.indexOf(';', j);
            if (k < 0)
                break;

            i = k + 1;
            String name = desc.substring(j + 1, k);
            String name2 = map.get(name);
            if (name2 != null) {
                newdesc.append(desc.substring(head, j));
                newdesc.append('L');
                newdesc.append(name2);
                newdesc.append(';');
                head = i;
            }
        }

        if (head == 0)
            return desc;
        int len = desc.length();
        if (head < len)
            newdesc.append(desc.substring(head, len));

        return newdesc.toString();
    }

    /**
     * Returns the descriptor representing the given type.
     */
    public static String of(CtClass type) {
        StringBuilder sbuf = new StringBuilder();
        toDescriptor(sbuf, type);
        return sbuf.toString();
    }

    private static void toDescriptor(StringBuilder desc, CtClass type) {
        if (type.isArray()) {
            desc.append('[');
            try {
                toDescriptor(desc, type.getComponentType());
            }
            catch (NotFoundException e) {
                desc.append('L');
                String name = type.getName();
                desc.append(toJvmName(name.substring(0, name.length() - 2)));
                desc.append(';');
            }
        }
        else if (type.isPrimitive()) {
            CtPrimitiveType pt = (CtPrimitiveType)type;
            desc.append(pt.getDescriptor());
        }
        else { // class type
            desc.append('L');
            desc.append(type.getName().replace('.', '/'));
            desc.append(';');
        }
    }

    /**
     * Returns the descriptor representing a constructor receiving
     * the given parameter types.
     *
     * @param paramTypes parameter types
     */
    public static String ofConstructor(CtClass[] paramTypes) {
        return ofMethod(CtClass.voidType, paramTypes);
    }

    /**
     * Returns the descriptor representing a method that receives
     * the given parameter types and returns the given type.
     *
     * @param returnType return type
     * @param paramTypes parameter types
     */
    public static String ofMethod(CtClass returnType, CtClass[] paramTypes) {
        StringBuilder desc = new StringBuilder();
        desc.append('(');
        if (paramTypes != null) {
            int n = paramTypes.length;
            for (int i = 0; i < n; ++i)
                toDescriptor(desc, paramTypes[i]);
        }

        desc.append(')');
        if (returnType != null)
            toDescriptor(desc, returnType);

        return desc.toString();
    }

    /**
     * Returns the descriptor representing a list of parameter types.
     * For example, if the given parameter types are two <code>int</code>,
     * then this method returns <code>"(II)"</code>.
     *
     * @param paramTypes parameter types
     */
    public static String ofParameters(CtClass[] paramTypes) {
        return ofMethod(null, paramTypes);
    }

    /**
     * Appends a parameter type to the parameter list represented
     * by the given descriptor.
     *
     * <p><code>classname</code> must not be an array type.
     *
     * @param classname parameter type (not primitive type)
     * @param desc      descriptor
     */
    public static String appendParameter(String classname, String desc) {
        int i = desc.indexOf(')');
        if (i < 0)
            return desc;
        StringBuilder newdesc = new StringBuilder();
        newdesc.append(desc.substring(0, i));
        newdesc.append('L');
        newdesc.append(classname.replace('.', '/'));
        newdesc.append(';');
        newdesc.append(desc.substring(i));
        return newdesc.toString();
    }

    /**
     * Inserts a parameter type at the beginning of the parameter
     * list represented
     * by the given descriptor.
     *
     * <p><code>classname</code> must not be an array type.
     *
     * @param classname parameter type (not primitive type)
     * @param desc      descriptor
     */
    public static String insertParameter(String classname, String desc) {
        if (desc.charAt(0) != '(')
            return desc;
        return "(L" + classname.replace('.', '/') + ';'
               + desc.substring(1);
    }

    /**
     * Appends a parameter type to the parameter list represented
     * by the given descriptor.  The appended parameter becomes
     * the last parameter.
     *
     * @param type      the type of the appended parameter.
     * @param descriptor      the original descriptor.
     */
    public static String appendParameter(CtClass type, String descriptor) {
        int i = descriptor.indexOf(')');
        if (i < 0)
            return descriptor;
        StringBuilder newdesc = new StringBuilder();
        newdesc.append(descriptor.substring(0, i));
        toDescriptor(newdesc, type);
        newdesc.append(descriptor.substring(i));
        return newdesc.toString();
    }

    /**
     * Inserts a parameter type at the beginning of the parameter
     * list represented
     * by the given descriptor.
     *
     * @param type              the type of the inserted parameter.
     * @param descriptor        the descriptor of the method.
     */
    public static String insertParameter(CtClass type,
                                         String descriptor) {
        if (descriptor.charAt(0) != '(')
            return descriptor;
        return "(" + of(type) + descriptor.substring(1);
    }

    /**
     * Changes the return type included in the given descriptor.
     *
     * <p><code>classname</code> must not be an array type.
     *
     * @param classname return type
     * @param desc      descriptor
     */
    public static String changeReturnType(String classname, String desc) {
        int i = desc.indexOf(')');
        if (i < 0)
            return desc;
        StringBuilder newdesc = new StringBuilder();
        newdesc.append(desc.substring(0, i + 1));
        newdesc.append('L');
        newdesc.append(classname.replace('.', '/'));
        newdesc.append(';');
        return newdesc.toString();
    }

    /**
     * Returns the <code>CtClass</code> objects representing the parameter
     * types specified by the given descriptor.
     *
     * @param desc descriptor
     * @param cp   the class pool used for obtaining
     *             a <code>CtClass</code> object.
     */
    public static CtClass[] getParameterTypes(String desc, ClassPool cp)
        throws NotFoundException
    {
        if (desc.charAt(0) != '(')
            return null;
        int num = numOfParameters(desc);
        CtClass[] args = new CtClass[num];
        int n = 0;
        int i = 1;
        do {
            i = toCtClass(cp, desc, i, args, n++);
        } while (i > 0);
        return args;
    }

    /**
     * Returns true if the list of the parameter types of desc1 is equal to
     * that of desc2.
     * For example, "(II)V" and "(II)I" are equal.
     */
    public static boolean eqParamTypes(String desc1, String desc2) {
        if (desc1.charAt(0) != '(')
            return false;

        for (int i = 0; true; ++i) {
            char c = desc1.charAt(i);
            if (c != desc2.charAt(i))
                return false;

            if (c == ')')
                return true;
        }
    }

    /**
     * Returns the signature of the given descriptor.  The signature does
     * not include the return type.  For example, the signature of "(I)V"
     * is "(I)".
     */
    public static String getParamDescriptor(String decl) {
        return decl.substring(0, decl.indexOf(')') + 1);
    }

    /**
     * Returns the <code>CtClass</code> object representing the return
     * type specified by the given descriptor.
     *
     * @param desc descriptor
     * @param cp   the class pool used for obtaining
     *             a <code>CtClass</code> object.
     */
    public static CtClass getReturnType(String desc, ClassPool cp)
        throws NotFoundException
    {
        int i = desc.indexOf(')');
        if (i < 0)
            return null;
        CtClass[] type = new CtClass[1];
        toCtClass(cp, desc, i + 1, type, 0);
        return type[0];
    }

    /**
     * Returns the number of the prameters included in the given
     * descriptor.
     *
     * @param desc descriptor
     */
    public static int numOfParameters(String desc) {
        int n = 0;
        int i = 1;
        for (;;) {
            char c = desc.charAt(i);
            if (c == ')')
                break;

            while (c == '[')
                c = desc.charAt(++i);

            if (c == 'L') {
                i = desc.indexOf(';', i) + 1;
                if (i <= 0)
                    throw new IndexOutOfBoundsException("bad descriptor");
            }
            else
                ++i;

            ++n;
        }

        return n;
    }

    /**
     * Returns a <code>CtClass</code> object representing the type
     * specified by the given descriptor.
     *
     * <p>This method works even if the package-class separator is
     * not <code>/</code> but <code>.</code> (period).  For example,
     * it accepts <code>Ljava.lang.Object;</code>
     * as well as <code>Ljava/lang/Object;</code>.
     *
     * @param desc descriptor.
     * @param cp   the class pool used for obtaining
     *             a <code>CtClass</code> object.
     */
    public static CtClass toCtClass(String desc, ClassPool cp)
        throws NotFoundException
    {
        CtClass[] clazz = new CtClass[1];
        int res = toCtClass(cp, desc, 0, clazz, 0);
        if (res >= 0)
            return clazz[0];
        // maybe, you forgot to surround the class name with
        // L and ;.  It violates the protocol, but I'm tolerant...
        return cp.get(desc.replace('/', '.'));
    }

    private static int toCtClass(ClassPool cp, String desc, int i,
                                 CtClass[] args, int n)
        throws NotFoundException
    {
        int i2;
        String name;

        int arrayDim = 0;
        char c = desc.charAt(i);
        while (c == '[') {
            ++arrayDim;
            c = desc.charAt(++i);
        }

        if (c == 'L') {
            i2 = desc.indexOf(';', ++i);
            name = desc.substring(i, i2++).replace('/', '.');
        }
        else {
            CtClass type = toPrimitiveClass(c);
            if (type == null)
                return -1; // error

            i2 = i + 1;
            if (arrayDim == 0) {
                args[n] = type;
                return i2; // neither an array type or a class type
            }
            name = type.getName();
        }

        if (arrayDim > 0) {
            StringBuilder sbuf = new StringBuilder(name);
            while (arrayDim-- > 0)
                sbuf.append("[]");

            name = sbuf.toString();
        }

        args[n] = cp.get(name);
        return i2;
    }

    static CtClass toPrimitiveClass(char c) {
        CtClass type = null;
        switch (c) {
        case 'Z' :
            type = CtClass.booleanType;
            break;
        case 'C' :
            type = CtClass.charType;
            break;
        case 'B' :
            type = CtClass.byteType;
            break;
        case 'S' :
            type = CtClass.shortType;
            break;
        case 'I' :
            type = CtClass.intType;
            break;
        case 'J' :
            type = CtClass.longType;
            break;
        case 'F' :
            type = CtClass.floatType;
            break;
        case 'D' :
            type = CtClass.doubleType;
            break;
        case 'V' :
            type = CtClass.voidType;
            break;
        }

        return type;
    }

    /**
     * Computes the dimension of the array represented by the given
     * descriptor.  For example, if the descriptor is <code>"[[I"</code>,
     * then this method returns 2.
     *
     * @param desc the descriptor.
     * @return 0        if the descriptor does not represent an array type.
     */
    public static int arrayDimension(String desc) {
        int dim = 0;
        while (desc.charAt(dim) == '[')
            ++dim;

        return dim;
    }

    /**
     * Returns the descriptor of the type of the array component.
     * For example, if the given descriptor is
     * <code>"[[Ljava/lang/String;"</code> and the given dimension is 2,
     * then this method returns <code>"Ljava/lang/String;"</code>.
     *
     * @param desc the descriptor.
     * @param dim  the array dimension.
     */
    public static String toArrayComponent(String desc, int dim) {
        return desc.substring(dim);
    }

    /**
     * Computes the data size specified by the given descriptor.
     * For example, if the descriptor is "D", this method returns 2.
     *
     * <p>If the descriptor represents a method type, this method returns
     * (the size of the returned value) - (the sum of the data sizes
     * of all the parameters).  For example, if the descriptor is
     * <code>"(I)D"</code>, then this method returns 1 (= 2 - 1).
     *
     * @param desc descriptor
     */
    public static int dataSize(String desc) {
        return dataSize(desc, true);
    }

    /**
     * Computes the data size of parameters.
     * If one of the parameters is double type, the size of that parameter
     * is 2 words.  For example, if the given descriptor is
     *  <code>"(IJ)D"</code>, then this method returns 3.  The size of the
     * return type is not computed.
     * 
     * @param desc      a method descriptor.
     */
    public static int paramSize(String desc) {
        return -dataSize(desc, false);
    }

    private static int dataSize(String desc, boolean withRet) {
        int n = 0;
        char c = desc.charAt(0);
        if (c == '(') {
            int i = 1;
            for (;;) {
                c = desc.charAt(i);
                if (c == ')') {
                    c = desc.charAt(i + 1);
                    break;
                }

                boolean array = false;
                while (c == '[') {
                    array = true;
                    c = desc.charAt(++i);
                }

                if (c == 'L') {
                    i = desc.indexOf(';', i) + 1;
                    if (i <= 0)
                        throw new IndexOutOfBoundsException("bad descriptor");
                }
                else
                    ++i;

                if (!array && (c == 'J' || c == 'D'))
                    n -= 2;
                else
                    --n;
            }
        }

        if (withRet)
            if (c == 'J' || c == 'D')
                n += 2;
            else if (c != 'V')
                ++n;

        return n;
    }

    /**
     * Returns a human-readable representation of the
     * given descriptor.  For example, <code>Ljava/lang/Object;</code>
     * is converted into <code>java.lang.Object</code>.
     * <code>(I[I)V</code> is converted into <code>(int, int[])</code>
     * (the return type is ignored). 
     */
    public static String toString(String desc) {
        return PrettyPrinter.toString(desc);
    }

    static class PrettyPrinter {
        static String toString(String desc) {
            StringBuilder sbuf = new StringBuilder();
            if (desc.charAt(0) == '(') {
                int pos = 1;
                sbuf.append('(');
                while (desc.charAt(pos) != ')') {
                    if (pos > 1)
                        sbuf.append(',');

                    pos = readType(sbuf, pos, desc);
                }

                sbuf.append(')');
            }
            else
                readType(sbuf, 0, desc);

            return sbuf.toString();
        }

        static int readType(StringBuilder sbuf, int pos, String desc) {
            char c = desc.charAt(pos);
            int arrayDim = 0;
            while (c == '[') {
                arrayDim++;
                c = desc.charAt(++pos);
            }

            if (c == 'L')
                while (true) {
                    c = desc.charAt(++pos);
                    if (c == ';')
                        break;

                    if (c == '/')
                        c = '.';

                    sbuf.append(c);
                }
            else {
                CtClass t = toPrimitiveClass(c);
                sbuf.append(t.getName());
            }

            while (arrayDim-- > 0)
                sbuf.append("[]");

            return pos + 1;
        }
    }

    /**
     * An Iterator over a descriptor.
     */
    public static class Iterator {
        private String desc;
        private int index, curPos;
        private boolean param;

        /**
         * Constructs an iterator.
         *
         * @param s         descriptor.
         */
        public Iterator(String s) {
            desc = s;
            index = curPos = 0;
            param = false;
        }

        /**
         * Returns true if the iteration has more elements.
         */
        public boolean hasNext() {
            return index < desc.length();
        }

        /**
         * Returns true if the current element is a parameter type.
         */
        public boolean isParameter() { return param; }

        /**
         * Returns the first character of the current element.
         */
        public char currentChar() { return desc.charAt(curPos); }

        /**
         * Returns true if the current element is double or long type.
         */
        public boolean is2byte() {
            char c = currentChar();
            return c == 'D' || c == 'J';
        }

        /**
         * Returns the position of the next type character.
         * That type character becomes a new current element.
         */
        public int next() {
            int nextPos = index;
            char c = desc.charAt(nextPos);
            if (c == '(') {
                ++index;
                c = desc.charAt(++nextPos);
                param = true;
            }

            if (c == ')') {
                ++index;
                c = desc.charAt(++nextPos);
                param = false;
            }

            while (c == '[')
                c = desc.charAt(++nextPos);

            if (c == 'L') {
                nextPos = desc.indexOf(';', nextPos) + 1;
                if (nextPos <= 0)
                    throw new IndexOutOfBoundsException("bad descriptor");
            }
            else
                ++nextPos;

            curPos = index;
            index = nextPos;
            return curPos;
        }
    }
}
