/*
 * The Kuali Financial System, a comprehensive financial management system for higher education.
 *
 * Copyright 2005-2016 The Kuali Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.kuali.kfs.module.bc.batch.dataaccess.impl;

import org.apache.ojb.broker.metadata.ClassDescriptor;
import org.apache.ojb.broker.metadata.FieldDescriptor;
import org.apache.ojb.broker.metadata.MetadataManager;
import org.apache.ojb.broker.query.Criteria;
import org.apache.ojb.broker.query.QueryByCriteria;
import org.apache.ojb.broker.query.ReportQueryByCriteria;
import org.kuali.kfs.sys.util.TransactionalServiceUtils;
import org.kuali.rice.core.framework.persistence.ojb.dao.PlatformAwareDaoBaseOjb;

import java.util.HashMap;
import java.util.Iterator;


public class BudgetConstructionBatchHelperDaoOjb extends PlatformAwareDaoBaseOjb {

    /**
     * provides methods used throughout budget constuction batch to size hashmaps for efficiency
     * for distinct and group by queries, we try to improve on the count returned by OJB's getCount.
     * in both cases, OJB produces a COUNT(DISTINCT concat(select fields, primary key fields)), which can substantially overcount the rows returned.
     */
    /*
     * ******************************************************************************
     *   These are utility routines used by all the units
     * ******************************************************************************
     */
    //  return the recommended length of a hash map (to avoid collisions but avoid
    //  wasting too much space)
    //**********************************************************
    // our technique of doing joins in Java instead of OJB is going to use a lot of
    // memory.  since memory is a finite resource, we want the garbage collector to
    // remove things no longer in use.  we could use weak hashmaps, but since many of
    // the hashed objects in the globally scoped hashmaps are built within the scope
    // of a method, doing so might cause them to be trashed prematurely.  instead,
    // we instantiate all the hashmaps on declaration with a length of 1 (to avoid
    // null pointers).  then, we instantiate them again on first use with a size
    // determined by the likely number of objects * (1/.75) (see Horstman).  When
    // we are done with the hash objects, we clear them, so the underlying objects
    // are no longer referred to by anything and are fair game for the garbage
    // collector.
    //***********************************************************


    private String ojbPlatform;

    private HashMap<String, String> countDistinctLeader = null;
    private HashMap<String, String> countDistinctTrailer = null;
    private HashMap<String, String> concatSeparator = null;

    // a MAX, say, with a GROUP BY would return one row.  So, would COUNT (DISTINCT 1)
    private Integer DEFAULT_QUERY_RETURN_COUNT = 1;


    /*
     *  takes an OJB queryByCriteria object as input
     *  returns the recommended size of a hashmap that is to be created from the results of the query
     *  (the recommend size is calculated from the number of rows the query is expected to return using a formula
     *   given in the JDK5 HashMap class comments)
     */
    protected Integer hashCapacity(QueryByCriteria queryID) {
        // this corresponds to a little more than the default load factor of .75.
        // a rehash supposedly occurs when the actual number of elements exceeds (load factor)*capacity.
        // N rows < .75 capacity ==> capacity > 4N/3 or 1.3333N.  We add a little slop.
        Double tempValue = ((Number) (getPersistenceBrokerTemplate().getCount(queryID))).floatValue() * (1.45);
        return (Integer) tempValue.intValue();
    }

    /*
     *  takes an OJB reportQueryByCriteria object as input
     *  this is a second version of the overloaded method hashcapacity
     */

    protected Integer hashCapacity(ReportQueryByCriteria queryID) {
        // for a distinct or a group by query, we build our own COUNT(DISTINCT...) from the fields in the SELECT list, because OJB doesn't do this correctly.
        // our method will have less chance of overcounting for Oracle and MySQL.
        // it will default for other DB's at present.
        if (queryID.isDistinct() || (!(queryID.getGroupBy().isEmpty()))) {
            Double tempValue = queryCountDistinct(queryID).floatValue() * (1.45);
            return (Integer) tempValue.intValue();
        }
        //
        // since the query does not contain a DISTINCT or a GROUPBY, we use OJB's getCount.
        // this corresponds to a little more than the default load factor of .75
        // a rehash supposedly occurs when the actual number of elements exceeds
        // (load factor)*capacity
        // N rows < .75 capacity ==> capacity > 4N/3 or 1.3333N.  We add a little slop.
        Double tempValue = ((Number) (getPersistenceBrokerTemplate().getCount(queryID))).floatValue() * (1.45);
        return (Integer) tempValue.intValue();
    }

    protected Integer hashCapacity(Integer hashSize) {
        // this corresponds to a little more than the default load factor of .75
        // a rehash supposedly occurs when the actual number of elements exceeds
        // (load factor)*capacity
        // N rows < .75 capacity ==> capacity > 4N/3 or 1.3333N.  We add a little slop.
        Double tempValue = hashSize.floatValue() * (1.45);
        return (Integer) tempValue.intValue();
    }

    protected Integer hashObjectSize(Class classID, Criteria criteriaID) {
        // this counts all rows
        String[] selectList = new String[]{"COUNT(*)"};
        ReportQueryByCriteria queryID =
            new ReportQueryByCriteria(classID, selectList, criteriaID);
        Iterator resultRows =
            getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID);
        if (resultRows.hasNext()) {
            return (hashCapacity(((Number) ((Object[]) TransactionalServiceUtils.retrieveFirstAndExhaustIterator(resultRows))[0]).intValue()));
        }
        return (new Integer(1));
    }

    protected Integer hashObjectSize(Class classID, Criteria criteriaID,
                                     String propertyName) {
        // this one counts distinct values of a single field
        String[] selectList = buildCountDistinct(propertyName, classID);
        // if the field is not found, return the default
        if (selectList[0] == null) {
            return (new Integer(this.DEFAULT_QUERY_RETURN_COUNT));
        }
        ReportQueryByCriteria queryID =
            new ReportQueryByCriteria(classID, selectList, criteriaID);
        Iterator resultRows =
            getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(queryID);
        if (resultRows.hasNext()) {
            return (hashCapacity(((Number) ((Object[]) TransactionalServiceUtils.retrieveFirstAndExhaustIterator(resultRows))[0]).intValue()));
        }
        return (new Integer(1));
    }

    protected Integer hashObjectSize(Class classID, Criteria criteriaID,
                                     String[] selectList) {
        // this version is designed to do a count of distinct composite key values
        // it is assumed that the key's components can all be concatenated
        // there is apparently no concatenation function that is supported in all
        // versions of SQL (even though there is a standard)
        // OJB's getCount does NOT handle this properly (it counts the number of primary keys)
        // so, we use a helper method to build our own DISTINCT query, based on the OJB platform
        // if the platform is not Oracle or MySQL, we return a default size
        ReportQueryByCriteria queryID =
            new ReportQueryByCriteria(classID, selectList, criteriaID, true);
        return (hashCapacity(queryID));
    }

    protected String[] buildCountDistinct(ReportQueryByCriteria originalQuery) {
        // build the select list element COUNT(DISTINCT from the input query.
        // return an empty array for the SELECT list if this is not possible.
        boolean fieldValueFound = false;
        String[] returnSelectList = {""};
        if (!countDistinctLeader.containsKey(ojbPlatform)) {
            // the ojbPlatform is not registered in this Kuali implementation
            return returnSelectList;
        }
        StringBuilder countDistinctElement = new StringBuilder(500);
        countDistinctElement.append(countDistinctLeader.get(ojbPlatform));
        // now we have to find the DB column names (as opposed to the OJB attribute names) for the fields in the SELECT list
        HashMap<String, String> allFields = getDBFieldNamesForClass(originalQuery.getSearchClass());
        String[] querySelectList = originalQuery.getAttributes();
        for (String attributeName : querySelectList) {
            String columnName = allFields.get(attributeName);
            if (columnName != null) {
                // add a separator if there was a previous column
                if (fieldValueFound) {
                    countDistinctElement.append(concatSeparator.get(ojbPlatform));
                }
                // stick in the new column
                countDistinctElement.append(columnName);
                // indicate that one of the original select entries is a DB field name
                fieldValueFound = true;
            }
        }
        if (!fieldValueFound) {
            // none of the items in the SELECT list is a DB-field, so no COUNT(DISTINCT is possible.
            return returnSelectList;
        }
        countDistinctElement.append(countDistinctTrailer.get(ojbPlatform));
        returnSelectList[0] = countDistinctElement.toString();
        return returnSelectList;
    }

    protected String[] buildCountDistinct(String ojbAttributeName, Class ojbClass) {
        String[] returnSelectList = {""};
        // get the attribute/DB column name map for the class
        HashMap<String, String> allFields = getDBFieldNamesForClass(ojbClass);
        // build a COUNT( DISTINCT wrapper around the DB column name
        String dbColumnName = allFields.get(ojbAttributeName);
        if (dbColumnName == null) {
            // return an empty list if we fail
            return returnSelectList;
        }
        returnSelectList[0] = "COUNT(DISTINCT " + dbColumnName + ")";
        return returnSelectList;
    }

    /**
     * fetch the DB column names for the fields in the class for the query
     *
     * @param ojbClass = class of the query
     * @return hash set of DB column names keyed by OJB attribute name,
     */
    protected HashMap<String, String> getDBFieldNamesForClass(Class ojbClass) {
        ClassDescriptor ojbClassDescriptor = MetadataManager.getInstance().getRepository().getDescriptorFor(ojbClass);
        FieldDescriptor[] fieldDescriptorArray = ojbClassDescriptor.getFieldDescriptions();
        HashMap<String, String> returnSet = new HashMap<String, String>(((Double) (1.34 * fieldDescriptorArray.length)).intValue());
        for (FieldDescriptor fieldInDB : fieldDescriptorArray) {
            returnSet.put(fieldInDB.getAttributeName(), fieldInDB.getColumnName());
        }
        return returnSet;
    }

    /**
     * build a correct, DB-specific COUNT DISTINCT query to indicate how many rows a distinct or GROUP BY query will return.  the default count is returned if this is not possible
     *
     * @param originalQuery = OJB report query for which to find a value for the row count returned
     * @return: number of rows the query should return
     */
    protected Integer queryCountDistinct(ReportQueryByCriteria originalQuery) {
        // for every query with a distinct attribute, or with a group by:
        // we will build a COUNT(DISTINCT ...) with proper concatentation for Oracle and MySQL based on the fields in the select list.
        // for other databases we will simply return a default size
        // for queries that do not have a distinct or group by, we will simply return OJB's getCount
        String[] countDistinctElement = buildCountDistinct(originalQuery);
        // we return the default if there were no field names in the select list
        if (countDistinctElement[0] == null) {
            return (new Integer(this.DEFAULT_QUERY_RETURN_COUNT));
        }
        Class targetClass = originalQuery.getSearchClass();
        Criteria criteriaID = originalQuery.getCriteria();
        ReportQueryByCriteria countQuery = new ReportQueryByCriteria(targetClass, countDistinctElement, criteriaID);
        // run the new COUNT(DISTINCT query in OJB, and return the result
        Iterator resultRows =
            getPersistenceBrokerTemplate().getReportQueryIteratorByQuery(countQuery);
        if (resultRows.hasNext()) {
            return ((Integer) (((Number) ((Object[]) TransactionalServiceUtils.retrieveFirstAndExhaustIterator(resultRows))[0]).intValue()));
        }
        return (new Integer(this.DEFAULT_QUERY_RETURN_COUNT));
    }

    public String getOjbPlatform() {
        return this.ojbPlatform;
    }

    /**
     * initialize the ojbPlatform from the configuration properties
     *
     * @param ojbPlatform = configuration property indicating the DB platform in use
     */
    public void setOjbPlatform(String ojbPlatform) {
        this.ojbPlatform = ojbPlatform;
    }


    /**
     * build the Oracle-specific COUNT (DISTINCT syntax--which is ANSI standard
     *
     * @param ojbOraclePlatform is the Kuali constant matching the configuration property for Oracle
     */
    public void setOjbOraclePlatform(String ojbOraclePlatform) {
        // set up the Oracle-specific syntax in an associative array so it's easy to access
        if (countDistinctLeader == null) {
            countDistinctLeader = new HashMap<String, String>();
        }
        if (countDistinctTrailer == null) {
            countDistinctTrailer = new HashMap<String, String>();
        }
        if (concatSeparator == null) {
            concatSeparator = new HashMap<String, String>();
        }
        countDistinctLeader.put(ojbOraclePlatform, new String("COUNT( DISTINCT "));
        countDistinctTrailer.put(ojbOraclePlatform, new String(")"));
        concatSeparator.put(ojbOraclePlatform, new String("||"));
    }

    /**
     * build the MYSQL-specific COUNT(DISTINCT syntax
     *
     * @param ojbMySqlPlatform is the Kuali constant matching the configuration property for MySQL
     */
    public void setOjbMySqlPlatform(String ojbMySqlPlatform) {
        // set up the MYSQL-specific syntax in an associative array so it's easy to access
        if (countDistinctLeader == null) {
            countDistinctLeader = new HashMap<String, String>();
        }
        if (countDistinctTrailer == null) {
            countDistinctTrailer = new HashMap<String, String>();
        }
        if (concatSeparator == null) {
            concatSeparator = new HashMap<String, String>();
        }
        countDistinctLeader.put(ojbMySqlPlatform, new String("COUNT( DISTINCT CONCAT("));
        countDistinctTrailer.put(ojbMySqlPlatform, new String("))"));
        concatSeparator.put(ojbMySqlPlatform, new String(","));
    }

}
