/**
 * Copyright (c) 2012, The National Archives <pronom@nationalarchives.gsi.gov.uk>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following
 * conditions are met:
 *
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 *  * Neither the name of the The National Archives nor the
 *    names of its contributors may be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package uk.gov.nationalarchives.droid.profile;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import uk.gov.nationalarchives.droid.core.interfaces.filter.Filter;
import uk.gov.nationalarchives.droid.core.interfaces.filter.expressions.QueryBuilder;
import uk.gov.nationalarchives.droid.profile.referencedata.Format;

/**
 * JPA implementation of ProfileDao.
 * 
 * @author rflitcroft
 */
public class JpaProfileDaoImpl implements ProfileDao {

    private final Log log = LogFactory.getLog(getClass());

    @PersistenceContext
    private EntityManager entityManager;

    /**
     * Flushes the DROID entity manager.
     */
    void flush() {
        entityManager.flush();
    }
    
    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public List<Format> getAllFormats() {
        String query = "from Format order by name";
        Query q = entityManager.createQuery(query);
        return q.getResultList();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void saveFormat(Format format) {
        entityManager.persist(format);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    //@Transactional(propagation = Propagation.REQUIRED)
    public List<ProfileResourceNode> findProfileResourceNodes(Long parentId) {

        //String query = "select n from ProfileResourceNode n"
        //        + " where n.parent.id  " + (parentId == null ? " is null" : " = ? ");

        String query = "select * from profile_resource_node n"
            + " where n.parent_id " + getParentIdQuery(parentId);
        
        //log.debug("query = " + query);

        //Query q = entityManager.createQuery(query);
        Query q = entityManager.createNativeQuery(query, ProfileResourceNode.class);
        
        //log.debug("parent Id  = " + parentId);
        if (parentId != null) {
            q.setParameter(1, parentId);
        }

        //long start = System.currentTimeMillis();
        List<ProfileResourceNode> results = q.getResultList();
        //log.debug("Query time (ms) =  " + (System.currentTimeMillis() - start));

        return results;
    }


    /**
     * {@inheritDoc}
     */
    
    @Override
    @SuppressWarnings("unchecked")
    //@Transactional(propagation = Propagation.REQUIRED)
    public List<ProfileResourceNode> findProfileResourceNodes(Long parentId,
            Filter filter) {
        QueryBuilder queryBuilder = SqlUtils.getQueryBuilder(filter);
        String ejbFilter = queryBuilder.toEjbQl();
        String query = getSQLQueryString(ejbFilter, parentId);
        Query q = entityManager.createNativeQuery(query, "ResourceNodeWithFilterStatus");
        
        // Set the parameters:
        int i = 1;
        Object[] values = queryBuilder.getValues();
        for (int j = 0; j < values.length; j++) {
            Object value = values[j];
            
            // FIXME: ugly hack to transform certain enumerations back into
            // their ordinal values.  For some reason, if putting the enum
            // into the parameter list directly, some work, and some die with
            // an exception trying to set SQLInteger to a byte[] in the derby
            // prepared statement.  Wierdly, NodeStatus and PUID work, but the
            // other enumerations don't.
            value = SqlUtils.transformParameterToSQLValue(value);
            q.setParameter(i++, value);
        }
        for (int j = 0; j < values.length; j++) {
            Object value = values[j];
            
            // FIXME: ugly hack to transform certain enumerations back into
            // their ordinal values.  For some reason, if putting the enum
            // into the parameter list directly, some work, and some die with
            // an exception trying to set SQLInteger to a byte[] in the derby
            // prepared statement.  Wierdly, NodeStatus and PUID work, but the
            // other enumerations don't.
            value = SqlUtils.transformParameterToSQLValue(value);
            q.setParameter(i++, value);
        }
        if (parentId != null) {
            q.setParameter(i++, parentId);
        }
        
        // Get the results:
        long start = System.currentTimeMillis();
        List<Object[]> queryResults = q.getResultList();
        log.debug("Query time (ms) = " + (System.currentTimeMillis() - start));

        // get the profile resource nodes and set their filter status:
        List<ProfileResourceNode> result = new ArrayList<ProfileResourceNode>();
        for (Object[] objects : queryResults) {
            Integer status = (Integer) objects[1];
            if (status > 0) {
                ProfileResourceNode node = (ProfileResourceNode) objects[0];
                node.setFilterStatus(status);
                result.add(node);
            }
        }

        return result;
    }    

   
    private String getSQLQueryString(final String ejbFilter, final Long parentId) {
        boolean formatCriteriaExist = formatCriteriaExist(ejbFilter);
        boolean formatMetadataExist = formatCriteriaExist ? formatMetadataExist(ejbFilter) : false;
        String filterCriteriaDirect = SqlUtils.transformEJBtoSQLFields(ejbFilter,
                "profile", "form");
        String filterCriteriaChild = SqlUtils.transformEJBtoSQLFields(ejbFilter,
                "children", "child_form");
        
        String query = formatCriteriaExist ? "select distinct profile.*," : "select profile.*,";
        query += " case when (" + filterCriteriaDirect + ") then 1"
            + " else case when profile.resource_type <> 2 and exists ("
            + " select children.node_id from profile_resource_node as children";
        
        if (formatCriteriaExist) { // if we have filter conditions on the file formats:
            if (formatMetadataExist) { // join to the format table through identifications.
                query = query + " inner join identification as child_ident on child_ident.node_id = children.node_id"
                    + " inner join format as child_form on child_form.puid = child_ident.puid";
            } else { // a puid-only format query only needs to join to identifications.
                query = query + " inner join identification as child_form on child_form.node_id = children.node_id";
            }
        }

        query = query + " where children.prefix > profile.prefix and children.prefix < profile.prefix_plus_one"
            + " and (" + filterCriteriaChild + ")) then 2 else 0 end end as FilterStatus"
            + " from profile_resource_node as profile";

        if (formatCriteriaExist) { // if we have filter conditions on the file formats:
            if (formatMetadataExist) { // join to the format table through identifications.
                query = query + " inner join identification as ident on ident.node_id = profile.node_id"
                    + " inner join format as form on form.puid = ident.puid";
            } else { // // a puid-only format query only needs to join to identifications.
                query = query + " inner join identification as form on form.node_id = profile.node_id";
            }
        }
            
        return query + " where profile.parent_id " + getParentIdQuery(parentId);
    }
    
    private boolean formatCriteriaExist(final String filter) {
        return filter.contains("format.");
    }

    private boolean formatMetadataExist(final String filter) {
        return filter.contains("format.mimeType") || filter.contains("format.name");
    }
    
    
    private String getParentIdQuery(Long parentId) {
        return parentId == null ? "is null" : " = ?";
    }
    
}
