/*
 * Copyright (C) 2009 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 signature.converter.dex;

import static signature.converter.dex.DexUtil.getClassName;
import static signature.converter.dex.DexUtil.getPackageName;
import signature.model.IClassDefinition;
import signature.model.IClassReference;
import signature.model.IConstructor;
import signature.model.IGenericDeclaration;
import signature.model.IMethod;
import signature.model.ITypeReference;
import signature.model.ITypeVariableDefinition;
import signature.model.ITypeVariableReference;
import signature.model.impl.SigArrayType;
import signature.model.impl.SigParameterizedType;
import signature.model.impl.SigPrimitiveType;
import signature.model.impl.SigTypeVariableDefinition;
import signature.model.impl.SigWildcardType;
import signature.model.impl.Uninitialized;
import signature.model.util.ITypeFactory;

import java.lang.reflect.GenericSignatureFormatError;
import java.util.ArrayList;
import java.util.List;

/**
 * Implements a parser for the generics signature attribute. Uses a top-down,
 * recursive descent parsing approach for the following grammar:
 * 
 * <pre>
 * ClassSignature ::=
 *     OptFormalTypeParams SuperclassSignature {SuperinterfaceSignature}.
 * SuperclassSignature ::= ClassTypeSignature.
 * SuperinterfaceSignature ::= ClassTypeSignature.
 *
 * OptFormalTypeParams ::=
 *     ["<" FormalTypeParameter {FormalTypeParameter} ">"].
 *
 * FormalTypeParameter ::= Ident ClassBound {InterfaceBound}.
 * ClassBound ::= ":" [FieldTypeSignature].
 * InterfaceBound ::= ":" FieldTypeSignature.
 *
 * FieldTypeSignature ::=
 *     ClassTypeSignature | ArrayTypeSignature | TypeVariableSignature.
 * ArrayTypeSignature ::= "[" TypSignature.
 *
 * ClassTypeSignature ::=
 *     "L" {Ident "/"} Ident OptTypeArguments {"." Ident OptTypeArguments} ";".
 *
 * OptTypeArguments ::= "<" TypeArgument {TypeArgument} ">".
 *
 * TypeArgument ::= ([WildcardIndicator] FieldTypeSignature) | "*".
 * WildcardIndicator ::= "+" | "-".
 *
 * TypeVariableSignature ::= "T" Ident ";".
 *
 * TypSignature ::= FieldTypeSignature | BaseType.
 * BaseType ::= "B" | "C" | "D" | "F" | "I" | "J" | "S" | "Z".
 *
 * MethodTypeSignature ::=
 *     OptFormalTypeParams "(" {TypeSignature} ")" ReturnType {ThrowsSignature}.
 * ThrowsSignature ::= ("^" ClassTypeSignature) | ("^" TypeVariableSignature).
 *
 * ReturnType ::= TypSignature | VoidDescriptor.
 * VoidDescriptor ::= "V".
 * </pre>
 */
public class GenericSignatureParser {

    public List<ITypeReference> exceptionTypes;
    public List<ITypeReference> parameterTypes;
    public List<ITypeVariableDefinition> formalTypeParameters;
    public ITypeReference returnType;
    public ITypeReference fieldType;
    public List<ITypeReference> interfaceTypes;
    public ITypeReference superclassType;

    private IGenericDeclaration genericDecl;

    /*
     * Parser:
     */
    private char symbol; // 0: eof; else valid term symbol or first char of
    // identifier.
    private String identifier;


    /*
     * Scanner: eof is private to the scan methods and it's set only when a scan
     * is issued at the end of the buffer.
     */
    private boolean eof;

    private char[] buffer;
    private int pos;

    private final ITypeFactory factory;
    private final IClassInitializer classFinder;
    private boolean parseForField;


    public GenericSignatureParser(ITypeFactory factory,
            IClassInitializer classFinder) {
        this.factory = factory;
        this.classFinder = classFinder;
    }

    private void setInput(IGenericDeclaration genericDecl, String input) {
        if (input != null) {
            this.genericDecl = genericDecl;
            this.buffer = input.toCharArray();
            this.eof = false;
            scanSymbol();
        } else {
            this.eof = true;
        }
    }

    public ITypeReference parseNonGenericType(String typeSignature) {
        setInput(null, typeSignature);
        ITypeReference type = parsePrimitiveType();
        if (type == null) {
            type = parseFieldTypeSignature();
        }
        return type;
    }

    public ITypeReference parseNonGenericReturnType(String typeSignature) {
        setInput(null, typeSignature);
        ITypeReference returnType = parsePrimitiveType();
        if (returnType == null) {
            returnType = parseReturnType();
        }
        return returnType;
    }

    private ITypeReference parsePrimitiveType() {
        switch (symbol) {
        case 'B':
            scanSymbol();
            return SigPrimitiveType.BYTE_TYPE;
        case 'C':
            scanSymbol();
            return SigPrimitiveType.CHAR_TYPE;
        case 'D':
            scanSymbol();
            return SigPrimitiveType.DOUBLE_TYPE;
        case 'F':
            scanSymbol();
            return SigPrimitiveType.FLOAT_TYPE;
        case 'I':
            scanSymbol();
            return SigPrimitiveType.INT_TYPE;
        case 'J':
            scanSymbol();
            return SigPrimitiveType.LONG_TYPE;
        case 'S':
            scanSymbol();
            return SigPrimitiveType.SHORT_TYPE;
        case 'Z':
            scanSymbol();
            return SigPrimitiveType.BOOLEAN_TYPE;
        default:
            return null;
        }
    }

    /**
     * Parses the generic signature of a class and creates the data structure
     * representing the signature.
     * 
     * @param classToProcess
     *            the GenericDeclaration calling this method
     * @param signature
     *            the generic signature of the class
     */
    public void parseForClass(IClassDefinition classToProcess,
            String signature) {
        setInput(classToProcess, signature);
        if (!eof) {
            parseClassSignature();
        } else {
            throw new IllegalStateException("Generic signature is invalid!");
        }
    }

    /**
     * Parses the generic signature of a method and creates the data structure
     * representing the signature.
     * 
     * @param genericDecl
     *            the GenericDeclaration calling this method
     * @param signature
     *            the generic signature of the class
     */
    public void parseForMethod(IMethod genericDecl, String signature) {
        setInput(genericDecl, signature);
        if (!eof) {
            parseMethodTypeSignature();
        } else {
            throw new IllegalStateException("Generic signature is invalid!");
        }
    }

    /**
     * Parses the generic signature of a constructor and creates the data
     * structure representing the signature.
     * 
     * @param genericDecl
     *            the GenericDeclaration calling this method
     * @param signature
     *            the generic signature of the class
     */
    public void parseForConstructor(IConstructor genericDecl,
            String signature) {
        setInput(genericDecl, signature);
        if (!eof) {
            parseMethodTypeSignature();
        } else {
            throw new IllegalStateException("Generic signature is invalid!");
        }
    }

    /**
     * Parses the generic signature of a field and creates the data structure
     * representing the signature.
     * 
     * @param genericDecl
     *            the GenericDeclaration calling this method
     * @param signature
     *            the generic signature of the class
     */
    public void parseForField(IClassDefinition genericDecl, String signature) {
        parseForField = true;
        setInput(genericDecl, signature);
        try {
            if (!eof) {
                this.fieldType = parseFieldTypeSignature();
            } else {
                throw new IllegalStateException(
                        "Generic signature is invalid!");
            }
        } finally {
            parseForField = false;
        }
    }

    private void parseClassSignature() {
        // ClassSignature ::=
        // OptFormalTypeParameters SuperclassSignature
        // {SuperinterfaceSignature}.

        parseOptFormalTypeParameters();

        // SuperclassSignature ::= ClassTypeSignature.
        this.superclassType = parseClassTypeSignature();

        interfaceTypes = new ArrayList<ITypeReference>(16);
        while (symbol > 0) {
            // SuperinterfaceSignature ::= ClassTypeSignature.
            interfaceTypes.add(parseClassTypeSignature());
        }
    }

    private void parseOptFormalTypeParameters() {
        // OptFormalTypeParameters ::=
        // ["<" FormalTypeParameter {FormalTypeParameter} ">"].

        List<ITypeVariableDefinition> typeParameters =
                new ArrayList<ITypeVariableDefinition>();

        if (symbol == '<') {
            scanSymbol();
            typeParameters.add(parseFormalTypeParameter());
            while ((symbol != '>') && (symbol > 0)) {
                typeParameters.add(parseFormalTypeParameter());
            }
            expect('>');
        }

        formalTypeParameters = typeParameters;
    }

    private SigTypeVariableDefinition parseFormalTypeParameter() {
        // FormalTypeParameter ::= Ident ClassBound {InterfaceBound}.

        scanIdentifier();
        String name = identifier.intern();
        SigTypeVariableDefinition typeVariable = factory.getTypeVariable(name,
                genericDecl);

        List<ITypeReference> bounds = new ArrayList<ITypeReference>();

        // ClassBound ::= ":" [FieldTypeSignature].
        expect(':');
        if (symbol == 'L' || symbol == '[' || symbol == 'T') {
            bounds.add(parseFieldTypeSignature());
        }

        while (symbol == ':') {
            // InterfaceBound ::= ":" FieldTypeSignature.
            scanSymbol();
            bounds.add(parseFieldTypeSignature());
        }
        typeVariable.setUpperBounds(bounds);
        return typeVariable;
    }

    /**
     * Returns the generic declaration for the type variable with the specified
     * name.
     * 
     * @param variableName
     *            the name of the type variable
     * @param declaration
     *            the declaration to start searching
     * @return the declaration which defines the specified type variable
     */
    private IGenericDeclaration getDeclarationOfTypeVariable(
            String variableName, IClassDefinition declaration) {
        assert variableName != null;
        assert declaration != null;

        if (!Uninitialized.isInitialized(declaration.getTypeParameters())) {
            declaration = classFinder.initializeClass(declaration
                    .getPackageName(), declaration.getName());
        }

        for (ITypeVariableDefinition typeVariable : declaration
                .getTypeParameters()) {
            if (variableName.equals(typeVariable.getName())) {
                return declaration;
            }
        }
        return getDeclarationOfTypeVariable(variableName, declaration
                .getDeclaringClass());
    }

    private ITypeReference parseFieldTypeSignature() {
        // FieldTypeSignature ::= ClassTypeSignature | ArrayTypeSignature
        // | TypeVariableSignature.

        switch (symbol) {
        case 'L':
            return parseClassTypeSignature();
        case '[':
            // ArrayTypeSignature ::= "[" TypSignature.
            scanSymbol();
            SigArrayType arrayType = factory.getArrayType(parseTypeSignature());
            return arrayType;
        case 'T':
            return parseTypeVariableSignature();
        default:
            throw new GenericSignatureFormatError();
        }
    }

    private ITypeReference parseClassTypeSignature() {
        // ClassTypeSignature ::= "L" {Ident "/"} Ident
        // OptTypeArguments {"." Ident OptTypeArguments} ";".

        expect('L');

        StringBuilder qualIdent = new StringBuilder("L");
        scanIdentifier();
        while (symbol == '/') {
            scanSymbol();
            qualIdent.append(identifier).append("/");
            scanIdentifier();
        }

        qualIdent.append(this.identifier);


        List<ITypeReference> typeArgs = parseOptTypeArguments();

        ITypeReference parentType = null;

        String packageName = getPackageName(qualIdent.toString() + ";");
        String className = getClassName(qualIdent.toString() + ";");

        if (typeArgs.isEmpty()) {
            parentType = factory.getClassReference(packageName, className);
        } else {
            IClassReference rawType = factory.getClassReference(packageName,
                    className);
            SigParameterizedType parameterizedType = factory
                    .getParameterizedType(null, rawType, typeArgs);
            parentType = parameterizedType;
        }

        ITypeReference typeToReturn = parentType;


        // if owner type is a parameterized type, the types are separated by '.'
        while (symbol == '.') {
            // Deal with Member Classes:
            scanSymbol();
            scanIdentifier();
            qualIdent.append("$").append(identifier);
            typeArgs = parseOptTypeArguments();
            ITypeReference memberType = null;

            packageName = getPackageName(qualIdent.toString() + ";");
            className = getClassName(qualIdent.toString() + ";");

            if (typeArgs.isEmpty()) {
                memberType = factory.getClassReference(packageName, className);
            } else {
                IClassReference rawType = factory.getClassReference(
                        packageName, className);
                SigParameterizedType parameterizedType = factory
                        .getParameterizedType(parentType, rawType, typeArgs);
                memberType = parameterizedType;
            }
            typeToReturn = memberType;
        }

        expect(';');

        return typeToReturn;
    }

    private List<ITypeReference> parseOptTypeArguments() {
        // OptTypeArguments ::= "<" TypeArgument {TypeArgument} ">".

        List<ITypeReference> typeArgs = new ArrayList<ITypeReference>(8);
        if (symbol == '<') {
            scanSymbol();

            typeArgs.add(parseTypeArgument());
            while ((symbol != '>') && (symbol > 0)) {
                typeArgs.add(parseTypeArgument());
            }
            expect('>');
        }
        return typeArgs;
    }

    private ITypeReference parseTypeArgument() {
        // TypeArgument ::= (["+" | "-"] FieldTypeSignature) | "*".
        List<ITypeReference> extendsBound = new ArrayList<ITypeReference>(1);
        ITypeReference superBound = null;
        if (symbol == '*') {
            scanSymbol();
            extendsBound.add(factory.getClassReference("java.lang", "Object"));
            SigWildcardType wildcardType = factory.getWildcardType(superBound,
                    extendsBound);
            return wildcardType;
        } else if (symbol == '+') {
            scanSymbol();
            extendsBound.add(parseFieldTypeSignature());
            SigWildcardType wildcardType = factory.getWildcardType(superBound,
                    extendsBound);
            return wildcardType;
        } else if (symbol == '-') {
            scanSymbol();
            superBound = parseFieldTypeSignature();
            extendsBound.add(factory.getClassReference("java.lang", "Object"));
            SigWildcardType wildcardType = factory.getWildcardType(superBound,
                    extendsBound);
            return wildcardType;
        } else {
            return parseFieldTypeSignature();
        }
    }

    private ITypeVariableReference parseTypeVariableSignature() {
        // TypeVariableSignature ::= "T" Ident ";".
        expect('T');
        scanIdentifier();
        expect(';');

        IGenericDeclaration declaration = genericDecl;

        if (!factory.containsTypeVariableDefinition(identifier, declaration)) {
            // since a field is not a generic declaration, i need to treat it
            // differently.
            // the generic declaration
            if (parseForField) {
                declaration = getDeclarationOfTypeVariable(identifier,
                        (IClassDefinition) genericDecl);
            } else {
                declaration = getDeclarationOfTypeVariable(identifier,
                        genericDecl.getDeclaringClass());
            }
            // just create type variable
            factory.getTypeVariable(identifier, declaration);
        }

        return factory.getTypeVariableReference(identifier, declaration);
    }

    private ITypeReference parseTypeSignature() {
        switch (symbol) {
        case 'B':
            scanSymbol();
            return SigPrimitiveType.BYTE_TYPE;
        case 'C':
            scanSymbol();
            return SigPrimitiveType.CHAR_TYPE;
        case 'D':
            scanSymbol();
            return SigPrimitiveType.DOUBLE_TYPE;
        case 'F':
            scanSymbol();
            return SigPrimitiveType.FLOAT_TYPE;
        case 'I':
            scanSymbol();
            return SigPrimitiveType.INT_TYPE;
        case 'J':
            scanSymbol();
            return SigPrimitiveType.LONG_TYPE;
        case 'S':
            scanSymbol();
            return SigPrimitiveType.SHORT_TYPE;
        case 'Z':
            scanSymbol();
            return SigPrimitiveType.BOOLEAN_TYPE;
        default:
            // Not an elementary type, but a FieldTypeSignature.
            return parseFieldTypeSignature();
        }
    }

    private void parseMethodTypeSignature() {
        // MethodTypeSignature ::= [FormalTypeParameters]
        // "(" {TypeSignature} ")" ReturnType {ThrowsSignature}.

        parseOptFormalTypeParameters();

        parameterTypes = new ArrayList<ITypeReference>(16);
        expect('(');
        while (symbol != ')' && (symbol > 0)) {
            parameterTypes.add(parseTypeSignature());
        }
        expect(')');

        returnType = parseReturnType();

        exceptionTypes = new ArrayList<ITypeReference>(8);
        while (symbol == '^') {
            scanSymbol();

            // ThrowsSignature ::= ("^" ClassTypeSignature) |
            // ("^" TypeVariableSignature).
            if (symbol == 'T') {
                exceptionTypes.add(parseTypeVariableSignature());
            } else {
                exceptionTypes.add(parseClassTypeSignature());
            }
        }
    }

    private ITypeReference parseReturnType() {
        // ReturnType ::= TypeSignature | "V".
        if (symbol != 'V') {
            return parseTypeSignature();
        } else {
            scanSymbol();
            return SigPrimitiveType.VOID_TYPE;
        }
    }


    //
    // Scanner:
    //

    private void scanSymbol() {
        if (!eof) {
            if (pos < buffer.length) {
                symbol = buffer[pos];
                pos++;
            } else {
                symbol = 0;
                eof = true;
            }
        } else {
            throw new GenericSignatureFormatError();
        }
    }

    private void expect(char c) {
        if (symbol == c) {
            scanSymbol();
        } else {
            throw new GenericSignatureFormatError();
        }
    }

    private boolean isStopSymbol(char ch) {
        switch (ch) {
        case ':':
        case '/':
        case ';':
        case '<':
        case '.':
            return true;
        }
        return false;
    }

    // PRE: symbol is the first char of the identifier.
    // POST: symbol = the next symbol AFTER the identifier.
    private void scanIdentifier() {
        if (!eof) {
            StringBuilder identBuf = new StringBuilder(32);
            if (!isStopSymbol(symbol)) {
                identBuf.append(symbol);
                do {
                    char ch = buffer[pos];
                    if ((ch >= 'a') && (ch <= 'z') || (ch >= 'A')
                            && (ch <= 'Z') || !isStopSymbol(ch)) {
                        identBuf.append(buffer[pos]);
                        pos++;
                    } else {
                        identifier = identBuf.toString();
                        scanSymbol();
                        return;
                    }
                } while (pos != buffer.length);
                identifier = identBuf.toString();
                symbol = 0;
                eof = true;
            } else {
                // Ident starts with incorrect char.
                symbol = 0;
                eof = true;
                throw new GenericSignatureFormatError();
            }
        } else {
            throw new GenericSignatureFormatError();
        }
    }
}