/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed 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 com.android.dexgen.rop.type;

import java.util.HashMap;

/**
 * Representation of a method decriptor. Instances of this class are
 * generally interned and may be usefully compared with each other
 * using {@code ==}.
 */
public final class Prototype implements Comparable<Prototype> {
    /** {@code non-null;} intern table mapping string descriptors to instances */
    private static final HashMap<String, Prototype> internTable =
        new HashMap<String, Prototype>(500);

    /** {@code non-null;} method descriptor */
    private final String descriptor;

    /** {@code non-null;} return type */
    private final Type returnType;

    /** {@code non-null;} list of parameter types */
    private final StdTypeList parameterTypes;

    /** {@code null-ok;} list of parameter frame types, if calculated */
    private StdTypeList parameterFrameTypes;

    /**
     * Returns the unique instance corresponding to the
     * given method descriptor. See vmspec-2 sec4.3.3 for details on the
     * field descriptor syntax.
     *
     * @param descriptor {@code non-null;} the descriptor
     * @return {@code non-null;} the corresponding instance
     * @throws IllegalArgumentException thrown if the descriptor has
     * invalid syntax
     */
    public static Prototype intern(String descriptor) {
        if (descriptor == null) {
            throw new NullPointerException("descriptor == null");
        }     
        Prototype result = internTable.get(descriptor);
        if (result != null) {
            return result;
        }

        Type[] params = makeParameterArray(descriptor);
        int paramCount = 0;
        int at = 1;

        for (;;) {
            int startAt = at;
            char c = descriptor.charAt(at);
            if (c == ')') {
                at++;
                break;
            }

            // Skip array markers.
            while (c == '[') {
                at++;
                c = descriptor.charAt(at);
            }

            if (c == 'L') {
                // It looks like the start of a class name; find the end.
                int endAt = descriptor.indexOf(';', at);
                if (endAt == -1) {
                    throw new IllegalArgumentException("bad descriptor");
                }
                at = endAt + 1;
            } else {
                at++;
            }

            params[paramCount] =
                Type.intern(descriptor.substring(startAt, at));
            paramCount++;
        }

        Type returnType = Type.internReturnType(descriptor.substring(at));
        StdTypeList parameterTypes = new StdTypeList(paramCount);

        for (int i = 0; i < paramCount; i++) {
            parameterTypes.set(i, params[i]);
        }

        result = new Prototype(descriptor, returnType, parameterTypes);
        return putIntern(result);
    }

    /**
     * Helper for {@link #intern} which returns an empty array to
     * populate with parsed parameter types, and which also ensures
     * that there is a '(' at the start of the descriptor and a
     * single ')' somewhere before the end.
     *
     * @param descriptor {@code non-null;} the descriptor string
     * @return {@code non-null;} array large enough to hold all parsed parameter
     * types, but which is likely actually larger than needed
     */
    private static Type[] makeParameterArray(String descriptor) {
        int length = descriptor.length();

        if (descriptor.charAt(0) != '(') {
            throw new IllegalArgumentException("bad descriptor");
        }

        /*
         * This is a cheesy way to establish an upper bound on the
         * number of parameters: Just count capital letters.
         */
        int closeAt = 0;
        int maxParams = 0;
        for (int i = 1; i < length; i++) {
            char c = descriptor.charAt(i);
            if (c == ')') {
                closeAt = i;
                break;
            }
            if ((c >= 'A') && (c <= 'Z')) {
                maxParams++;
            }
        }

        if ((closeAt == 0) || (closeAt == (length - 1))) {
            throw new IllegalArgumentException("bad descriptor");
        }

        if (descriptor.indexOf(')', closeAt + 1) != -1) {
            throw new IllegalArgumentException("bad descriptor");
        }

        return new Type[maxParams];
    }

    /**
     * Interns an instance, adding to the descriptor as necessary based
     * on the given definer, name, and flags. For example, an init
     * method has an uninitialized object of type {@code definer}
     * as its first argument.
     *
     * @param descriptor {@code non-null;} the descriptor string
     * @param definer {@code non-null;} class the method is defined on
     * @param isStatic whether this is a static method
     * @param isInit whether this is an init method
     * @return {@code non-null;} the interned instance
     */
    public static Prototype intern(String descriptor, Type definer,
            boolean isStatic, boolean isInit) {
        Prototype base = intern(descriptor);

        if (isStatic) {
            return base;
        }

        if (isInit) {
            definer = definer.asUninitialized(Integer.MAX_VALUE);
        }

        return base.withFirstParameter(definer);
    }

    /**
     * Interns an instance which consists of the given number of
     * {@code int}s along with the given return type
     *
     * @param returnType {@code non-null;} the return type
     * @param count {@code > 0;} the number of elements in the prototype
     * @return {@code non-null;} the interned instance
     */
    public static Prototype internInts(Type returnType, int count) {
        // Make the descriptor...

        StringBuffer sb = new StringBuffer(100);

        sb.append('(');

        for (int i = 0; i < count; i++) {
            sb.append('I');
        }

        sb.append(')');
        sb.append(returnType.getDescriptor());

        // ...and intern it.
        return intern(sb.toString());
    }

    /**
     * Constructs an instance. This is a private constructor; use one
     * of the public static methods to get instances.
     *
     * @param descriptor {@code non-null;} the descriptor string
     */
    private Prototype(String descriptor, Type returnType,
            StdTypeList parameterTypes) {
        if (descriptor == null) {
            throw new NullPointerException("descriptor == null");
        }

        if (returnType == null) {
            throw new NullPointerException("returnType == null");
        }

        if (parameterTypes == null) {
            throw new NullPointerException("parameterTypes == null");
        }

        this.descriptor = descriptor;
        this.returnType = returnType;
        this.parameterTypes = parameterTypes;
        this.parameterFrameTypes = null;
    }

    /** {@inheritDoc} */
    @Override
    public boolean equals(Object other) {
        if (this == other) {
            /*
             * Since externally-visible instances are interned, this
             * check helps weed out some easy cases.
             */
            return true;
        }

        if (!(other instanceof Prototype)) {
            return false;
        }

        return descriptor.equals(((Prototype) other).descriptor);
    }

    /** {@inheritDoc} */
    @Override
    public int hashCode() {
        return descriptor.hashCode();
    }

    /** {@inheritDoc} */
    public int compareTo(Prototype other) {
        if (this == other) {
            return 0;
        }

        /*
         * The return type is the major order, and then args in order,
         * and then the shorter list comes first (similar to string
         * sorting).
         */

        int result = returnType.compareTo(other.returnType);

        if (result != 0) {
            return result;
        }

        int thisSize = parameterTypes.size();
        int otherSize = other.parameterTypes.size();
        int size = Math.min(thisSize, otherSize);

        for (int i = 0; i < size; i++) {
            Type thisType = parameterTypes.get(i);
            Type otherType = other.parameterTypes.get(i);

            result = thisType.compareTo(otherType);

            if (result != 0) {
                return result;
            }
        }

        if (thisSize < otherSize) {
            return -1;
        } else if (thisSize > otherSize) {
            return 1;
        } else {
            return 0;
        }
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return descriptor;
    }

    /**
     * Gets the descriptor string.
     *
     * @return {@code non-null;} the descriptor
     */
    public String getDescriptor() {
        return descriptor;
    }

    /**
     * Gets the return type.
     *
     * @return {@code non-null;} the return type
     */
    public Type getReturnType() {
        return returnType;
    }

    /**
     * Gets the list of parameter types.
     *
     * @return {@code non-null;} the list of parameter types
     */
    public StdTypeList getParameterTypes() {
        return parameterTypes;
    }

    /**
     * Gets the list of frame types corresponding to the list of parameter
     * types. The difference between the two lists (if any) is that all
     * "intlike" types (see {@link Type#isIntlike}) are replaced by
     * {@link Type#INT}.
     *
     * @return {@code non-null;} the list of parameter frame types
     */
    public StdTypeList getParameterFrameTypes() {
        if (parameterFrameTypes == null) {
            int sz = parameterTypes.size();
            StdTypeList list = new StdTypeList(sz);
            boolean any = false;
            for (int i = 0; i < sz; i++) {
                Type one = parameterTypes.get(i);
                if (one.isIntlike()) {
                    any = true;
                    one = Type.INT;
                }
                list.set(i, one);
            }
            parameterFrameTypes = any ? list : parameterTypes;
        }

        return parameterFrameTypes;
    }

    /**
     * Returns a new interned instance, which is the same as this instance,
     * except that it has an additional parameter prepended to the original's
     * argument list.
     *
     * @param param {@code non-null;} the new first parameter
     * @return {@code non-null;} an appropriately-constructed instance
     */
    public Prototype withFirstParameter(Type param) {
        String newDesc = "(" + param.getDescriptor() + descriptor.substring(1);
        StdTypeList newParams = parameterTypes.withFirst(param);

        newParams.setImmutable();

        Prototype result =
            new Prototype(newDesc, returnType, newParams);

        return putIntern(result);
    }

    /**
     * Puts the given instance in the intern table if it's not already
     * there. If a conflicting value is already in the table, then leave it.
     * Return the interned value.
     *
     * @param desc {@code non-null;} instance to make interned
     * @return {@code non-null;} the actual interned object
     */
    private static Prototype putIntern(Prototype desc) {
        synchronized (internTable) {
            String descriptor = desc.getDescriptor();
            Prototype already = internTable.get(descriptor);
            if (already != null) {
                return already;
            }
            internTable.put(descriptor, desc);
            return desc;
        }
    }
}
