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

import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.Map.Entry;

import signature.converter.Visibility;
import signature.model.IAnnotation;
import signature.model.IAnnotationElement;
import signature.model.IAnnotationField;
import signature.model.IApi;
import signature.model.IClassDefinition;
import signature.model.IClassReference;
import signature.model.IConstructor;
import signature.model.IEnumConstant;
import signature.model.IField;
import signature.model.IGenericDeclaration;
import signature.model.IMethod;
import signature.model.IPackage;
import signature.model.IParameter;
import signature.model.ITypeReference;
import signature.model.ITypeVariableDefinition;
import signature.model.ITypeVariableReference;
import signature.model.Kind;
import signature.model.Modifier;
import signature.model.impl.SigAnnotation;
import signature.model.impl.SigAnnotationElement;
import signature.model.impl.SigAnnotationField;
import signature.model.impl.SigApi;
import signature.model.impl.SigClassDefinition;
import signature.model.impl.SigClassReference;
import signature.model.impl.SigConstructor;
import signature.model.impl.SigEnumConstant;
import signature.model.impl.SigExecutableMember;
import signature.model.impl.SigField;
import signature.model.impl.SigMethod;
import signature.model.impl.SigPackage;
import signature.model.impl.SigParameter;
import signature.model.impl.SigPrimitiveType;
import signature.model.impl.SigTypeVariableDefinition;
import signature.model.impl.SigTypeVariableReference;
import signature.model.util.TypePool;

import com.sun.javadoc.AnnotationDesc;
import com.sun.javadoc.AnnotationTypeDoc;
import com.sun.javadoc.AnnotationTypeElementDoc;
import com.sun.javadoc.AnnotationValue;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.ConstructorDoc;
import com.sun.javadoc.ExecutableMemberDoc;
import com.sun.javadoc.FieldDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.PackageDoc;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.ParameterizedType;
import com.sun.javadoc.ProgramElementDoc;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Type;
import com.sun.javadoc.TypeVariable;
import com.sun.javadoc.WildcardType;
import com.sun.javadoc.AnnotationDesc.ElementValuePair;

public class DocletToSigConverter {

    TypePool pool;
    Set<String> packageNames;

    /**
     * Converts the signature information javadoc knows about into a
     * signature.model.ISources structure.
     */
    public IApi convertDocletRoot(String name, RootDoc root,
            Visibility visibility, Set<String> packageNames) {
        this.pool = new TypePool();
        this.packageNames = packageNames;

        Set<IPackage> packages = new HashSet<IPackage>();

        for (PackageDoc pack : root.specifiedPackages()) {
            assert packageNames.contains(pack.name());
            packages.add(convertPackage(pack));
        }

        SigApi sources = new SigApi(name, visibility);
        sources.setPackages(packages);
        return sources;
    }

    private IPackage convertPackage(PackageDoc packageDoc) {
        Set<IClassDefinition> classes = new HashSet<IClassDefinition>();
        for (ClassDoc clazz : packageDoc.allClasses()) {
            // classes.add((IClass)convertType(clazz));
            classes.add(convertClass(clazz));
        }

        SigPackage p = new SigPackage(packageDoc.name());
        p.setClasses(classes);
        p.setAnnotations(convertAnnotations(packageDoc.annotations()));
        return p;
    }

    private SigClassDefinition convertClass(ClassDoc classDoc) {

        SigClassDefinition c = pool.getClass(classDoc.containingPackage()
                .name(), classDoc.name());
        if (c.getKind() != Kind.UNINITIALIZED) return c;

        if (classDoc.isEnum())
            c.setKind(Kind.ENUM);
        else if (classDoc.isInterface())
            c.setKind(Kind.INTERFACE);
        else if (classDoc.isClass())
            c.setKind(Kind.CLASS);
        else if (classDoc.isAnnotationType()) c.setKind(Kind.ANNOTATION);

        if (!packageNames.contains(c.getPackageName())) {
            // no additional conversion for this class is necessary
            initializeClass(c);
            return c;
        }

        c.setModifiers(convertModifiers(classDoc.modifierSpecifier()));
        if (Kind.INTERFACE.equals(c.getKind())
                || Kind.ANNOTATION.equals(c.getKind())) {
            c.getModifiers().add(Modifier.ABSTRACT);
        }

        // superclass may be a class or a parameterized type (e.g. extends
        // List<String>),
        // may also be null if classDoc is an interface
        Type superclassType = classDoc.superclassType();
        if (superclassType != null) {
            c.setSuperClass(convertTypeReference(classDoc.superclassType()));
        } else {
            c.setSuperClass(null);
        }

        Set<ITypeReference> interfaces = new HashSet<ITypeReference>();
        for (Type interfaceType : classDoc.interfaceTypes()) {
            interfaces.add(convertTypeReference(interfaceType));
        }
        c.setInterfaces(interfaces);

        ClassDoc containingClass = classDoc.containingClass();
        if (containingClass != null)
            c.setDeclaringClass(convertClass(containingClass));
        else
            c.setDeclaringClass(null);

        Set<IClassDefinition> innerClasses = new HashSet<IClassDefinition>();
        for (ClassDoc innerClass : classDoc.innerClasses()) {
            innerClasses.add(convertClass(innerClass));
        }
        c.setInnerClasses(innerClasses);

        Set<IConstructor> constructors = new HashSet<IConstructor>();
        for (ConstructorDoc constructor : classDoc.constructors()) {
            constructors.add(convertConstructor(constructor));
        }
        c.setConstructors(constructors);

        Set<IMethod> methods = new HashSet<IMethod>();
        for (MethodDoc method : classDoc.methods()) {
            methods.add(convertMethod(method));
        }
        c.setMethods(methods);

        Set<IField> fields = new HashSet<IField>();
        for (FieldDoc field : classDoc.fields()) {
            fields.add(convertField(field));
        }
        c.setFields(fields);

        Set<IEnumConstant> enumConstants = new HashSet<IEnumConstant>();
        int ordinal = 0;
        for (FieldDoc enumConstant : classDoc.enumConstants()) {
            enumConstants.add(convertEnumConstant(enumConstant, ordinal++));
        }
        c.setEnumConstants(enumConstants);

        List<ITypeVariableDefinition> typeParameters =
                new LinkedList<ITypeVariableDefinition>();
        for (TypeVariable typeVariable : classDoc.typeParameters()) {
            typeParameters
                    .add(((ITypeVariableReference) convertTypeReference(
                            typeVariable)).getTypeVariableDefinition());
        }
        c.setTypeParameters(typeParameters);

        if (classDoc.isAnnotationType()) {
            Map<SigAnnotationField, AnnotationTypeElementDoc> annotationFieldAnnotations =
                    new HashMap<SigAnnotationField, AnnotationTypeElementDoc>();

            // AnnotationTypeDoc annotationType =
            // classDoc.asAnnotationTypeDoc(); // bug in Doclet Implementation,
            // has been reported to sun
            AnnotationTypeDoc annotationType = (AnnotationTypeDoc) classDoc;

            Set<IAnnotationField> annotationFields =
                    new HashSet<IAnnotationField>();
            for (AnnotationTypeElementDoc annotationElement : annotationType
                    .elements()) {
                SigAnnotationField annotationField = new SigAnnotationField(
                        annotationElement.name());
                annotationField.setModifiers(convertModifiers(annotationElement
                        .modifierSpecifier()));
                annotationField.setType(convertTypeReference(annotationElement
                        .returnType()));
                annotationField
                        .setDefaultValue(convertAnnotationValue(
                                annotationElement.defaultValue()));

                // the annotations on fields are set later because these
                // annotations may be of
                // the same type and may use fields which are not yet defined
                annotationFieldAnnotations.put(annotationField,
                        annotationElement);

                annotationFields.add(annotationField);
            }
            c.setAnnotationFields(annotationFields);

            // set annotation field annotations
            for (Entry<SigAnnotationField, AnnotationTypeElementDoc> entry :
                    annotationFieldAnnotations.entrySet()) {
                entry.getKey().setAnnotations(
                        convertAnnotations(entry.getValue().annotations()));
            }
        } else { // no annotation type
            c.setAnnotationFields(null);
        }

        // set class annotations
        c.setAnnotations(convertAnnotations(classDoc.annotations()));

        return c;

    }

    private Object convertAnnotationValue(AnnotationValue annotationValue) {
        if (annotationValue == null) {
            return null;
        }
        Object value = annotationValue.value();
        if (value instanceof Type) {
            // Type contains primitive types as well, e.g. void.class
            return convertTypeReference((Type) value);
        } else if (value instanceof String) {
            return value;
        } else if (value instanceof Double || value instanceof Float
                || value instanceof Long || value instanceof Integer
                || value instanceof Short || value instanceof Byte
                || value instanceof Character || value instanceof Boolean) {
            return value;
        } else if (value instanceof FieldDoc) {
            FieldDoc field = (FieldDoc) value;
            String name = field.name();
            ITypeReference fieldType = convertTypeReference(field.type());
            IClassReference fieldClassRef = (IClassReference) fieldType;
            IClassDefinition fieldClass = fieldClassRef.getClassDefinition();

            assert fieldClass.getKind() == Kind.ENUM;
            Set<IEnumConstant> constants = fieldClass.getEnumConstants();
            for (IEnumConstant enumConstant : constants) {
                if (enumConstant.getName().equals(name)) value = enumConstant;
            }
            assert value instanceof IEnumConstant;
            return value;
        } else if (value instanceof AnnotationDesc) {
            return convertAnnotation((AnnotationDesc) value);
        } else if (value instanceof AnnotationValue) {
            return convertAnnotationValue((AnnotationValue) value);
        } else if (value instanceof AnnotationValue[]) {
            AnnotationValue[] arr = (AnnotationValue[]) value;
            int length = arr.length;
            Object[] annotationArray = new Object[length];
            for (int i = 0; i < length; i++) {
                annotationArray[i] = convertAnnotationValue(arr[i]);
            }
            return annotationArray;
        } else {
            throw new RuntimeException("not expected case");
        }
    }

    private ITypeReference convertArrayType(Type type) {
        assert type.asWildcardType() == null;
        assert type.asAnnotationTypeDoc() == null;

        ITypeReference baseType = null;
        if (type.asTypeVariable() != null) {
            baseType = convertTypeReference(type.asTypeVariable());
        } else if (type.asParameterizedType() != null) {
            baseType = convertTypeReference(type.asParameterizedType());
        } else if (type.asClassDoc() != null) {
            baseType = new SigClassReference(convertClass(type.asClassDoc()));
        } else if (type.isPrimitive()) {
            baseType = SigPrimitiveType.valueOfTypeName(type.typeName());
        } else {
            throw new RuntimeException(type.toString());
        }

        ITypeReference arrayType = baseType;
        int dimension = type.dimension().length() / 2;
        while (dimension > 0) {
            arrayType = pool.getArrayType(arrayType);
            dimension--;
        }

        return arrayType;
    }

    private SigTypeVariableDefinition currentTypeVariableDefinition = null;

    private ITypeReference convertTypeReference(Type type) {
        assert type != null;

        if (!"".equals(type.dimension())) {
            return convertArrayType(type);
        }

        ParameterizedType pType = type.asParameterizedType();
        if (pType != null) {
            ITypeReference ownerType = null;
            Type containingType = pType.containingType();
            if (containingType != null)
                ownerType = convertTypeReference(containingType);
            IClassReference rawType = new SigClassReference(convertClass(pType
                    .asClassDoc()));

            List<ITypeReference> typeArguments =
                    new LinkedList<ITypeReference>();
            for (Type typeArgument : pType.typeArguments()) {
                typeArguments.add(convertTypeReference(typeArgument));
            }

            if (typeArguments.size() > 0) {
                return pool.getParameterizedType(ownerType, rawType,
                        typeArguments);
            } else {
                return rawType;
            }
        }

        TypeVariable tv = type.asTypeVariable();
        if (tv != null) {
            String name = tv.typeName();

            if (currentTypeVariableDefinition != null
                    && name.equals(currentTypeVariableDefinition.getName()))
                return new SigTypeVariableReference(
                        currentTypeVariableDefinition);

            IGenericDeclaration genericDeclaration = null;
            ProgramElementDoc programElement = tv.owner();
            if (programElement instanceof ClassDoc) {
                genericDeclaration = convertClass((ClassDoc) programElement);
            } else if (programElement instanceof MethodDoc
                    && currentMethod.size() > 0) {
                genericDeclaration = currentMethod.peek();
            } else if (programElement instanceof ConstructorDoc
                    && currentConstructor.size() > 0) {
                genericDeclaration = currentConstructor.peek();
            } else {
                throw new IllegalStateException("situation not expected");
            }
            SigTypeVariableDefinition typeVariable = pool.getTypeVariable(name,
                    genericDeclaration);

            List<ITypeReference> upperBounds = new LinkedList<ITypeReference>();
            for (Type upperBound : tv.bounds()) {
                // we are converting a type variable declaration which is stored
                // in the
                // field currentTypeVariableDefinition
                assert currentTypeVariableDefinition == null;
                currentTypeVariableDefinition = typeVariable;
                upperBounds.add(convertTypeReference(upperBound));
                currentTypeVariableDefinition = null;
            }
            if (upperBounds.size() == 0) {
                // no explicit bounds, use java.lang.Object
                upperBounds.add(pool.getClassReference("java.lang", "Object"));
            }
            typeVariable.setUpperBounds(upperBounds);

            return new SigTypeVariableReference(typeVariable);
        }

        WildcardType wt = type.asWildcardType();
        if (wt != null) {
            ITypeReference lowerBound = null;
            for (Type superBound : wt.superBounds()) {
                lowerBound = convertTypeReference(superBound);
            }

            List<ITypeReference> upperBounds = new LinkedList<ITypeReference>();
            for (Type upperBound : wt.extendsBounds()) {
                upperBounds.add(convertTypeReference(upperBound));
            }
            if (upperBounds.size() == 0) {
                // no explicit bounds, use java.lang.Object
                upperBounds.add(pool.getClassReference("java.lang", "Object"));
            }

            return pool.getWildcardType(lowerBound, upperBounds);
        }

        ClassDoc c = type.asClassDoc();
        if (c != null) {
            return new SigClassReference(convertClass(c));
        }

        if (type.isPrimitive()) {
            return SigPrimitiveType.valueOfTypeName(type.typeName());
        }

        throw new IllegalStateException(type.toString());
    }

    private void convertExecutableMember(ExecutableMemberDoc member,
            SigExecutableMember m) {
        Set<Modifier> modifiers = convertModifiers(member.modifierSpecifier());

        // Doclet Bug: final values method is not considered as final
        if (member.containingClass().isEnum() && member.name().equals("values")
                && member.parameters().length == 0) {
            modifiers.add(Modifier.FINAL);
        }

        if (member.containingClass().isInterface()) {
            modifiers.add(Modifier.ABSTRACT);
        }

        m.setModifiers(modifiers);
        m.setAnnotations(convertAnnotations(member.annotations()));
        m.setDeclaringClass(convertClass(member.containingClass()));

        List<ITypeVariableDefinition> typeParameters =
                new LinkedList<ITypeVariableDefinition>();
        for (TypeVariable typeParameter : member.typeParameters()) {
            String name = typeParameter.typeName();
            IGenericDeclaration genericDeclaration = null;
            if (currentMethod.size() > 0)
                genericDeclaration = currentMethod.peek();
            else if (currentConstructor.size() > 0)
                genericDeclaration = currentConstructor.peek();
            else
                throw new RuntimeException();
            SigTypeVariableDefinition p = pool.getTypeVariable(name,
                    genericDeclaration);

            List<ITypeReference> upperBounds = new LinkedList<ITypeReference>();
            for (Type u : typeParameter.bounds()) {
                upperBounds.add(convertTypeReference(u));
            }
            p.setUpperBounds(upperBounds);
            typeParameters.add(p);
        }
        m.setTypeParameters(typeParameters);

        List<IParameter> parameters = new LinkedList<IParameter>();
        for (Parameter parameter : member.parameters()) {
            SigParameter p = new SigParameter(convertTypeReference(parameter
                    .type()));
            p.setAnnotations(convertAnnotations(parameter.annotations()));
            parameters.add(p);
        }
        m.setParameters(parameters);

        Set<ITypeReference> exceptions = new HashSet<ITypeReference>();
        for (Type exceptionType : member.thrownExceptionTypes()) {
            exceptions.add(convertTypeReference(exceptionType));
        }
        m.setExceptions(exceptions);
    }

    private Stack<SigMethod> currentMethod = new Stack<SigMethod>();

    private IMethod convertMethod(MethodDoc method) {
        SigMethod m = new SigMethod(method.name());
        currentMethod.push(m);
        convertExecutableMember(method, m);
        m.setReturnType(convertTypeReference(method.returnType()));
        currentMethod.pop();
        return m;
    }

    private Stack<SigConstructor> currentConstructor =
            new Stack<SigConstructor>();

    private IConstructor convertConstructor(ConstructorDoc constructor) {
        SigConstructor c = new SigConstructor(constructor.name());
        currentConstructor.push(c);
        convertExecutableMember(constructor, c);
        currentConstructor.pop();
        return c;
    }

    private IField convertField(FieldDoc field) {
        SigField f = new SigField(field.name());
        f.setAnnotations(convertAnnotations(field.annotations()));
        f.setModifiers(convertModifiers(field.modifierSpecifier()));
        f.setType(convertTypeReference(field.type()));
        return f;
    }

    private IEnumConstant convertEnumConstant(FieldDoc enumConstant,
            int ordinal) {
        SigEnumConstant ec = new SigEnumConstant(enumConstant.name());
        ec.setOrdinal(ordinal);
        ec.setAnnotations(convertAnnotations(enumConstant.annotations()));
        ec.setModifiers(convertModifiers(enumConstant.modifierSpecifier()));
        ec.setType(convertTypeReference(enumConstant.type()));
        return ec;
    }

    private Set<IAnnotation> convertAnnotations(
            AnnotationDesc[] annotationDescs) {
        Set<IAnnotation> annotations = new HashSet<IAnnotation>();
        for (AnnotationDesc annotationDesc : annotationDescs) {
            if (!annotationRetentionIsSource(annotationDesc))
                annotations.add(convertAnnotation(annotationDesc));
        }
        return annotations;
    }

    private boolean annotationRetentionIsSource(AnnotationDesc annotationDesc) {
        AnnotationTypeDoc type = annotationDesc.annotationType();
        AnnotationDesc[] annotations = type.annotations();
        for (AnnotationDesc d : annotations) {
            if ("java.lang.annotation.Retention".equals(d.annotationType()
                    .qualifiedName())) {
                for (ElementValuePair value : d.elementValues()) {
                    if ("value".equals(value.element().name())) {
                        return "java.lang.annotation.RetentionPolicy.SOURCE"
                                .equals(value.value().value().toString());
                    }
                }
            }
        }
        // default retention policy is CLASS
        return false;
    }

    private IAnnotation convertAnnotation(AnnotationDesc annotationDesc) {
        SigAnnotation a = new SigAnnotation();

        IClassReference annotationType = (IClassReference) convertTypeReference(
                annotationDesc.annotationType());
        a.setType(annotationType);

        Set<IAnnotationElement> elements = new HashSet<IAnnotationElement>();
        for (AnnotationDesc.ElementValuePair pair : annotationDesc
                .elementValues()) {
            SigAnnotationElement element = new SigAnnotationElement();
            elements.add(element);

            element.setValue(convertAnnotationValue(pair.value()));
            String name = pair.element().name();
            for (IAnnotationField field : annotationType.getClassDefinition()
                    .getAnnotationFields()) {
                if (field.getName().equals(name)) {
                    element.setDeclaringField(field);
                }
            }
        }
        a.setElements(elements);
        return a;
    }

    private void initializeClass(SigClassDefinition c) {
        c.setAnnotationFields(null);
        c.setAnnotations(null);
        c.setConstructors(null);
        c.setDeclaringClass(null);
        c.setEnumConstants(null);
        c.setFields(null);
        c.setInnerClasses(null);
        c.setInterfaces(null);
        c.setMethods(null);
        c.setModifiers(null);
        c.setSuperClass(null);
        c.setTypeParameters(null);
    }

    private Set<Modifier> convertModifiers(int mod) {
        Set<Modifier> modifiers = EnumSet.noneOf(Modifier.class);
        if (java.lang.reflect.Modifier.isAbstract(mod))
            modifiers.add(Modifier.ABSTRACT);
        if (java.lang.reflect.Modifier.isFinal(mod))
            modifiers.add(Modifier.FINAL);
        // if (java.lang.reflect.Modifier.isNative(mod))
        // modifiers.add(Modifier.NATIVE);
        if (java.lang.reflect.Modifier.isPrivate(mod))
            modifiers.add(Modifier.PRIVATE);
        if (java.lang.reflect.Modifier.isProtected(mod))
            modifiers.add(Modifier.PROTECTED);
        if (java.lang.reflect.Modifier.isPublic(mod))
            modifiers.add(Modifier.PUBLIC);
        if (java.lang.reflect.Modifier.isStatic(mod))
            modifiers.add(Modifier.STATIC);
        // if (java.lang.reflect.Modifier.isStrict(mod))
        // modifiers.add(Modifier.STRICT);
        // if (java.lang.reflect.Modifier.isSynchronized(mod))
        // modifiers.add(Modifier.SYNCHRONIZED);
        // if (java.lang.reflect.Modifier.isTransient(mod))
        // modifiers.add(Modifier.TRANSIENT);
        if (java.lang.reflect.Modifier.isVolatile(mod))
            modifiers.add(Modifier.VOLATILE);

        return modifiers;
    }

}
