/*
 *                    BioJava development code
 *
 * This code may be freely distributed and modified under the
 * terms of the GNU Lesser General Public Licence.  This should
 * be distributed with the code.  If you do not have a copy,
 * see:
 *
 *      http://www.gnu.org/copyleft/lesser.html
 *
 * Copyright for this code is held jointly by the individual
 * authors.  These should be listed in @author doc comments.
 *
 * For more information on the BioJava project and its aims,
 * or to join the biojava-l mailing list, visit the home page
 * at:
 *
 *      http://www.biojava.org/
 *
 */

package org.biojavax.bio.db.biosql;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.biojavax.DocRefAuthor;
import org.biojavax.RichObjectBuilder;
import org.biojavax.SimpleCrossRef;
import org.biojavax.SimpleDocRef;
import org.biojavax.SimpleNamespace;
import org.biojavax.bio.taxa.SimpleNCBITaxon;
import org.biojavax.ontology.SimpleComparableOntology;


/**
 * Takes requests for RichObjects and sees if it can load them from a Hibernate
 * database. If it can, it returns the loaded objects. Else, it creates them
 * and persists them, then returns them. Doesn't retain a memory map so runs
 * a lot of Hibernate queries if used frequently, but on the plus side this
 * makes it memory-efficient.
 * @author Richard Holland
 * @author David Scott
 * @since 1.5
 */
public class BioSQLRichObjectBuilder implements RichObjectBuilder {
    
    private Object session;
    private Method createQuery;
    private Method setParameter;
    private Method uniqueResult;
    private Method persist;
    
    /**
     * Creates a new instance of SimpleRichObjectBuilder. The session parameter
     * is a Hibernate Session object and must not be null. It is this session
     * that database objects will be retrieved from/persisted to.
     * @see <a href="http://www.hibernate.org/hib_docs/v3/api/org/hibernate/Session.html"> org.hibernate.Session</a>
     */
    public BioSQLRichObjectBuilder(Object session) {
        try {
            // Lazy load the Session class from Hibernate.
            Class hibernateSession = session.getClass();
            Class realHibernateSession = Class.forName("org.hibernate.Session");
            // Test to see if our parameter is really a Session
            if (!realHibernateSession.isAssignableFrom(hibernateSession))
                throw new IllegalArgumentException("Parameter must be a org.hibernate.Session object");
            this.session = session;
            // Lookup the createQuery and persist methods
            this.createQuery = hibernateSession.getMethod("createQuery", new Class[]{String.class});
            this.persist = hibernateSession.getMethod("persist", new Class[]{String.class,Object.class});
            // Lazy load the Query class from Hibernate.
            Class hibernateQuery = Class.forName("org.hibernate.Query");
            // Lookup the setParameter and uniqueQuery methods
            this.setParameter = hibernateQuery.getMethod("setParameter", new Class[]{int.class,Object.class});
            this.uniqueResult = hibernateQuery.getMethod("uniqueResult", new Class[]{});
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * {@inheritDoc}
     * Attempts to look up the details of the object in the database. If it
     * finds them it loads the object and returns it. Else, it persists the
     * wrapper object it made to do the search with and returns that.
     */
    public Object buildObject(Class clazz, List paramsList) {
        // convert the params list to remove nulls as we can't process those.
        List ourParamsList = new ArrayList(paramsList);
        for (Iterator i = ourParamsList.iterator(); i.hasNext(); ) 
        	if (i.next()==null) i.remove();
        // Create the Hibernate query to look it up with
        String queryText;
        String queryType;
        if (SimpleNamespace.class.isAssignableFrom(clazz)) {
            queryText = "from Namespace as ns where ns.name = ?";
            queryType = "Namespace";
        } else if (SimpleComparableOntology.class.isAssignableFrom(clazz)) {
            queryText = "from Ontology as o where o.name = ?";
            queryType = "Ontology";
        } else if (SimpleNCBITaxon.class.isAssignableFrom(clazz)) {
            queryText = "from Taxon as o where o.NCBITaxID = ?";
            queryType = "Taxon";
        } else if (SimpleCrossRef.class.isAssignableFrom(clazz)) {
            queryText = "from CrossRef as cr where cr.dbname = ? and cr.accession = ? and cr.version = ?";
            queryType = "CrossRef";
        } else if (SimpleDocRef.class.isAssignableFrom(clazz)) {
        	queryType = "DocRef";
        	// convert List constructor to String representation for query
        	ourParamsList.set(0, DocRefAuthor.Tools.generateAuthorString((List)ourParamsList.get(0)));
        	if (ourParamsList.size()<3) {
        		queryText = "from DocRef as cr where cr.authors = ? and cr.location = ? and cr.title is null";
        	} else {
        		queryText = "from DocRef as cr where cr.authors = ? and cr.location = ? and cr.title = ?";
        	}        
        } else throw new IllegalArgumentException("Don't know how to handle objects of type "+clazz);
        // Run the query.
        try {
            // Build the query object
            Object query = this.createQuery.invoke(this.session, new Object[]{queryText});
            // Set the parameters
            for (int i = 0; i < ourParamsList.size(); i++) {
                query = this.setParameter.invoke(query, new Object[]{new Integer(i), ourParamsList.get(i)});
            }
            // Get the results
            Object result = this.uniqueResult.invoke(query, null);
            // Return the found object, if found
            if (result!=null) return result;
            // Create, persist and return the new object otherwise
            else {
                // Load the class
                Class[] types = new Class[ourParamsList.size()];
                // Find its constructor with given params
                for (int i = 0; i < ourParamsList.size(); i++) {
                    if (ourParamsList.get(i) instanceof Set) types[i] = Set.class;
                    else if (ourParamsList.get(i) instanceof Map) types[i] = Map.class;
                    else if (ourParamsList.get(i) instanceof List) types[i] = List.class;
                    else types[i] = ourParamsList.get(i).getClass();
                }
                Constructor c = clazz.getConstructor(types);
                // Instantiate it with the parameters
                Object o = c.newInstance(ourParamsList.toArray());
                // Persist and return it.
                this.persist.invoke(this.session, new Object[]{queryType,o});
                return o;
            }
        } catch (Exception e) {
            // Write a useful message explaining what we were trying to do. It will
            // be in the form "class(param,param...)".
            StringBuffer paramsstuff = new StringBuffer();
            paramsstuff.append(clazz);
            paramsstuff.append("(");
            for (int i = 0; i < ourParamsList.size(); i++) {
                if (i>0) paramsstuff.append(",");
            	paramsstuff.append(ourParamsList.get(i).getClass());
            }
            paramsstuff.append(")");
            // Throw the exception with our nice message
            throw new RuntimeException("Error while trying to call new "+paramsstuff,e);
        }
    }
    
}
