/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 1997-2011 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.tools.xjc.reader;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import com.sun.codemodel.JClass;
import com.sun.codemodel.JClassAlreadyExistsException;
import com.sun.codemodel.JCodeModel;
import com.sun.codemodel.JDefinedClass;
import com.sun.codemodel.JType;
import com.sun.tools.xjc.ErrorReceiver;

import org.xml.sax.Locator;
import org.xml.sax.SAXParseException;

/**
 * Type-related utility methods.
 * 
 * @author
 *    <a href="mailto:kohsuke.kawaguchi@sun.com">Kohsuke KAWAGUCHI</a>
 */
public class TypeUtil {
    
    
    /**
     * Computes the common base type of two types.
     * 
     * @param types
     *      set of {@link JType} objects.
     */
    public static JType getCommonBaseType( JCodeModel codeModel, Collection<? extends JType> types ) {
        return getCommonBaseType( codeModel, types.toArray(new JType[types.size()]) );
    }

    /**
     * Computes the common base type of types.
     * 
     * TODO: this is a very interesting problem. Since one type has possibly
     * multiple base types, it's not an easy problem.
     * The current implementation is very naive.
     * 
     * To make the result deterministic across differente JVMs, we have to
     * use a Set whose ordering is deterministic.
     */
    public static JType getCommonBaseType(JCodeModel codeModel, JType... t) {
        // first, eliminate duplicates.
        Set<JType> uniqueTypes = new TreeSet<JType>(typeComparator);
        for (JType type : t)
            uniqueTypes.add(type);

        // if this yields only one type. return now.
        // this is the only case where we can return a primitive type
        // from this method
        if (uniqueTypes.size() == 1)
            return uniqueTypes.iterator().next();

        // assertion failed. nullType can be used only under a very special circumstance
        assert !uniqueTypes.isEmpty();

        // the null type doesn't need to be taken into account.
        uniqueTypes.remove(codeModel.NULL);

        // box all the types and compute the intersection of all types
        Set<JClass> s = null;

        for (JType type : uniqueTypes) {
            JClass cls = type.boxify();

            if (s == null)
                s = getAssignableTypes(cls);
            else
                s.retainAll(getAssignableTypes(cls));
        }

        // any JClass can be casted to Object, so make sure it's always there
        s.add( codeModel.ref(Object.class));

        // refine 's' by removing "lower" types.
        // for example, if we have both java.lang.Object and
        // java.io.InputStream, then we don't want to use java.lang.Object.

        JClass[] raw = s.toArray(new JClass[s.size()]);
        s.clear();

        for (int i = 0; i < raw.length; i++) { // for each raw[i]
            int j;
            for (j = 0; j < raw.length; j++) { // see if raw[j] "includes" raw[i]
                if (i == j)
                    continue;

                if (raw[i].isAssignableFrom(raw[j]))
                    break; // raw[j] is derived from raw[i], hence j includes i.
            }

            if (j == raw.length)
                // no other type inclueds raw[i]. remember this value.
                s.add(raw[i]);
        }

        assert !s.isEmpty(); // since at least java.lang.Object has to be there

        // we now pick the candidate for the return type
        JClass result = pickOne(s);

        // finally, sometimes this method is used to compute the base type of types like
        // JAXBElement<A>, JAXBElement<B>, and JAXBElement<C>.
        // for those inputs, at this point result=JAXBElement.
        //
        // here, we'll try to figure out the parameterization
        // so that we can return JAXBElement<? extends D> instead of just "JAXBElement".
        if(result.isParameterized())
            return result;

        // for each uniqueType we store the list of base type parameterization
        List<List<JClass>> parameters = new ArrayList<List<JClass>>(uniqueTypes.size());
        int paramLen = -1;

        for (JType type : uniqueTypes) {
            JClass cls = type.boxify();
            JClass bp = cls.getBaseClass(result);
            // if there's no parameterization in the base type,
            // we won't do any better than <?>. Thus no point in trying to figure out the parameterization.
            // just return the base type.
            if(bp.equals(result))
                return result;

            assert bp.isParameterized();
            List<JClass> tp = bp.getTypeParameters();
            parameters.add(tp);

            assert paramLen==-1 || paramLen==tp.size();
                // since 'bp' always is a parameterized version of 'result', it should always
                // have the same number of parameters.
            paramLen = tp.size();
        }

        List<JClass> paramResult = new ArrayList<JClass>();
        List<JClass> argList = new ArrayList<JClass>(parameters.size());
        // for each type parameter compute the common base type
        for( int i=0; i<paramLen; i++ ) {
            argList.clear();
            for (List<JClass> list : parameters)
                argList.add(list.get(i));

            // compute the lower bound.
            JClass bound = (JClass)getCommonBaseType(codeModel,argList);
            boolean allSame = true;
            for (JClass a : argList)
                allSame &= a.equals(bound);
            if(!allSame)
                bound = bound.wildcard();

            paramResult.add(bound);
        }

        return result.narrow(paramResult);
    }

    private static JClass pickOne(Set<JClass> s) {
        // we may have more than one candidates at this point.
        // any user-defined generated types should have
        // precedence over system-defined existing types.
        //
        // so try to return such a type if any.
        for (JClass c : s)
            if (c instanceof JDefinedClass)
                return c;

        // we can do more if we like. for example,
        // we can avoid types in the RI runtime.
        // but for now, just return the first one.
        return s.iterator().next();
    }

    private static Set<JClass> getAssignableTypes( JClass t ) {
        Set<JClass> r = new TreeSet<JClass>(typeComparator);
        getAssignableTypes(t,r);
        return r;
    }

    /**
     * Returns the set of all classes/interfaces that a given type
     * implements/extends, including itself.
     * 
     * For example, if you pass java.io.FilterInputStream, then the returned
     * set will contain java.lang.Object, java.lang.InputStream, and
     * java.lang.FilterInputStream.
     */
    private static void getAssignableTypes( JClass t, Set<JClass> s ) {
        if(!s.add(t))
            return;

        // add its raw type
        s.add(t.erasure());

        // if this type is added for the first time,
        // recursively process the super class.
        JClass _super = t._extends();
        if(_super!=null)
            getAssignableTypes(_super,s);
        
        // recursively process all implemented interfaces
        Iterator<JClass> itr = t._implements();
        while(itr.hasNext())
            getAssignableTypes(itr.next(),s);
    }

    /**
     * Obtains a {@link JType} object for the string representation
     * of a type.
     */
    public static JType getType( JCodeModel codeModel,
        String typeName, ErrorReceiver errorHandler, Locator errorSource ) {

        try {
            return codeModel.parseType(typeName);
        } catch( ClassNotFoundException ee ) {

            // make it a warning
            errorHandler.warning( new SAXParseException(
                Messages.ERR_CLASS_NOT_FOUND.format(typeName)
                ,errorSource));

            // recover by assuming that it's a class that derives from Object
            return codeModel.directClass(typeName);
        }
    }
    
    /**
     * Compares {@link JType} objects by their names.
     */
    private static final Comparator<JType> typeComparator = new Comparator<JType>() {
        public int compare(JType t1, JType t2) {
            return t1.fullName().compareTo(t2.fullName());
        }
    };
}
